Skip to main content

submilli_engine/stdlib/agents/
declaration.rs

1//! Type surface of `submilli:agents`.
2//!
3//! Two forms, one per meaning, as in `submilli:llm`. `runText` returns the
4//! agent's final answer as text and is a plain function; a `schema` it takes
5//! is the program's own, sent with the task and never used to check the
6//! answer. `run<T>` returns a checked `T`: it is declared generic and
7//! **runtime-generic**, so the host reads the JSON Schema of `T` from the
8//! `$TypeDescriptor` in the call's `$types` environment and sends it with the
9//! run. `T` has no default. The typechecker gates it through its row in the
10//! checked-call table (`stdlib::checked_call`, matched by mangled name through
11//! [`is_checked_run`]), and the host checks the answer with the descriptor's
12//! predicate, so a mismatch throws a `TypeError` naming the type. See
13//! `stdlib/llm/declaration.rs` for the descriptor and why the written
14//! declaration is the whole public API.
15
16use std::collections::BTreeMap;
17
18use crate::{
19    Dispatch, PackageDeclaration, Param, PropertySig, Span, Type, TypeKind, TypeSymbol, ValueKind,
20    ValueSymbol,
21};
22
23use super::MODULE_NAME;
24
25const RUN: &str = "run";
26const RUN_TEXT: &str = "runText";
27const RUN_TYPE_PARAM: &str = "T";
28
29/// Whether `mangled` is the typed `run`, so the typechecker's gate matches
30/// however the symbol was imported. `runText` is a plain function.
31pub fn is_checked_run(mangled: &crate::MangledName) -> bool {
32    *mangled == crate::mangle::package_symbol(MODULE_NAME, RUN)
33}
34
35pub fn package_declaration() -> PackageDeclaration {
36    let mut defs = PackageDeclaration::with_package(MODULE_NAME);
37    insert_agent_info_interface(&mut defs);
38    insert_fn(
39        &mut defs,
40        RUN_TEXT,
41        Vec::new(),
42        vec![
43            Param::new("agent", Type::String),
44            Param::new("input", Type::String),
45            Param::with_default(
46                "schema",
47                Type::Union(vec![Type::String, Type::Undefined]),
48                crate::DefaultValue::Undefined,
49            ),
50        ],
51        Type::String,
52        "/**\n * Hand `input` to the sub-agent `agent` and return its final answer as \
53         text.\n *\n * `schema`, when given, is a JSON Schema sent with the task as \
54         written; it must be a JSON object, else a catchable \
55         `TypeError`. The answer is not parsed or checked against it; for a checked answer \
56         call `run<T>`, whose schema the compiler emits from `T`.\n *\n * The harness \
57         runs the agent and decides how long it may take and what it may spend. A run \
58         that fails, is cancelled, or names an agent the harness does not have throws \
59         a catchable error; so does a runtime with no agent provider.\n * @param agent \
60         Agent name. Call `list()` for the agents you may use.\n * @param input The \
61         task, as text.\n * @param schema A JSON Schema sent with the task, unchanged; \
62         the answer stays text.\n * @capability agent.run { agent: $agent }\n */",
63    );
64    insert_fn(
65        &mut defs,
66        RUN,
67        vec![RUN_TYPE_PARAM.to_string()],
68        vec![
69            Param::new("agent", Type::String),
70            Param::new("input", Type::String),
71        ],
72        Type::TypeVar(RUN_TYPE_PARAM.to_string()),
73        "/**\n * Hand `input` to the sub-agent `agent` and return its final answer, \
74         checked as a `T`.\n *\n * `run<Report>(agent, input)` emits a JSON Schema for \
75         `T` at compile time and sends it with the task, and the answer is then checked \
76         structurally against `T`. `T` is required: write it, or let the expected type \
77         supply it. For the answer as text call `runText(agent, input)`. An answer that is not JSON throws a catchable `SyntaxError`, and one \
78         that does not match throws a catchable `TypeError`; nothing is coerced.\n *\n * `T` must be a type the schema can \
79         carry: no functions, no `bigint`, no `Uint8Array`, no `unknown`, and no \
80         recursive type.\n *\n * The harness runs the agent and decides how long it may \
81         take and what it may spend. A run that fails, is cancelled, or names an agent \
82         the harness does not have throws a catchable error; so does a runtime with no \
83         agent provider.\n * @param agent Agent name. Call `list()` for the agents you \
84         may use.\n * @param input The task, as text.\n * @capability agent.run { agent: \
85         $agent }\n */",
86    );
87    // `run` reads its `T` as a `$TypeDescriptor` from the generic `$types`
88    // environment; the JSON Schema of `T` travels there.
89    defs.runtime_generics
90        .insert(crate::mangle::package_symbol(MODULE_NAME, RUN));
91    insert_fn(
92        &mut defs,
93        "list",
94        Vec::new(),
95        Vec::new(),
96        Type::Array(Box::new(agent_info_type())),
97        "/**\n * The agents this caller may hand work to.\n *\n * Each agent the harness \
98         offers is filtered under `agent.run` by the same `agent` filter that gates \
99         `run`, so the list never offers an agent the caller would be denied. Nothing \
100         in it reveals how many were filtered out.\n *\n * Treat `description` as \
101         advice: it is operator-authored text.\n * @capability agent.run\n */",
102    );
103    defs
104}
105
106fn agent_info_type() -> Type {
107    Type::InterfaceRef {
108        mangled: crate::mangle::package_symbol(MODULE_NAME, "AgentInfo"),
109        package: crate::Package(MODULE_NAME.to_string()),
110        name: "AgentInfo".to_string(),
111        args: Vec::new(),
112    }
113}
114
115fn insert_agent_info_interface(defs: &mut PackageDeclaration) {
116    let mut properties = BTreeMap::new();
117    insert_property(
118        &mut properties,
119        "name",
120        Type::String,
121        "/** The agent name, exactly as `run` expects it. */",
122    );
123    insert_property(
124        &mut properties,
125        "description",
126        Type::union(vec![Type::String, Type::Undefined]),
127        "/** What the agent is for, or `undefined` when the harness gives none. Operator-authored advice, always a single line within a fixed length bound. */",
128    );
129    defs.types.insert(
130        "AgentInfo".to_string(),
131        TypeSymbol {
132            name: "AgentInfo".to_string(),
133            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "AgentInfo"),
134            declaration_span: Span::at(crate::FileId::AGENTS),
135            kind: TypeKind::Interface {
136                index: None,
137                generics: Default::default(),
138                methods: BTreeMap::new(),
139                properties,
140                dispatch: Dispatch::VTable,
141                doc: crate::doc(
142                    crate::FileId::AGENTS,
143                    "/** One agent this caller may hand work to. Returned by `list()`. */",
144                ),
145            },
146        },
147    );
148}
149
150fn insert_property(
151    properties: &mut BTreeMap<String, PropertySig>,
152    name: &str,
153    ty: Type,
154    doc: &str,
155) {
156    properties.insert(
157        name.to_string(),
158        PropertySig {
159            ty,
160            readonly: true,
161            optional: false,
162            intrinsic: false,
163            doc: crate::doc(crate::FileId::AGENTS, doc),
164        },
165    );
166}
167
168fn insert_fn(
169    defs: &mut PackageDeclaration,
170    name: &str,
171    generics: Vec<String>,
172    params: Vec<Param>,
173    ret: Type,
174    doc: &str,
175) {
176    defs.values.insert(
177        name.to_string(),
178        ValueSymbol {
179            name: name.to_string(),
180            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
181            declaration_span: Span::at(crate::FileId::AGENTS),
182            kind: ValueKind::Function {
183                generics: crate::GenericParameters::unconstrained(generics),
184                params,
185                ret,
186                type_predicate: None,
187                doc: crate::doc(crate::FileId::AGENTS, doc),
188            },
189        },
190    );
191}