submilli_engine/runtime/agents.rs
1//! The embedder-supplied seam for `submilli:agents`: a program hands work to a
2//! sub-agent and gets its result back.
3//!
4//! The engine owns the guest surface, the capability check and the call log;
5//! the harness owns what an agent is and how it runs. Everything that crosses
6//! [`AgentProvider`] is owned, serializable data, so a harness may implement it
7//! in process or forward it over the wire.
8//!
9//! **The provider bounds the run.** The execution timeout is checked only while
10//! Wasm runs, so it does not interrupt a pending host call: a sub-agent run
11//! lasts as long as the provider lets it. Time limits, token spend, recursion
12//! depth and cancellation of a child when its parent goes away are the
13//! provider's to enforce. Dropping the future returned by
14//! [`AgentProvider::run`] is how the engine cancels a run.
15
16use std::future::Future;
17use std::pin::Pin;
18
19use serde::{Deserialize, Serialize};
20
21use crate::stdlib::shared::truncated;
22
23pub const AGENTS_MODULE_NAME: &str = "submilli:agents";
24
25/// One sub-agent run, as the program asked for it.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct AgentRequest {
28 /// The agent's name, exactly as the program wrote it. Not checked against
29 /// [`AgentProvider::agents`]: an unknown name is the provider's
30 /// [`AgentCallError::NotFound`]. The policy decided on it as written, so a
31 /// provider must match it byte for byte — no case folding, Unicode
32 /// normalization or path canonicalization — and must not use it as a path.
33 pub agent: String,
34 /// The task for the agent.
35 pub input: String,
36 /// A JSON Schema the result must satisfy, present when the program wrote a
37 /// type argument. The engine checks the result against the type itself; the
38 /// schema tells the agent what shape to answer in.
39 pub schema: Option<String>,
40 /// The package whose code made the call: `main` for the program itself.
41 pub caller: String,
42}
43
44/// What a finished run hands back.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct AgentOutcome {
47 /// The agent's final answer. When the request carried a schema, this is the
48 /// JSON text of a value meant to satisfy it.
49 pub text: String,
50 /// What the run cost, as far as the provider knows. Recorded in the call
51 /// log; the engine charges nothing for it.
52 pub usage: Option<AgentUsage>,
53}
54
55/// Tokens a run spent. `None` means unknown, never zero.
56#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub struct AgentUsage {
58 pub input_tokens: Option<u64>,
59 pub output_tokens: Option<u64>,
60}
61
62/// One agent a program may hand work to.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct AgentInfo {
65 pub name: String,
66 /// Operator-authored text on what the agent is for. The engine reduces it to
67 /// one bounded line before a program sees it.
68 pub description: Option<String>,
69}
70
71/// Why a run did not produce a result. Every message is safe to show the
72/// program: none carries the input or the agent's output.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub enum AgentCallError {
75 /// No [`AgentProvider`] is wired into the runtime.
76 NotConfigured,
77 /// The harness has no agent of this name.
78 NotFound { agent: String },
79 /// The run started and failed. `message` is a fixed classification chosen by
80 /// the provider, not the agent's output.
81 Failed { agent: String, message: String },
82 /// The run was stopped before it finished.
83 Cancelled { agent: String },
84}
85
86impl std::fmt::Display for AgentCallError {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 Self::NotConfigured => write!(
90 f,
91 "no agent provider is configured: this harness does not run sub-agents"
92 ),
93 Self::NotFound { agent } => write!(
94 f,
95 "no agent named `{}`; call `list()` for the agents you may use",
96 truncated(agent)
97 ),
98 Self::Failed { agent, message } => {
99 write!(f, "agent `{}` failed: {message}", truncated(agent))
100 }
101 Self::Cancelled { agent } => write!(f, "agent `{}` was cancelled", truncated(agent)),
102 }
103 }
104}
105
106impl std::error::Error for AgentCallError {}
107
108/// The harness side of `submilli:agents`.
109pub trait AgentProvider: Send + Sync {
110 /// Run `request.agent` on `request.input` and return its final answer.
111 fn run<'a>(
112 &'a self,
113 request: AgentRequest,
114 ) -> Pin<Box<dyn Future<Output = Result<AgentOutcome, AgentCallError>> + Send + 'a>>;
115
116 /// Every agent the harness offers. The engine removes the ones the policy
117 /// denies to the caller before the program sees the list.
118 fn agents<'a>(
119 &'a self,
120 ) -> Pin<Box<dyn Future<Output = Result<Vec<AgentInfo>, AgentCallError>> + Send + 'a>>;
121}