Skip to main content

submilli_engine/stdlib/agents/
declaration.rs

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