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