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