1pub mod declaration;
17
18use crate::runtime::effects;
19use std::sync::Arc;
20
21use wasmtime::{FuncType, HeapType, Linker, RefType, Val, ValType};
22
23use crate::runtime::StoreData;
24use crate::runtime::agents::{
25 AgentCallError, AgentInfo, AgentOutcome, AgentProvider, AgentRequest,
26};
27use crate::runtime::call_log::{ModelUsage, Payload, Side, record_payload, record_usage};
28use crate::runtime::decision::CallTicket;
29use crate::runtime::fuel;
30use crate::runtime::host::{
31 abi_arg, read_string_arg, register_host_value_async, write_submilli_string_struct,
32};
33use crate::runtime::intrinsic_types::build_intrinsic_types;
34use crate::stdlib::abi::{self, read_type_argument};
35use crate::stdlib::json_schema;
36use crate::stdlib::shared::{
37 check_and_approve_call, check_candidate, permitted_candidates, preflight_listing,
38 running_package, sanitize_description, truncated,
39};
40
41pub use declaration::package_declaration;
42
43pub const MODULE_NAME: &str = crate::runtime::agents::AGENTS_MODULE_NAME;
44
45pub const CAPABILITY: &str = "agent.run";
47
48pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
49 let engine = linker.engine().clone();
50 let intr = build_intrinsic_types(&engine)?;
51 let string = ValType::Ref(RefType::new(
52 false,
53 HeapType::ConcreteStruct(intr.string.clone()),
54 ));
55 let nullable_object = ValType::Ref(RefType::new(
56 true,
57 HeapType::ConcreteStruct(intr.object.clone()),
58 ));
59 let array = ValType::Ref(RefType::new(
60 false,
61 HeapType::ConcreteStruct(intr.array.clone()),
62 ));
63
64 register_host_value_async(
67 linker,
68 MODULE_NAME,
69 crate::mangle::package_symbol(MODULE_NAME, "runText"),
70 FuncType::new(
71 &engine,
72 [string.clone(), string.clone(), nullable_object.clone()],
73 [string.clone()],
74 ),
75 |caller, params| {
76 Box::pin(async move {
77 let agent =
78 read_string_arg(&mut *caller, abi_arg(params, 0)?, "agents.runText (agent)")?;
79 let input =
80 read_string_arg(&mut *caller, abi_arg(params, 1)?, "agents.runText (input)")?;
81 let schema = json_schema::read_written_schema(
82 caller,
83 abi_arg(params, 2)?,
84 "agents.runText",
85 )?;
86 let answer = run(caller, "runText", &agent, input, schema).await?;
87 let text = write_submilli_string_struct(caller, &answer)?;
88 Ok(Val::AnyRef(Some(text.to_anyref())))
89 })
90 },
91 )?;
92 let types = ValType::Ref(RefType::new(
96 false,
97 HeapType::ConcreteArray(intr.object_fields.clone()),
98 ));
99 register_host_value_async(
100 linker,
101 MODULE_NAME,
102 crate::mangle::package_symbol(MODULE_NAME, "run"),
103 FuncType::new(
104 &engine,
105 [string.clone(), string.clone(), types],
106 [nullable_object.clone()],
108 ),
109 |caller, params| {
110 Box::pin(async move {
111 let agent =
112 read_string_arg(&mut *caller, abi_arg(params, 0)?, "agents.run (agent)")?;
113 let input =
114 read_string_arg(&mut *caller, abi_arg(params, 1)?, "agents.run (input)")?;
115 let descriptor = read_type_argument(caller, abi_arg(params, 2)?, 0)?;
116 let schema = descriptor.schema_for("agents.run")?.to_string();
117 let answer = run(caller, "run", &agent, input, Some(schema)).await?;
118 let value = crate::runtime::json::parse_json_as_unknown(
119 caller,
120 &answer,
121 &format!(
122 "agents.run(\"{}\"): the agent's result is not JSON",
123 truncated(&agent)
124 ),
125 )?;
126 if descriptor.accepts(caller, value).await? {
129 return Ok(value);
130 }
131 Err(crate::runtime::host::type_error(format!(
132 "agents.run(\"{}\"): the agent's answer does not match `{}`",
133 truncated(&agent),
134 descriptor.name
135 )))
136 })
137 },
138 )?;
139
140 register_host_value_async(
141 linker,
142 MODULE_NAME,
143 crate::mangle::package_symbol(MODULE_NAME, "list"),
144 FuncType::new(&engine, [], [array]),
145 |caller, _params| Box::pin(async move { list(caller).await }),
146 )?;
147
148 Ok(())
149}
150
151async fn run(
157 caller: &mut wasmtime::Caller<'_, StoreData>,
158 api: &'static str,
159 agent: &str,
160 input: String,
161 schema: Option<String>,
162) -> wasmtime::Result<String> {
163 let ticket = gate(caller, agent).await?;
164 let principal = running_package(caller).map_err(|error| error.into_denial(CAPABILITY))?;
165 let sent = (input.len() + schema.as_deref().map_or(0, str::len)) as u64;
166 record_payload(&*caller, ticket, Side::Request, || {
167 Payload::meta(serde_json::json!({ "op": "run", "agent": agent, "schema": schema }))
168 .with_owned_body(input.clone().into_bytes())
169 .with_size(sent)
170 });
171 let request = AgentRequest {
172 agent: agent.to_string(),
173 input,
174 schema,
175 caller: principal,
176 };
177 let outcome = effects::execute(caller, "agents.run", request, move |caller, _, request| {
178 Box::pin(async move {
179 fuel::charge(&mut *caller, fuel::IO, sent)?;
180 let provider = provider(caller).map_err(|error| fail(caller, ticket, api, &error))?;
181 let outcome = provider
182 .run(request)
183 .await
184 .map_err(|error| fail(caller, ticket, api, &error))?;
185 record_outcome(caller, ticket, &outcome);
186 fuel::settle(&mut *caller, fuel::IO, outcome.text.len() as u64)?;
187 Ok(outcome)
188 })
189 })
190 .await?;
191 Ok(outcome.text)
192}
193
194fn fail(
196 caller: &wasmtime::Caller<'_, StoreData>,
197 ticket: Option<CallTicket>,
198 op: &str,
199 error: &AgentCallError,
200) -> wasmtime::Error {
201 record_payload(caller, ticket, Side::Response, || {
202 Payload::meta(serde_json::json!({ "call_error": call_error_record(error) }))
203 });
204 throw(op, error)
205}
206
207fn record_outcome(
208 caller: &wasmtime::Caller<'_, StoreData>,
209 ticket: Option<CallTicket>,
210 outcome: &AgentOutcome,
211) {
212 let usage = outcome.usage.clone().unwrap_or_default();
213 record_payload(caller, ticket, Side::Response, || {
214 Payload::meta(serde_json::json!({
215 "usage": {
216 "input_tokens": usage.input_tokens,
217 "output_tokens": usage.output_tokens,
218 }
219 }))
220 .with_owned_body(outcome.text.clone().into_bytes())
221 .with_size(outcome.text.len() as u64)
222 });
223 record_usage(
224 caller,
225 ticket,
226 ModelUsage {
227 input_tokens: usage.input_tokens,
228 output_tokens: usage.output_tokens,
229 },
230 );
231}
232
233async fn gate(
237 caller: &mut wasmtime::Caller<'_, StoreData>,
238 agent: &str,
239) -> wasmtime::Result<Option<CallTicket>> {
240 check_and_approve_call(caller, CAPABILITY, gate_context(agent)).await
241}
242
243fn gate_context(agent: &str) -> serde_json::Value {
244 serde_json::json!({ "agent": agent })
245}
246
247async fn list(caller: &mut wasmtime::Caller<'_, StoreData>) -> wasmtime::Result<Val> {
251 preflight_listing(caller, CAPABILITY, &serde_json::json!({ "agent": "" }))?;
252 let candidates = effects::execute(caller, "agents.list", (), |caller, _, ()| {
253 Box::pin(async move {
254 provider(caller)
255 .map_err(|error| throw("list", &error))?
256 .agents()
257 .await
258 .map_err(|e| throw("list", &e))
259 })
260 })
261 .await?;
262 let visible = permitted_candidates(
263 caller,
264 candidates,
265 |agent| agent.name.as_str(),
266 |caller, name| check_candidate(caller, CAPABILITY, gate_context(name)),
267 )?;
268 let mut built = Vec::with_capacity(visible.len());
269 for agent in visible {
270 built.push(build_agent_info(caller, agent)?);
271 }
272 abi::new_array(caller, &built)
273}
274
275fn provider(
279 caller: &wasmtime::Caller<'_, StoreData>,
280) -> Result<Arc<dyn AgentProvider>, AgentCallError> {
281 caller
282 .data()
283 .agent_provider
284 .clone()
285 .ok_or(AgentCallError::NotConfigured)
286}
287
288fn throw(op: &str, error: &AgentCallError) -> wasmtime::Error {
289 wasmtime::Error::msg(format!("agents.{op}: {error}"))
290}
291
292fn call_error_record(error: &AgentCallError) -> serde_json::Value {
294 match error {
295 AgentCallError::NotConfigured => serde_json::json!({ "kind": "not-configured" }),
296 AgentCallError::NotFound { agent } => {
297 serde_json::json!({ "kind": "not-found", "agent": agent })
298 }
299 AgentCallError::Failed { agent, message } => {
300 serde_json::json!({ "kind": "failed", "agent": agent, "message": message })
301 }
302 AgentCallError::Cancelled { agent } => {
303 serde_json::json!({ "kind": "cancelled", "agent": agent })
304 }
305 }
306}
307
308fn build_agent_info(
309 caller: &mut wasmtime::Caller<'_, StoreData>,
310 agent: AgentInfo,
311) -> wasmtime::Result<Val> {
312 let name = write_submilli_string_struct(caller, &agent.name)?;
313 let description = match agent.description.as_deref().and_then(sanitize_description) {
314 Some(text) => Val::AnyRef(Some(
315 write_submilli_string_struct(caller, &text)?.to_anyref(),
316 )),
317 None => crate::runtime::prelude::undefined::value(caller)?,
318 };
319 abi::new_object(
320 caller,
321 std::collections::BTreeMap::from([
322 ("name".to_string(), Val::AnyRef(Some(name.to_anyref()))),
323 ("description".to_string(), description),
324 ]),
325 )
326}