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,
36 read_type_argument, string_field,
37};
38use crate::stdlib::json_schema;
39use crate::stdlib::shared::{
40 check_and_approve_call, check_candidate, permitted_candidates, preflight_listing,
41 running_package, sanitize_description, truncated,
42};
43
44pub use declaration::package_declaration;
45
46pub const MODULE_NAME: &str = crate::runtime::agents::AGENTS_MODULE_NAME;
47
48pub const CAPABILITY: &str = "agent.run";
50
51const A_NAME: usize = 1;
53const A_DESCRIPTION: usize = 2;
54
55const AGENT_INFO_VIEW: HostView = HostView {
56 module: MODULE_NAME,
57 interface: "AgentInfo",
58 backing: agent_info_backing_struct,
59 properties: &[("name", A_NAME), ("description", A_DESCRIPTION)],
60};
61
62pub(crate) const HOST_VIEWS: &[&HostView] = &[&AGENT_INFO_VIEW];
64
65pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
66 let engine = linker.engine().clone();
67 let intr = build_intrinsic_types(&engine)?;
68 let string = ValType::Ref(RefType::new(
69 false,
70 HeapType::ConcreteStruct(intr.string.clone()),
71 ));
72 let nullable_object = ValType::Ref(RefType::new(
73 true,
74 HeapType::ConcreteStruct(intr.object.clone()),
75 ));
76 let array = ValType::Ref(RefType::new(
77 false,
78 HeapType::ConcreteStruct(intr.array.clone()),
79 ));
80
81 register_host_value_async(
84 linker,
85 MODULE_NAME,
86 crate::mangle::package_symbol(MODULE_NAME, "runText"),
87 FuncType::new(
88 &engine,
89 [string.clone(), string.clone(), nullable_object.clone()],
90 [string.clone()],
91 ),
92 |caller, params| {
93 Box::pin(async move {
94 let agent =
95 read_string_arg(&mut *caller, abi_arg(params, 0)?, "agents.runText (agent)")?;
96 let input =
97 read_string_arg(&mut *caller, abi_arg(params, 1)?, "agents.runText (input)")?;
98 let schema = json_schema::read_written_schema(
99 caller,
100 abi_arg(params, 2)?,
101 "agents.runText",
102 )?;
103 let answer = run(caller, "runText", &agent, input, schema).await?;
104 let text = write_submilli_string_struct(caller, &answer)?;
105 Ok(Val::AnyRef(Some(text.to_anyref())))
106 })
107 },
108 )?;
109 let types = ValType::Ref(RefType::new(
113 false,
114 HeapType::ConcreteArray(intr.object_fields.clone()),
115 ));
116 register_host_value_async(
117 linker,
118 MODULE_NAME,
119 crate::mangle::package_symbol(MODULE_NAME, "run"),
120 FuncType::new(
121 &engine,
122 [string.clone(), string.clone(), types],
123 [nullable_object.clone()],
125 ),
126 |caller, params| {
127 Box::pin(async move {
128 let agent =
129 read_string_arg(&mut *caller, abi_arg(params, 0)?, "agents.run (agent)")?;
130 let input =
131 read_string_arg(&mut *caller, abi_arg(params, 1)?, "agents.run (input)")?;
132 let descriptor = read_type_argument(caller, abi_arg(params, 2)?, 0)?;
133 let schema = descriptor.schema_for("agents.run")?.to_string();
134 let answer = run(caller, "run", &agent, input, Some(schema)).await?;
135 let value = crate::runtime::json::parse_json_as_unknown(
136 caller,
137 &answer,
138 &format!(
139 "agents.run(\"{}\"): the agent's result is not JSON",
140 truncated(&agent)
141 ),
142 )?;
143 if descriptor.accepts(caller, value).await? {
146 return Ok(value);
147 }
148 Err(crate::runtime::host::type_error(format!(
149 "agents.run(\"{}\"): the agent's answer does not match `{}`",
150 truncated(&agent),
151 descriptor.name
152 )))
153 })
154 },
155 )?;
156
157 register_host_value_async(
158 linker,
159 MODULE_NAME,
160 crate::mangle::package_symbol(MODULE_NAME, "list"),
161 FuncType::new(&engine, [], [array]),
162 |caller, _params| Box::pin(async move { list(caller).await }),
163 )?;
164
165 let receiver = ValType::Ref(RefType::new(
166 false,
167 HeapType::ConcreteStruct(intr.object.clone()),
168 ));
169 install_field_getters(linker, &engine, &receiver, &AGENT_INFO_VIEW)
170}
171
172async fn run(
178 caller: &mut wasmtime::Caller<'_, StoreData>,
179 api: &'static str,
180 agent: &str,
181 input: String,
182 schema: Option<String>,
183) -> wasmtime::Result<String> {
184 let ticket = gate(caller, agent).await?;
185 let principal = running_package(caller).map_err(|error| error.into_denial(CAPABILITY))?;
186 let sent = (input.len() + schema.as_deref().map_or(0, str::len)) as u64;
187 record_payload(&*caller, ticket, Side::Request, || {
188 Payload::meta(serde_json::json!({ "op": "run", "agent": agent, "schema": schema }))
189 .with_owned_body(input.clone().into_bytes())
190 .with_size(sent)
191 });
192 let request = AgentRequest {
193 agent: agent.to_string(),
194 input,
195 schema,
196 caller: principal,
197 };
198 let outcome = effects::execute(caller, "agents.run", request, move |caller, _, request| {
199 Box::pin(async move {
200 fuel::charge(&mut *caller, fuel::IO, sent)?;
201 let provider = provider(caller).map_err(|error| fail(caller, ticket, api, &error))?;
202 let outcome = provider
203 .run(request)
204 .await
205 .map_err(|error| fail(caller, ticket, api, &error))?;
206 record_outcome(caller, ticket, &outcome);
207 fuel::settle(&mut *caller, fuel::IO, outcome.text.len() as u64)?;
208 Ok(outcome)
209 })
210 })
211 .await?;
212 Ok(outcome.text)
213}
214
215fn fail(
217 caller: &wasmtime::Caller<'_, StoreData>,
218 ticket: Option<CallTicket>,
219 op: &str,
220 error: &AgentCallError,
221) -> wasmtime::Error {
222 record_payload(caller, ticket, Side::Response, || {
223 Payload::meta(serde_json::json!({ "call_error": call_error_record(error) }))
224 });
225 throw(op, error)
226}
227
228fn record_outcome(
229 caller: &wasmtime::Caller<'_, StoreData>,
230 ticket: Option<CallTicket>,
231 outcome: &AgentOutcome,
232) {
233 let usage = outcome.usage.clone().unwrap_or_default();
234 record_payload(caller, ticket, Side::Response, || {
235 Payload::meta(serde_json::json!({
236 "usage": {
237 "input_tokens": usage.input_tokens,
238 "output_tokens": usage.output_tokens,
239 }
240 }))
241 .with_owned_body(outcome.text.clone().into_bytes())
242 .with_size(outcome.text.len() as u64)
243 });
244 record_usage(
245 caller,
246 ticket,
247 ModelUsage {
248 input_tokens: usage.input_tokens,
249 output_tokens: usage.output_tokens,
250 },
251 );
252}
253
254async fn gate(
258 caller: &mut wasmtime::Caller<'_, StoreData>,
259 agent: &str,
260) -> wasmtime::Result<Option<CallTicket>> {
261 check_and_approve_call(caller, CAPABILITY, gate_context(agent)).await
262}
263
264fn gate_context(agent: &str) -> serde_json::Value {
265 serde_json::json!({ "agent": agent })
266}
267
268async fn list(caller: &mut wasmtime::Caller<'_, StoreData>) -> wasmtime::Result<Val> {
272 preflight_listing(caller, CAPABILITY, &serde_json::json!({ "agent": "" }))?;
273 let candidates = effects::execute(caller, "agents.list", (), |caller, _, ()| {
274 Box::pin(async move {
275 provider(caller)
276 .map_err(|error| throw("list", &error))?
277 .agents()
278 .await
279 .map_err(|e| throw("list", &e))
280 })
281 })
282 .await?;
283 let visible = permitted_candidates(
284 caller,
285 candidates,
286 |agent| agent.name.as_str(),
287 |caller, name| check_candidate(caller, CAPABILITY, gate_context(name)),
288 )?;
289 let mut built = Vec::with_capacity(visible.len());
290 for agent in visible {
291 built.push(build_agent_info(caller, agent)?);
292 }
293 abi::new_array(caller, &built)
294}
295
296fn provider(
300 caller: &wasmtime::Caller<'_, StoreData>,
301) -> Result<Arc<dyn AgentProvider>, AgentCallError> {
302 caller
303 .data()
304 .agent_provider
305 .clone()
306 .ok_or(AgentCallError::NotConfigured)
307}
308
309fn throw(op: &str, error: &AgentCallError) -> wasmtime::Error {
310 wasmtime::Error::msg(format!("agents.{op}: {error}"))
311}
312
313fn call_error_record(error: &AgentCallError) -> serde_json::Value {
315 match error {
316 AgentCallError::NotConfigured => serde_json::json!({ "kind": "not-configured" }),
317 AgentCallError::NotFound { agent } => {
318 serde_json::json!({ "kind": "not-found", "agent": agent })
319 }
320 AgentCallError::Failed { agent, message } => {
321 serde_json::json!({ "kind": "failed", "agent": agent, "message": message })
322 }
323 AgentCallError::Cancelled { agent } => {
324 serde_json::json!({ "kind": "cancelled", "agent": agent })
325 }
326 }
327}
328
329fn agent_info_backing_struct(engine: &wasmtime::Engine) -> wasmtime::Result<StructType> {
330 let intr = build_intrinsic_types(engine)?;
331 backing_struct(
332 engine,
333 &intr,
334 vec![
335 string_field(&intr), nullable_object_field(&intr), ],
338 )
339}
340
341fn build_agent_info(
342 caller: &mut wasmtime::Caller<'_, StoreData>,
343 agent: AgentInfo,
344) -> wasmtime::Result<Val> {
345 let name = write_submilli_string_struct(caller, &agent.name)?;
346 let description = match agent.description.as_deref().and_then(sanitize_description) {
347 Some(text) => Val::AnyRef(Some(
348 write_submilli_string_struct(caller, &text)?.to_anyref(),
349 )),
350 None => crate::runtime::prelude::undefined::value(caller)?,
351 };
352 let ty = agent_info_backing_struct(caller.engine())?;
353 abi::new_backing(
354 caller,
355 ty,
356 &[Val::AnyRef(Some(name.to_anyref())), description],
357 )
358}