1pub mod declaration;
17
18use std::sync::Arc;
19
20use wasmtime::{FuncType, HeapType, Linker, RefType, StructType, Val, ValType};
21
22use crate::runtime::StoreData;
23use crate::runtime::agents::{
24 AgentCallError, AgentInfo, AgentOutcome, AgentProvider, AgentRequest,
25};
26use crate::runtime::call_log::{ModelUsage, Payload, Side, record_payload, record_usage};
27use crate::runtime::decision::CallTicket;
28use crate::runtime::fuel;
29use crate::runtime::host::{
30 abi_arg, abi_result, read_string_arg, register_host_fn_async, write_submilli_string_struct,
31};
32use crate::runtime::intrinsic_types::build_intrinsic_types;
33use crate::stdlib::abi::{
34 self, backing_struct, install_field_getters, nullable_object_field, string_field,
35};
36use crate::stdlib::shared::{
37 check_security_call, permitted_candidates, preflight_listing, running_package,
38 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
48const A_NAME: usize = 1;
50const A_DESCRIPTION: usize = 2;
51
52pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
53 let engine = linker.engine().clone();
54 let intr = build_intrinsic_types(&engine)?;
55 let string = ValType::Ref(RefType::new(
56 false,
57 HeapType::ConcreteStruct(intr.string.clone()),
58 ));
59 let nullable_object = ValType::Ref(RefType::new(
60 true,
61 HeapType::ConcreteStruct(intr.object.clone()),
62 ));
63 let array = ValType::Ref(RefType::new(
64 false,
65 HeapType::ConcreteStruct(intr.array.clone()),
66 ));
67
68 register_host_fn_async(
69 linker,
70 MODULE_NAME,
71 crate::mangle::package_symbol(MODULE_NAME, "run"),
72 FuncType::new(
73 &engine,
74 [string.clone(), string.clone(), nullable_object.clone()],
75 [nullable_object.clone()],
77 ),
78 false,
79 |caller, params, results| {
80 Box::pin(async move {
81 let agent =
82 read_string_arg(&mut *caller, abi_arg(params, 0)?, "agents.run (agent)")?;
83 let input =
84 read_string_arg(&mut *caller, abi_arg(params, 1)?, "agents.run (input)")?;
85 let schema = read_optional_string(caller, abi_arg(params, 2)?)?;
86 let result = abi_result(results, 0)?;
87 *result = run(caller, agent, input, schema).await?;
88 Ok(())
89 })
90 },
91 )?;
92
93 register_host_fn_async(
94 linker,
95 MODULE_NAME,
96 crate::mangle::package_symbol(MODULE_NAME, "list"),
97 FuncType::new(&engine, [], [array]),
98 false,
99 |caller, _params, results| {
100 Box::pin(async move {
101 *abi_result(results, 0)? = list(caller).await?;
102 Ok(())
103 })
104 },
105 )?;
106
107 let receiver = ValType::Ref(RefType::new(
108 false,
109 HeapType::ConcreteStruct(intr.object.clone()),
110 ));
111 install_field_getters(
112 linker,
113 MODULE_NAME,
114 "AgentInfo",
115 &engine,
116 &receiver,
117 &[
118 ("name", A_NAME, string),
119 ("description", A_DESCRIPTION, nullable_object),
120 ],
121 )
122}
123
124async fn run(
127 caller: &mut wasmtime::Caller<'_, StoreData>,
128 agent: String,
129 input: String,
130 schema: Option<String>,
131) -> wasmtime::Result<Val> {
132 let ticket = gate(caller, &agent)?;
133 let provider = provider(caller).map_err(|error| fail(caller, ticket, "run", &error))?;
134 let principal = running_package(caller).map_err(|error| error.into_denial(CAPABILITY))?;
135 let sent = (input.len() + schema.as_deref().map_or(0, str::len)) as u64;
136 fuel::charge(&mut *caller, fuel::IO, sent)?;
137 record_payload(&*caller, ticket, Side::Request, || {
138 Payload::meta(serde_json::json!({ "op": "run", "agent": agent, "schema": schema }))
139 .with_owned_body(input.clone().into_bytes())
140 .with_size(sent)
141 });
142 let typed = schema.is_some();
143 let request = AgentRequest {
144 agent: agent.clone(),
145 input,
146 schema,
147 caller: principal,
148 };
149 match provider.run(request).await {
150 Ok(outcome) => {
151 record_outcome(caller, ticket, &outcome);
152 fuel::settle(&mut *caller, fuel::IO, outcome.text.len() as u64)?;
153 fuel::settle_result(caller, |caller| {
154 if typed {
155 crate::runtime::json::parse_json_as_unknown(
156 caller,
157 &outcome.text,
158 &format!(
159 "agents.run(\"{}\"): the agent's result is not JSON",
160 truncated(&agent)
161 ),
162 )
163 } else {
164 let text = write_submilli_string_struct(caller, &outcome.text)?;
165 Ok(Val::AnyRef(Some(text.to_anyref())))
166 }
167 })
168 }
169 Err(error) => Err(fail(caller, ticket, "run", &error)),
170 }
171}
172
173fn fail(
175 caller: &wasmtime::Caller<'_, StoreData>,
176 ticket: Option<CallTicket>,
177 op: &str,
178 error: &AgentCallError,
179) -> wasmtime::Error {
180 record_payload(caller, ticket, Side::Response, || {
181 Payload::meta(serde_json::json!({ "call_error": call_error_record(error) }))
182 });
183 throw(op, error)
184}
185
186fn record_outcome(
187 caller: &wasmtime::Caller<'_, StoreData>,
188 ticket: Option<CallTicket>,
189 outcome: &AgentOutcome,
190) {
191 let usage = outcome.usage.clone().unwrap_or_default();
192 record_payload(caller, ticket, Side::Response, || {
193 Payload::meta(serde_json::json!({
194 "usage": {
195 "input_tokens": usage.input_tokens,
196 "output_tokens": usage.output_tokens,
197 }
198 }))
199 .with_owned_body(outcome.text.clone().into_bytes())
200 .with_size(outcome.text.len() as u64)
201 });
202 record_usage(
203 caller,
204 ticket,
205 ModelUsage {
206 input_tokens: usage.input_tokens,
207 output_tokens: usage.output_tokens,
208 },
209 );
210}
211
212fn gate(
216 caller: &mut wasmtime::Caller<'_, StoreData>,
217 agent: &str,
218) -> wasmtime::Result<Option<CallTicket>> {
219 check_security_call(caller, CAPABILITY, serde_json::json!({ "agent": agent }))
220}
221
222async fn list(caller: &mut wasmtime::Caller<'_, StoreData>) -> wasmtime::Result<Val> {
226 preflight_listing(caller, CAPABILITY, &serde_json::json!({ "agent": "" }))?;
227 let provider = provider(caller).map_err(|error| throw("list", &error))?;
228 let candidates = provider.agents().await.map_err(|e| throw("list", &e))?;
229 let visible = permitted_candidates(
230 caller,
231 candidates,
232 |agent| agent.name.as_str(),
233 |caller, name| gate(caller, name).map(|_| ()),
234 )?;
235 let mut built = Vec::with_capacity(visible.len());
236 for agent in visible {
237 built.push(build_agent_info(caller, agent)?);
238 }
239 abi::new_array(caller, &built)
240}
241
242fn provider(
246 caller: &wasmtime::Caller<'_, StoreData>,
247) -> Result<Arc<dyn AgentProvider>, AgentCallError> {
248 caller
249 .data()
250 .agent_provider
251 .clone()
252 .ok_or(AgentCallError::NotConfigured)
253}
254
255fn throw(op: &str, error: &AgentCallError) -> wasmtime::Error {
256 wasmtime::Error::msg(format!("agents.{op}: {error}"))
257}
258
259fn call_error_record(error: &AgentCallError) -> serde_json::Value {
261 match error {
262 AgentCallError::NotConfigured => serde_json::json!({ "kind": "not-configured" }),
263 AgentCallError::NotFound { agent } => {
264 serde_json::json!({ "kind": "not-found", "agent": agent })
265 }
266 AgentCallError::Failed { agent, message } => {
267 serde_json::json!({ "kind": "failed", "agent": agent, "message": message })
268 }
269 AgentCallError::Cancelled { agent } => {
270 serde_json::json!({ "kind": "cancelled", "agent": agent })
271 }
272 }
273}
274
275fn read_optional_string(
277 caller: &mut wasmtime::Caller<'_, StoreData>,
278 val: &Val,
279) -> wasmtime::Result<Option<String>> {
280 if crate::runtime::prelude::undefined::is_undefined(caller, val)? {
281 return Ok(None);
282 }
283 read_string_arg(caller, val, "agents.run (schema)").map(Some)
284}
285
286fn agent_info_backing_struct(engine: &wasmtime::Engine) -> wasmtime::Result<StructType> {
287 let intr = build_intrinsic_types(engine)?;
288 backing_struct(
289 engine,
290 &intr,
291 vec![
292 string_field(&intr), nullable_object_field(&intr), ],
295 )
296}
297
298fn build_agent_info(
299 caller: &mut wasmtime::Caller<'_, StoreData>,
300 agent: AgentInfo,
301) -> wasmtime::Result<Val> {
302 let name = write_submilli_string_struct(caller, &agent.name)?;
303 let description = match agent.description.as_deref().and_then(sanitize_description) {
304 Some(text) => Val::AnyRef(Some(
305 write_submilli_string_struct(caller, &text)?.to_anyref(),
306 )),
307 None => crate::runtime::prelude::undefined::value(caller)?,
308 };
309 let ty = agent_info_backing_struct(caller.engine())?;
310 abi::new_backing(
311 caller,
312 ty,
313 &[Val::AnyRef(Some(name.to_anyref())), description],
314 )
315}