Skip to main content

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}