1use super::children::ChildKind;
16use super::events::Event;
17use super::reactor::{PendingKind, PendingTool, Runtime, Target};
18use crate::context::ROOT;
19use crate::context::plan::{self, Plan};
20use crate::registry::{Registry, Route};
21use crate::state::now_ms;
22use crate::subagent::protocol::ControlMsg;
23use crate::supervisor::tree::NodeId;
24use serde_json::{Value, json};
25use std::time::Duration;
26
27#[derive(Debug, Clone, Default)]
29pub(crate) struct ToolCaller {
30 pub node: Option<NodeId>,
31 pub req: u64,
32 pub ctx: Option<String>,
33 pub run: Option<String>,
34 pub step: Option<String>,
35 pub principal: Option<String>,
36 pub subagent: Option<String>,
37 pub msg_depth: u32,
41}
42
43impl ToolCaller {
44 fn label(&self) -> String {
45 if let Some(s) = &self.subagent {
46 return format!("subagent:{s}");
47 }
48 if let (Some(r), Some(s)) = (&self.run, &self.step) {
49 return format!("step:{r}/{s}");
50 }
51 format!("ctx:{}", self.ctx.as_deref().unwrap_or(ROOT))
52 }
53 pub(crate) fn context_id(&self) -> String {
55 self.ctx.clone().unwrap_or_else(|| ROOT.to_string())
56 }
57 fn ctx_value(&self, instance: &str) -> Value {
58 json!({"instance": instance, "ctx": self.ctx, "run": self.run, "step": self.step, "principal": self.principal, "subagent": self.subagent})
59 }
60}
61
62pub(crate) enum ToolOutcome {
64 Ready(Value, bool),
65 Deferred(PendingKind),
66 Executing,
68}
69
70impl Runtime {
71 pub(crate) fn on_tool_request(&mut self, node: NodeId, id: u64, name: &str, args: Value) {
73 self.counters.tool_calls += 1;
74 let caller = match self.children.get(node).map(|c| c.kind.clone()) {
75 Some(ChildKind::RootTurn { ctx, msg_depth, .. }) => ToolCaller {
76 node: Some(node),
77 req: id,
78 ctx: Some(ctx.clone()),
79 principal: self.contexts.get(&ctx).and_then(|c| c.principal.clone()),
80 msg_depth,
81 ..Default::default()
82 },
83 Some(ChildKind::StepTurn { run, step, .. }) => ToolCaller {
84 node: Some(node),
85 req: id,
86 run: Some(run.clone()),
87 step: Some(step),
88 ctx: self.runs.get(&run).and_then(|r| r.conversation.clone()),
89 principal: self.runs.get(&run).and_then(|r| r.principal.clone()),
90 msg_depth: self.runs.get(&run).map(|r| r.msg_depth).unwrap_or(0),
91 ..Default::default()
92 },
93 Some(ChildKind::Subagent { handle }) => ToolCaller {
94 node: Some(node),
95 req: id,
96 subagent: Some(handle),
97 ..Default::default()
98 },
99 Some(ChildKind::Think { ctx, .. }) => ToolCaller {
100 node: Some(node),
101 req: id,
102 ctx,
103 ..Default::default()
104 },
105 None => return,
106 };
107 self.log.info("tool.request", json!({"node": node.0, "req": id, "tool": name, "caller": caller.label(), "args": if self.log.content_capture() { args.clone() } else { Value::Null }}));
108 match self.execute_tool(&caller, name, args) {
109 ToolOutcome::Ready(v, err) => self.reply_tool(node, id, v, err),
110 ToolOutcome::Deferred(kind) => {
111 self.activity_park(node, name);
114 self.push_pending(PendingTool {
115 target: Target::Child(node, id),
116 name: name.to_string(),
117 kind,
118 started_ms: now_ms(),
119 });
120 }
121 ToolOutcome::Executing => {}
122 }
123 }
124
125 pub(crate) fn reply_tool(&mut self, node: NodeId, req: u64, result: Value, is_error: bool) {
127 self.log.debug(
128 "tool.reply",
129 json!({"node": node.0, "req": req, "is_error": is_error}),
130 );
131 if !self.children.send(
132 node,
133 &ControlMsg::ToolResult {
134 id: req,
135 result,
136 is_error,
137 },
138 ) {
139 self.log
140 .debug("tool.reply.dropped", json!({"node": node.0, "req": req}));
141 }
142 }
143
144 pub(crate) fn reply(&mut self, target: &Target, result: Value, is_error: bool) {
146 match target {
147 Target::Child(node, req) => self.reply_tool(*node, *req, result, is_error),
148 Target::Step(run, step) => {
149 let (run, step) = (run.clone(), step.clone());
150 let error = is_error.then(|| match &result {
151 Value::String(s) => s.clone(),
152 other => other.to_string(),
153 });
154 self.on_step_done(&run, &step, result, is_error, error, 0);
155 }
156 }
157 }
158
159 pub(crate) fn execute_tool(
161 &mut self,
162 caller: &ToolCaller,
163 name: &str,
164 args: Value,
165 ) -> ToolOutcome {
166 let allowed = match (&caller.subagent, &caller.run) {
168 (Some(_), _) => self
169 .registry
170 .allowed(&crate::registry::Caller::Subagent { allow: None }, name),
171 (None, Some(_)) => self
172 .registry
173 .allowed(&crate::registry::Caller::Workflow, name),
174 _ => self.registry.allowed(&crate::registry::Caller::Root, name),
175 };
176 if !allowed {
177 let reason = match self.registry.get(name) {
178 None => format!("no such tool {name:?}"),
179 Some(t) if t.disabled => format!("tool {name:?} is disabled by configuration"),
180 Some(t) if !t.is_available() => {
181 format!("tool {name:?} has no implementation (map it with tools.overrides)")
182 }
183 Some(_) => format!("tool {name:?} is not granted to {}", caller.label()),
184 };
185 return ToolOutcome::Ready(Value::String(reason), true);
186 }
187 if let Err(e) = self.registry.validate_args(name, &args) {
188 return ToolOutcome::Ready(Value::String(e), true);
189 }
190 if !self.settings.security.policies.is_empty()
195 && let Some(outcome) = self.apply_policy(caller, name, &args)
196 {
197 return outcome;
198 }
199 let route = self.registry.route(name).map(|r| match r {
200 Route::Internal => RouteKind::Internal,
201 Route::Mapped(m) => RouteKind::Mapped(m.clone()),
202 Route::Code => RouteKind::Code,
203 Route::Mcp { server, tool } => RouteKind::Mcp(server.to_string(), tool.to_string()),
204 Route::Workflow { workflow, sync } => RouteKind::Workflow(workflow.to_string(), sync),
205 });
206 let out = match route {
207 None => {
208 ToolOutcome::Ready(Value::String(format!("tool {name:?} is unavailable")), true)
209 }
210 Some(RouteKind::Internal) => self.builtin(caller, name, args),
211 Some(RouteKind::Mapped(m)) => self.run_mapped(caller, name, &m, args),
212 Some(RouteKind::Code) => match crate::tools::call(name, &args) {
213 Some(Ok(v)) => ToolOutcome::Ready(v, false),
214 Some(Err(e)) => ToolOutcome::Ready(Value::String(e), true),
215 None => {
216 ToolOutcome::Ready(Value::String(format!("code tool {name:?} vanished")), true)
217 }
218 },
219 Some(RouteKind::Mcp(server, tool)) => {
220 self.run_mcp_call(caller, name, &server, &tool, args)
221 }
222 Some(RouteKind::Workflow(workflow, sync)) => {
226 let mut wargs = json!({"name": workflow, "inputs": args});
227 if sync {
228 wargs["wait"] = json!(true);
229 }
230 self.workflow_tool(caller, "workflow.run", wargs)
231 }
232 };
233 match out {
235 ToolOutcome::Ready(v, false) => match self.registry.validate_result(name, &v) {
236 Ok(()) => ToolOutcome::Ready(v, false),
237 Err(e) => {
238 self.log
239 .warn("tool.result.schema", json!({"tool": name, "err": e}));
240 ToolOutcome::Ready(Value::String(e), true)
241 }
242 },
243 other => other,
244 }
245 }
246
247 fn run_mapped(
251 &mut self,
252 caller: &ToolCaller,
253 name: &str,
254 m: &crate::registry::Mapping,
255 args: Value,
256 ) -> ToolOutcome {
257 let ctx = caller.ctx_value(&self.instance);
258 let mcp_args = match Registry::map_args(m, &args, &ctx) {
259 Ok(a) => a,
260 Err(e) => return ToolOutcome::Ready(Value::String(e), true),
261 };
262 if let Err(e) = self.service_rate_take(&m.server) {
263 return ToolOutcome::Ready(Value::String(e), true);
264 }
265 let Some(client) = self.mcp.get(&m.server).cloned() else {
266 return ToolOutcome::Ready(
267 Value::String(format!("server {:?} for {name} is not connected", m.server)),
268 true,
269 );
270 };
271 let mapping = m.clone();
272 let tool_name = name.to_string();
273 let mut meta = json!({"agent/idempotency_key": format!("{}/{}#{}", self.instance, caller.label(), caller.req), "agent/instance": self.instance});
277 meta["agent/acting_for"] = json!(
278 caller.principal.clone().unwrap_or_else(|| self
279 .settings
280 .identity
281 .autonomous_id()
282 .to_string())
283 );
284 let labels = self.labels_of(caller.principal.as_deref());
285 if !labels.is_empty() {
286 meta["agent/labels"] = json!(labels);
287 }
288 let timeout = self
289 .settings
290 .mcp
291 .default_timeout
292 .map(|d| d.0)
293 .unwrap_or(Duration::from_secs(60));
294 let tx = self.events_tx.clone();
295 let target = ExecTarget::from(caller);
296 let call_ctx = ctx.clone();
297 std::thread::Builder::new()
298 .name(format!("tool:{tool_name}"))
299 .spawn(move || {
300 let res =
301 client.call_tool_with_meta_within(&mapping.tool, Some(mcp_args), meta, timeout);
302 let (result, is_error) = match res {
303 Ok(r) => {
304 let mut ctx = crate::store::mcp::result_ctx(&r);
306 ctx["args"] = args;
307 ctx["ctx"] = call_ctx;
308 if r.is_error() {
309 (Value::String(format!("{tool_name}: {}", r.text())), true)
310 } else {
311 match Registry::map_result(&mapping, &ctx) {
312 Ok(v) => (v, false),
313 Err(e) => (Value::String(e), true),
314 }
315 }
316 }
317 Err(e) => (
318 Value::String(format!("{tool_name}: transport error: {e}")),
319 true,
320 ),
321 };
322 target.send(&tx, result, is_error);
323 })
324 .ok();
325 ToolOutcome::Executing
326 }
327
328 fn run_mcp_call(
330 &mut self,
331 caller: &ToolCaller,
332 name: &str,
333 server: &str,
334 tool: &str,
335 args: Value,
336 ) -> ToolOutcome {
337 if let Err(e) = self.service_rate_take(server) {
340 return ToolOutcome::Ready(Value::String(e), true);
341 }
342 let Some(client) = self.mcp.get(server).cloned() else {
343 return ToolOutcome::Ready(
344 Value::String(format!("server {server:?} for {name} is not connected")),
345 true,
346 );
347 };
348 let tool = tool.to_string();
349 let mut meta = json!({"agent/idempotency_key": format!("{}/{}#{}", self.instance, caller.label(), caller.req), "agent/instance": self.instance});
353 meta["agent/acting_for"] = json!(
354 caller.principal.clone().unwrap_or_else(|| self
355 .settings
356 .identity
357 .autonomous_id()
358 .to_string())
359 );
360 let labels = self.labels_of(caller.principal.as_deref());
361 if !labels.is_empty() {
362 meta["agent/labels"] = json!(labels);
363 }
364 let timeout = self
365 .settings
366 .mcp
367 .default_timeout
368 .map(|d| d.0)
369 .unwrap_or(Duration::from_secs(60));
370 let tx = self.events_tx.clone();
371 let target = ExecTarget::from(caller);
372 std::thread::Builder::new()
373 .name(format!("mcp:{server}.{tool}"))
374 .spawn(move || {
375 let (result, is_error) =
376 match client.call_tool_with_meta_within(&tool, Some(args), meta, timeout) {
377 Ok(r) => (super::worker::tool_result_value(&r), r.is_error()),
378 Err(e) => (Value::String(format!("transport error: {e}")), true),
379 };
380 target.send(&tx, result, is_error);
381 })
382 .ok();
383 ToolOutcome::Executing
384 }
385
386 fn builtin(&mut self, caller: &ToolCaller, name: &str, args: Value) -> ToolOutcome {
389 let ok = |v: Value| ToolOutcome::Ready(v, false);
390 let err = |e: String| ToolOutcome::Ready(Value::String(e), true);
391 let by = caller.label();
392 match name {
393 "instruction.read" => ok(
395 json!({"text": self.instruction.text, "source": self.instruction.source, "uri": self.instruction.uri, "version": self.instruction.version.to_string()}),
396 ),
397 "instruction.subscribe" => {
398 let uri = args
399 .get("uri")
400 .and_then(Value::as_str)
401 .map(str::to_string)
402 .or_else(|| self.instruction.uri.clone());
403 match uri {
404 None => err(
405 "instruction.subscribe: the instruction is static text; give a uri".into(),
406 ),
407 Some(u) => match self.subscribe_instruction(&u) {
408 Ok(()) => ok(json!({"subscribed": true, "uri": u})),
409 Err(e) => err(e),
410 },
411 }
412 }
413 "memory.get" => match self
415 .memory
416 .get(&self.durable, args["key"].as_str().unwrap_or(""))
417 {
418 Ok(v) => ok(v),
419 Err(e) => err(e),
420 },
421 "memory.set" => {
422 let ttl = match args.get("ttl").and_then(Value::as_str) {
423 Some(t) => match crate::config::parse_duration(t) {
424 Ok(d) => Some(d.as_millis() as u64),
425 Err(e) => return err(format!("memory.set: ttl: {e}")),
426 },
427 None => None,
428 };
429 match self.memory.set(
430 &self.durable,
431 args["key"].as_str().unwrap_or(""),
432 args["value"].clone(),
433 ttl,
434 Some(&by),
435 ) {
436 Ok(v) => ok(v),
437 Err(e) => err(e),
438 }
439 }
440 "memory.list" => match self.memory.list(
441 &self.durable,
442 args.get("prefix").and_then(Value::as_str),
443 args.get("limit")
444 .and_then(Value::as_u64)
445 .map(|l| l as usize),
446 ) {
447 Ok(v) => ok(v),
448 Err(e) => err(e),
449 },
450 "memory.push" => match self.memory.push(
451 &self.durable,
452 args["key"].as_str().unwrap_or(""),
453 args.get("value").cloned().unwrap_or(Value::Null),
454 Some(&by),
455 ) {
456 Ok(v) => ok(v),
457 Err(e) => err(e),
458 },
459 "memory.shift" => match self.memory.shift(
460 &self.durable,
461 args["key"].as_str().unwrap_or(""),
462 Some(&by),
463 ) {
464 Ok(v) => ok(v),
465 Err(e) => err(e),
466 },
467 "memory.pop" => {
468 match self
469 .memory
470 .pop(&self.durable, args["key"].as_str().unwrap_or(""), Some(&by))
471 {
472 Ok(v) => ok(v),
473 Err(e) => err(e),
474 }
475 }
476 "memory.delete" => match self
477 .memory
478 .delete(&self.durable, args["key"].as_str().unwrap_or(""))
479 {
480 Ok(v) => ok(v),
481 Err(e) => err(e),
482 },
483 "artifact.create" => {
485 let content = match (
486 args.get("content"),
487 args.get("from_step").and_then(Value::as_str),
488 ) {
489 (Some(c), _) => c.clone(),
490 (None, Some(step)) => match caller
491 .run
492 .as_ref()
493 .and_then(|r| self.runs.get(r))
494 .and_then(|r| r.steps.get(step))
495 .and_then(|s| s.output.clone())
496 {
497 Some(o) => o,
498 None => {
499 return err(format!(
500 "artifact.create: from_step {step:?} has no output"
501 ));
502 }
503 },
504 (None, None) => {
505 return err("artifact.create: content or from_step is required".into());
506 }
507 };
508 let owner = caller.run.clone().or_else(|| caller.ctx.clone());
509 match self.artifacts.create(
510 &self.durable,
511 super::artifacts::NewArtifact {
512 name: args["name"].as_str().unwrap_or(""),
513 mime: args.get("mime").and_then(Value::as_str),
514 content,
515 created_by: Some(&by),
516 sensitive: args
517 .get("sensitive")
518 .and_then(Value::as_bool)
519 .unwrap_or(false),
520 owner: owner.as_deref(),
521 },
522 ) {
523 Ok(v) => ok(v),
524 Err(e) => err(e),
525 }
526 }
527 "artifact.get" => match self.artifacts.get_value(args["id"].as_str().unwrap_or("")) {
528 Ok(v) => ok(v),
529 Err(e) => err(e),
530 },
531 "artifact.delete" => match self
532 .artifacts
533 .delete(&self.durable, args["id"].as_str().unwrap_or(""))
534 {
535 Ok(v) => ok(v),
536 Err(e) => err(e),
537 },
538 "artifact.list" => ok(self.artifacts.list(
539 args.get("prefix").and_then(Value::as_str),
540 args.get("limit")
541 .and_then(Value::as_u64)
542 .map(|l| l as usize),
543 None,
544 )),
545 "plan.create" => {
547 let ctx_id = caller.context_id();
548 let max = self
549 .settings
550 .context
551 .plan
552 .max_items
553 .unwrap_or(plan::DEFAULT_MAX_ITEMS as u32) as usize;
554 let items: Vec<Value> = args["items"].as_array().cloned().unwrap_or_default();
555 match Plan::create(args["goal"].as_str().unwrap_or(""), &items, max) {
556 Ok(p) => {
557 let v = p.to_value();
558 self.context_for(&ctx_id, caller.principal.as_deref()).plan = Some(p);
559 self.context_for(&ctx_id, caller.principal.as_deref())
560 .touch();
561 self.log
562 .info("plan.updated", json!({"ctx": ctx_id, "op": "create"}));
563 ok(v)
564 }
565 Err(e) => err(e),
566 }
567 }
568 "plan.get" => {
569 let ctx_id = caller.context_id();
570 match self.contexts.get(&ctx_id).and_then(|c| c.plan.as_ref()) {
571 Some(p) => ok(json!({"plan": p.to_value(), "progress": p.progress()})),
572 None => ok(json!({"plan": null, "progress": "no plan"})),
573 }
574 }
575 "plan.update" => {
576 let ctx_id = caller.context_id();
577 let max = self
578 .settings
579 .context
580 .plan
581 .max_items
582 .unwrap_or(plan::DEFAULT_MAX_ITEMS as u32) as usize;
583 let c = self.context_for(&ctx_id, caller.principal.as_deref());
584 match c.plan.as_mut() {
585 None => err("plan.update: no plan (call plan.create first)".into()),
586 Some(p) => match p.update(&args, max) {
587 Ok(()) => {
588 let mut v = p.to_value();
589 v["progress"] = json!(p.progress());
590 c.touch();
591 self.log
592 .info("plan.updated", json!({"ctx": ctx_id, "op": "update"}));
593 ok(v)
594 }
595 Err(e) => err(e),
596 },
597 }
598 }
599 "plan.clear" => {
600 let ctx_id = caller.context_id();
601 let c = self.context_for(&ctx_id, caller.principal.as_deref());
602 let had = c.plan.take().is_some();
603 c.touch();
604 self.log
605 .info("plan.updated", json!({"ctx": ctx_id, "op": "clear"}));
606 ok(json!({"ok": had}))
607 }
608 "skills.list" => ok(self.skills.list_value()),
610 "skills.load" => {
611 let ctx_id = caller.context_id();
612 let name = args["name"].as_str().unwrap_or("").to_string();
613 let mcp = self.mcp.clone();
614 let resolver = move |server: &str| -> Option<
615 std::sync::Arc<dyn crate::context::skills::SkillServer>,
616 > {
617 mcp.get(server).map(|c| {
618 c.clone() as std::sync::Arc<dyn crate::context::skills::SkillServer>
619 })
620 };
621 match self
622 .skills
623 .load(&name, args.get("arguments").cloned(), &resolver)
624 {
625 Ok(body) => {
626 let max_loaded = self.settings.skills.max_loaded.unwrap_or(8) as usize;
627 let c = self.context_for(&ctx_id, caller.principal.as_deref());
628 match c.load_skill(&name, &body.hash, max_loaded) {
629 Ok(()) => ok(
630 json!({"loaded": true, "name": name, "hash": body.hash, "body": body.body}),
631 ),
632 Err(e) => err(e),
633 }
634 }
635 Err(e) => err(e),
636 }
637 }
638 "skills.unload" => {
639 let ctx_id = caller.context_id();
640 let c = self.context_for(&ctx_id, caller.principal.as_deref());
641 ok(json!({"ok": c.unload_skill(args["name"].as_str().unwrap_or(""))}))
642 }
643 "status" => ok(self.status_value()),
645 "sleep" => {
647 let d = match crate::config::parse_duration(args["duration"].as_str().unwrap_or(""))
648 {
649 Ok(d) => d,
650 Err(e) => return err(format!("sleep: {e}")),
651 };
652 let deadline = now_ms() + d.as_millis() as u64;
653 let owner = match caller.node {
654 Some(n) => {
655 json!({"kind": "tool", "node": n.0, "req": caller.req, "tool": "sleep"})
656 }
657 None => json!({"kind": "step", "run": caller.run, "step": caller.step}),
658 };
659 match self.timers.arm(
660 &self.durable,
661 deadline,
662 owner,
663 json!({"slept_ms": d.as_millis() as u64}),
664 ) {
665 Ok(id) => ToolOutcome::Deferred(PendingKind::Timer { id }),
666 Err(e) => err(format!("sleep: {e}")),
667 }
668 }
669 "await" => {
670 let cond = args["condition"].as_str().unwrap_or("").to_string();
671 if let Err(e) =
672 crate::cel::compile_check(cond.trim().trim_start_matches("CEL:").trim())
673 {
674 return err(format!("await: {e}"));
675 }
676 let timeout = args
677 .get("timeout")
678 .and_then(Value::as_str)
679 .and_then(|t| crate::config::parse_duration(t).ok())
680 .unwrap_or(Duration::from_secs(600));
681 ToolOutcome::Deferred(PendingKind::Await {
682 condition: cond,
683 deadline_ms: now_ms() + timeout.as_millis() as u64,
684 })
685 }
686 "context.compact" => {
688 let ctx_id = caller.context_id();
689 let keep_last = args
690 .get("keep_last")
691 .and_then(Value::as_u64)
692 .map(|k| k as usize)
693 .unwrap_or(self.settings.context.keep_last.unwrap_or(12) as usize);
694 let target = args.get("target_tokens").and_then(Value::as_u64);
695 match caller.node {
696 Some(node) => {
697 self.start_compaction(&ctx_id, keep_last, target, Some((node, caller.req)));
698 ToolOutcome::Deferred(PendingKind::Think {
699 child: NodeId(u64::MAX),
700 })
701 }
702 None => err("context.compact needs a calling turn".into()),
703 }
704 }
705 "think" => match caller.node {
706 Some(node) => match self.start_think(caller, &args, Some((node, caller.req))) {
707 Ok(child) => ToolOutcome::Deferred(PendingKind::Think { child }),
708 Err(e) => err(e),
709 },
710 None => err("think as a step is the `think` kind".into()),
711 },
712 "finish" => {
714 ok(json!({"ok": true}))
718 }
719 "ask_human" => self.ask_human_tool(caller, args),
723 "subagent.run" | "subagent.send" | "subagent.kill" | "subagent.status"
725 | "subagent.await" | "subagent.list" | "subagent.retire" => {
726 self.subagent_tool(caller, name, args)
727 }
728 "message.send" => self.message_send_tool(caller, args),
730 "workflow.run" | "workflow.list" | "workflow.status" | "workflow.cancel"
732 | "workflow.wait" | "workflow.create" | "workflow.update" | "workflow.delete"
733 | "workflow.pause" | "workflow.resume" | "workflow.signal" => {
734 self.workflow_tool(caller, name, args)
735 }
736 #[cfg(feature = "exec")]
738 "exec" => self.exec_tool(caller, args),
739 other => err(format!(
740 "internal tool {other:?} has no built-in implementation"
741 )),
742 }
743 }
744
745 pub(crate) fn policy_caller(caller: &ToolCaller) -> crate::config::v2::PolicyCaller {
747 use crate::config::v2::PolicyCaller;
748 match (&caller.subagent, &caller.run) {
749 (Some(_), _) => PolicyCaller::Subagent,
750 (None, Some(_)) => PolicyCaller::Workflow,
751 _ => PolicyCaller::Root,
752 }
753 }
754
755 fn apply_policy(
757 &mut self,
758 caller: &ToolCaller,
759 name: &str,
760 args: &Value,
761 ) -> Option<ToolOutcome> {
762 use crate::config::v2::PolicyAction;
763 let tags = self
764 .registry
765 .tags_of(std::slice::from_ref(&name.to_string()));
766 let who = Self::policy_caller(caller);
767 let call = crate::sec::policy::Call {
768 tool: name,
769 tags: &tags,
770 caller: who,
771 principal: caller.principal.as_deref(),
772 args,
773 };
774 let verdict = match crate::sec::policy::evaluate(&self.settings.security.policies, &call) {
775 Ok(None) => return None,
776 Ok(Some(v)) => v,
777 Err(rule) => {
778 self.log.error(
780 "tool.policy.error",
781 json!({"tool": name, "rule": rule, "caller": caller.label()}),
782 );
783 return Some(ToolOutcome::Ready(
784 Value::String(format!(
785 "refused: security.policies[{rule}] has an argument guard that could not be evaluated"
786 )),
787 true,
788 ));
789 }
790 };
791 match verdict.action {
792 PolicyAction::Allow => None,
793 PolicyAction::Deny | PolicyAction::Shadow => {
794 let held = verdict.action == PolicyAction::Shadow;
795 self.log.info(
796 "tool.policy.refused",
797 json!({"tool": name, "rule": verdict.rule, "caller": caller.label(),
798 "action": if held { "shadow" } else { "deny" }}),
799 );
800 self.audit(super::audit::AuditEvent {
801 action: "tool.policy",
802 target: json!({"tool": name, "rule": verdict.rule}),
803 outcome: if held { "shadow" } else { "deny" },
804 principal: caller.principal.as_deref(),
805 role: None,
806 request_id: None,
807 });
808 let msg = if held {
814 format!(
815 "held by security.policies[{}]: this call was NOT executed and no result exists. \
816 Treat it as not done — do not assume an outcome.",
817 verdict.rule
818 )
819 } else {
820 format!("denied by security.policies[{}]", verdict.rule)
821 };
822 Some(ToolOutcome::Ready(Value::String(msg), true))
823 }
824 PolicyAction::Ask => Some(self.policy_gate(caller, name, args, &verdict)),
825 }
826 }
827
828 fn policy_gate(
836 &mut self,
837 caller: &ToolCaller,
838 name: &str,
839 args: &Value,
840 verdict: &crate::sec::policy::Verdict,
841 ) -> ToolOutcome {
842 use crate::config::v2::PolicyAction;
843 let question = verdict
844 .question
845 .clone()
846 .unwrap_or_else(|| {
847 format!(
848 "{} wants to call {name} — allow?",
849 crate::sec::policy::caller_name(Self::policy_caller(caller))
850 )
851 })
852 .replace("{{tool}}", name)
853 .replace(
854 "{{caller}}",
855 crate::sec::policy::caller_name(Self::policy_caller(caller)),
856 )
857 .replace("{{args}}", &args.to_string());
858 #[cfg(feature = "a2a")]
859 let available = self.settings.interface.enabled && self.a2a_sink.is_some();
860 #[cfg(not(feature = "a2a"))]
861 let available = false;
862 if available {
863 self.log.info(
864 "tool.policy.ask",
865 json!({"tool": name, "rule": verdict.rule, "caller": caller.label()}),
866 );
867 #[cfg(feature = "a2a")]
868 {
869 let deadline =
870 now_ms() + verdict.timeout_ms.unwrap_or(super::human::ASK_TIMEOUT_MS);
871 return self.human_gate(caller, question, deadline, None, None);
876 }
877 }
878 let fallback = verdict.on_timeout;
883 self.log.warn(
886 "tool.policy.unanswerable",
887 json!({"tool": name, "rule": verdict.rule, "question": question,
888 "fallback": format!("{fallback:?}").to_lowercase(),
889 "note": "no human channel (interface.enabled is off)"}),
890 );
891 if fallback == PolicyAction::Allow {
892 return ToolOutcome::Ready(Value::Null, false);
893 }
894 ToolOutcome::Ready(
895 Value::String(format!(
896 "denied by security.policies[{}]: a person had to approve this call and no human channel is attached",
897 verdict.rule
898 )),
899 true,
900 )
901 }
902
903 fn message_send_tool(&mut self, caller: &ToolCaller, args: Value) -> ToolOutcome {
917 let err = |e: String| ToolOutcome::Ready(Value::String(e), true);
918 let text = args["text"].as_str().unwrap_or("").trim().to_string();
919 if text.is_empty() {
920 return err("message.send: text is required".into());
921 }
922 let to = args["to"].as_str().unwrap_or(ROOT).trim();
923 let ctx = if to.eq_ignore_ascii_case("new") {
924 format!("msg-{}", crate::state::ulid::new())
925 } else if to.is_empty() {
926 ROOT.to_string()
927 } else {
928 to.to_string()
929 };
930 if caller.ctx.as_deref() == Some(ctx.as_str()) {
931 return err(format!(
932 "message.send: {ctx:?} is this caller's own conversation — a turn cannot message itself"
933 ));
934 }
935 let depth = caller.msg_depth + 1;
936 let cap = self.settings.limits.message_depth();
937 if depth > cap {
938 self.log.warn(
939 "message.too_deep",
940 json!({"caller": caller.label(), "conversation": ctx, "depth": depth, "max": cap}),
941 );
942 return err(format!(
943 "message.send refused: {depth} chained deliveries exceeds limits.max_message_depth ({cap})"
944 ));
945 }
946 let payload = json!({"text": text, "context_id": ctx, "msg_depth": depth});
947 match self.accept_event(
948 super::events::kinds::A2A_MESSAGE,
949 caller.principal.clone(),
950 payload,
951 ) {
952 Ok(_) => ToolOutcome::Ready(
953 json!({"delivered": true, "conversation": ctx, "depth": depth}),
954 false,
955 ),
956 Err(e) => err(format!("message.send: {e}")),
957 }
958 }
959
960 #[cfg(feature = "exec")]
965 fn exec_tool(&mut self, caller: &ToolCaller, args: Value) -> ToolOutcome {
966 use super::exec;
967 let cfg = self.settings.security.exec.clone();
968 if !cfg.enabled {
969 return ToolOutcome::Ready(
970 Value::String("exec: local execution is disabled (security.exec.enabled)".into()),
971 true,
972 );
973 }
974 let cmd = args["cmd"].as_str().unwrap_or_default().to_string();
975 if cmd.is_empty() {
976 return ToolOutcome::Ready(Value::String("exec: `cmd` is required".into()), true);
977 }
978 if !cfg.allow.iter().any(|a| a == &cmd) {
980 return ToolOutcome::Ready(
981 Value::String(format!(
982 "exec: command {cmd:?} is not in security.exec.allow"
983 )),
984 true,
985 );
986 }
987 let Some(workdir) = cfg.workdir.clone() else {
988 return ToolOutcome::Ready(
989 Value::String("exec: security.exec.workdir must be set".into()),
990 true,
991 );
992 };
993 let cwd = match exec::resolve_cwd(std::path::Path::new(&workdir), args["cwd"].as_str()) {
994 Ok(c) => c,
995 Err(e) => return ToolOutcome::Ready(Value::String(format!("exec: {e}")), true),
996 };
997 let argv: Vec<String> = args["args"]
998 .as_array()
999 .map(|a| {
1000 a.iter()
1001 .filter_map(|v| v.as_str().map(String::from))
1002 .collect()
1003 })
1004 .unwrap_or_default();
1005 let stdin = args["cmd_stdin"]
1006 .as_str()
1007 .or_else(|| args["stdin"].as_str())
1008 .map(String::from);
1009 let max_timeout = cfg.timeout.map(|d| d.0).unwrap_or(Duration::from_secs(30));
1011 let req = args["timeout"]
1012 .as_str()
1013 .and_then(|s| crate::config::parse_duration(s).ok());
1014 let timeout = req.map(|d| d.min(max_timeout)).unwrap_or(max_timeout);
1015 let max_output = cfg.max_output.unwrap_or(1 << 20) as usize;
1016 let env_pass = cfg.env.clone();
1017
1018 self.log.info(
1019 "exec.run",
1020 json!({"cmd": cmd, "argc": argv.len(), "cwd": cwd.display().to_string(), "timeout_ms": timeout.as_millis() as u64, "caller": caller.label()}),
1021 );
1022 let tx = self.events_tx.clone();
1023 let target = ExecTarget::from(caller);
1024 std::thread::Builder::new()
1025 .name("tool:exec".into())
1026 .spawn(move || {
1027 let (result, is_error) = match exec::run_command(
1028 &cmd,
1029 &argv,
1030 &cwd,
1031 stdin.as_deref(),
1032 timeout,
1033 max_output,
1034 &env_pass,
1035 ) {
1036 Ok(v) => (v, false),
1037 Err(e) => (Value::String(format!("exec: {e}")), true),
1038 };
1039 target.send(&tx, result, is_error);
1040 })
1041 .ok();
1042 ToolOutcome::Executing
1043 }
1044
1045 pub(crate) fn context_for(
1047 &mut self,
1048 ctx_id: &str,
1049 principal: Option<&str>,
1050 ) -> &mut crate::context::ContextState {
1051 if ctx_id == ROOT {
1052 self.contexts.root()
1053 } else {
1054 self.contexts.conversation(ctx_id, principal)
1055 }
1056 }
1057
1058 pub(crate) fn start_think(
1060 &mut self,
1061 caller: &ToolCaller,
1062 args: &Value,
1063 reply_to: Option<(NodeId, u64)>,
1064 ) -> Result<NodeId, String> {
1065 let prompt = args["prompt"].as_str().unwrap_or("").to_string();
1066 if prompt.trim().is_empty() {
1067 return Err("think: prompt must be non-empty".into());
1068 }
1069 let ctx_id = caller.context_id();
1070 let mut messages = Vec::new();
1071 if let Some(reads) = args.get("reads").and_then(Value::as_array) {
1073 for k in reads.iter().filter_map(Value::as_str) {
1074 if let Ok(v) = self.memory.get(&self.durable, k)
1075 && v["found"] == json!(true)
1076 {
1077 messages.push(crate::context::Msg::system(format!(
1078 "memory[{k}] = {}",
1079 v["value"]
1080 )));
1081 }
1082 }
1083 }
1084 messages.push(crate::context::Msg::user(prompt, None));
1085 let output_schema = args.get("output_schema").cloned();
1086 let system = format!(
1087 "You are the reasoning module of {}. Think carefully about the request and reply with {}. No tools are available.",
1088 self.instance,
1089 if output_schema.is_some() {
1090 "ONLY one JSON object matching the schema"
1091 } else {
1092 "your conclusion (a JSON object when the request asks for structure)"
1093 }
1094 );
1095 let spec = crate::subagent::protocol::TurnSpec {
1096 kind: crate::subagent::protocol::TurnKind::Think,
1097 system,
1098 messages,
1099 tools: Vec::new(),
1100 internal: Vec::new(),
1101 mcp_routes: Default::default(),
1102 output_schema,
1103 max_rounds: 3,
1104 budget_admission: self.governor.is_active(),
1105 idempotency_prefix: String::new(),
1106 tool_meta: None,
1107 temperature: Some(0.0),
1108 max_tokens_per_call: 0,
1109 turn_id: self.next_id("think"),
1110 };
1111 let launch = super::turns::TurnLaunch {
1112 spec,
1113 kind: ChildKind::Think {
1114 purpose: "tool".into(),
1115 ctx: Some(ctx_id.clone()),
1116 reply_to,
1117 extra: Value::Null,
1118 reservation: None,
1119 },
1120 servers: Vec::new(),
1121 max_steps: 4,
1122 max_tokens: self.settings.limits.run.tokens(),
1123 deadline_ms: 300_000,
1124 agent_path: format!("think/{ctx_id}"),
1125 model: None,
1126 };
1127 self.spawn_turn(launch)
1128 }
1129
1130 pub(crate) fn poll_pending(&mut self) {
1134 if self.pending.is_empty() {
1135 return;
1136 }
1137 let now = now_ms();
1138 let mut done: Vec<(Target, Value, bool)> = Vec::new();
1147 for p in self.pending.iter() {
1148 let t = &p.target;
1149 match &p.kind {
1150 PendingKind::Await { condition, deadline_ms } => {
1151 let data = self.await_data();
1152 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
1153 match crate::cel::eval_bool(condition.trim().trim_start_matches("CEL:").trim(), &vars) {
1154 Ok(true) => done.push((t.clone(), json!({"satisfied": true}), false)),
1155 Ok(false) if now >= *deadline_ms => done.push((t.clone(), json!({"satisfied": false, "timed_out": true}), false)),
1156 Ok(false) => {}
1157 Err(e) => done.push((t.clone(), Value::String(format!("await: {e}")), true)),
1158 }
1159 }
1160 PendingKind::Run { run, deadline_ms } => match self.runs.get(run) {
1161 Some(r) if r.status.is_terminal() => done.push((t.clone(), json!({"run": run, "status": r.status, "output": r.output, "error": r.error}), false)),
1162 Some(_) if now >= *deadline_ms => done.push((t.clone(), json!({"run": run, "status": "running", "timed_out": true}), false)),
1163 Some(_) => {}
1164 None => done.push((t.clone(), Value::String(format!("run {run:?} does not exist")), true)),
1165 },
1166 PendingKind::Subagent { handle } => {
1167 if let Some(s) = self.subagents.get(handle)
1173 && (super::reactor::is_terminal_status(&s.status)
1174 || (s.tier.as_deref() == Some("instance")
1175 && s.mode == "sync"
1176 && s.result.is_some()))
1177 {
1178 done.push((t.clone(), json!({"handle": handle, "status": s.status, "result": s.result, "error": s.error}), false));
1179 }
1180 }
1181 PendingKind::Timer { .. }
1183 | PendingKind::Think { .. }
1184 | PendingKind::Human { .. } => {}
1185 }
1186 }
1187 for (target, v, e) in done {
1188 let len = self.pending.len();
1193 self.pending.retain(|p| p.target != target);
1194 if self.pending.len() == len {
1195 continue;
1196 }
1197 self.reply(&target, v, e);
1198 }
1199 self.poll_pending_human();
1200 }
1201
1202 fn await_data(&self) -> crate::engine::template::Data {
1204 let mut d = crate::engine::template::Data::new();
1205 d.insert(
1206 "runs".into(),
1207 Value::Object(
1208 self.runs
1209 .iter()
1210 .map(|(k, r)| (k.clone(), json!({"status": r.status, "output": r.output})))
1211 .collect(),
1212 ),
1213 );
1214 d.insert(
1215 "subagents".into(),
1216 Value::Object(
1217 self.subagents
1218 .iter()
1219 .map(|(k, s)| (k.clone(), json!({"status": s.status, "result": s.result})))
1220 .collect(),
1221 ),
1222 );
1223 d.insert("now_ms".into(), json!(now_ms()));
1224 d
1225 }
1226
1227 pub(crate) fn on_timer(&mut self, t: crate::state::TimerRecord) {
1229 let owner = &t.owner;
1230 match owner["kind"].as_str() {
1231 Some("tool") => {
1232 let node = NodeId(owner["node"].as_u64().unwrap_or(0));
1233 let req = owner["req"].as_u64().unwrap_or(0);
1234 self.pending
1235 .retain(|p| p.target != Target::Child(node, req));
1236 self.reply_tool(node, req, t.payload.clone(), false);
1237 }
1238 Some("step") | Some("step_budget") => {
1239 let run = owner["run"].as_str().unwrap_or("").to_string();
1240 let step = owner["step"].as_str().unwrap_or("").to_string();
1241 self.on_step_timer(
1242 &run,
1243 &step,
1244 owner["kind"].as_str() == Some("step_budget"),
1245 &t.payload,
1246 );
1247 }
1248 Some("goal") => self.on_goal_check(&t.payload),
1249 other => self
1250 .log
1251 .warn("timer.unknown_owner", json!({"id": t.id, "owner": other})),
1252 }
1253 }
1254
1255 pub(crate) fn on_tool_done(&mut self, node: NodeId, req: u64, result: Value, is_error: bool) {
1257 self.reply_tool(node, req, result, is_error);
1258 }
1259
1260 pub(crate) fn service_rate_take(&self, server: &str) -> Result<(), String> {
1267 crate::mcp::pace::take(server)
1268 }
1269}
1270
1271#[derive(Debug, Clone)]
1272enum RouteKind {
1273 Internal,
1274 Mapped(crate::registry::Mapping),
1275 Code,
1276 Mcp(String, String),
1277 Workflow(String, bool),
1279}
1280
1281enum ExecTarget {
1283 Tool { node: NodeId, req: u64 },
1284 Step { run: String, step: String },
1285}
1286
1287impl ExecTarget {
1288 fn from(caller: &ToolCaller) -> ExecTarget {
1289 match (caller.node, &caller.run, &caller.step) {
1290 (Some(node), _, _) => ExecTarget::Tool {
1291 node,
1292 req: caller.req,
1293 },
1294 (None, Some(run), Some(step)) => ExecTarget::Step {
1295 run: run.clone(),
1296 step: step.clone(),
1297 },
1298 _ => ExecTarget::Tool {
1299 node: NodeId(0),
1300 req: caller.req,
1301 },
1302 }
1303 }
1304 fn send(self, tx: &std::sync::mpsc::Sender<Event>, result: Value, is_error: bool) {
1305 let ev = match self {
1306 ExecTarget::Tool { node, req } => Event::ToolDone {
1307 node,
1308 req,
1309 result,
1310 is_error,
1311 },
1312 ExecTarget::Step { run, step } => Event::StepDone {
1313 run,
1314 step,
1315 error: is_error.then(|| result.to_string()),
1316 output: result,
1317 is_error,
1318 tokens: 0,
1319 },
1320 };
1321 let _ = tx.send(ev);
1322 }
1323}