Skip to main content

agentd/runtime/
tools.rs

1// SPDX-License-Identifier: Apache-2.0
2//! **Internal tool execution** (RFC 0028 §3): the runtime is the single place
3//! internal tools run — for a turn worker's `ToolRequest` (answered with
4//! `ToolResult`), for a workflow step (`tool` / `memory.*` / … kinds) and for
5//! A2A commands (P5). Arguments are validated against the contract's input
6//! schema before dispatch and results against the output schema after
7//! (schema failure ⇒ a tool error, never a panic). Some tools are **deferred**
8//! (`sleep`, `subagent.run sync`, `subagent.await`, `await`, `think`,
9//! `context.compact`, `workflow.run wait`, `workflow.wait`): the request is
10//! parked in `pending` and answered when its wait resolves. Mapped tools
11//! (overrides) run on an executor thread against the runtime's own MCP
12//! connection.
13
14use super::children::ChildKind;
15use super::events::Event;
16use super::reactor::{PendingKind, PendingTool, Runtime, Target};
17use crate::context::ROOT;
18use crate::context::plan::{self, Plan};
19use crate::registry::{Registry, Route};
20use crate::state::now_ms;
21use crate::subagent::protocol::ControlMsg;
22use crate::supervisor::tree::NodeId;
23use serde_json::{Value, json};
24use std::time::Duration;
25
26/// Who is calling (derived from the child kind or the step).
27#[derive(Debug, Clone, Default)]
28pub(crate) struct ToolCaller {
29    pub node: Option<NodeId>,
30    pub req: u64,
31    pub ctx: Option<String>,
32    pub run: Option<String>,
33    pub step: Option<String>,
34    pub principal: Option<String>,
35    pub subagent: Option<String>,
36}
37
38impl ToolCaller {
39    fn label(&self) -> String {
40        if let Some(s) = &self.subagent {
41            return format!("subagent:{s}");
42        }
43        if let (Some(r), Some(s)) = (&self.run, &self.step) {
44            return format!("step:{r}/{s}");
45        }
46        format!("ctx:{}", self.ctx.as_deref().unwrap_or(ROOT))
47    }
48    /// The context whose plan/skills a `plan.*`/`skills.*` call addresses.
49    pub(crate) fn context_id(&self) -> String {
50        self.ctx.clone().unwrap_or_else(|| ROOT.to_string())
51    }
52    fn ctx_value(&self, instance: &str) -> Value {
53        json!({"instance": instance, "ctx": self.ctx, "run": self.run, "step": self.step, "principal": self.principal, "subagent": self.subagent})
54    }
55}
56
57/// The result of executing a tool.
58pub(crate) enum ToolOutcome {
59    Ready(Value, bool),
60    Deferred(PendingKind),
61    /// Running on an executor thread; the reply arrives as an event.
62    Executing,
63}
64
65impl Runtime {
66    /// A child asked for an internal tool.
67    pub(crate) fn on_tool_request(&mut self, node: NodeId, id: u64, name: &str, args: Value) {
68        self.counters.tool_calls += 1;
69        let caller = match self.children.get(node).map(|c| c.kind.clone()) {
70            Some(ChildKind::RootTurn { ctx, .. }) => ToolCaller {
71                node: Some(node),
72                req: id,
73                ctx: Some(ctx.clone()),
74                principal: self.contexts.get(&ctx).and_then(|c| c.principal.clone()),
75                ..Default::default()
76            },
77            Some(ChildKind::StepTurn { run, step, .. }) => ToolCaller {
78                node: Some(node),
79                req: id,
80                run: Some(run.clone()),
81                step: Some(step),
82                ctx: self.runs.get(&run).and_then(|r| r.conversation.clone()),
83                principal: self.runs.get(&run).and_then(|r| r.principal.clone()),
84                ..Default::default()
85            },
86            Some(ChildKind::Subagent { handle }) => ToolCaller {
87                node: Some(node),
88                req: id,
89                subagent: Some(handle),
90                ..Default::default()
91            },
92            Some(ChildKind::Think { ctx, .. }) => ToolCaller {
93                node: Some(node),
94                req: id,
95                ctx,
96                ..Default::default()
97            },
98            None => return,
99        };
100        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 }}));
101        match self.execute_tool(&caller, name, args) {
102            ToolOutcome::Ready(v, err) => self.reply_tool(node, id, v, err),
103            ToolOutcome::Deferred(kind) => {
104                // The unit is parked on a wait, not thinking (RFC 0032 §17).
105                self.activity_park(node, name);
106                self.pending.push(PendingTool {
107                    target: Target::Child(node, id),
108                    name: name.to_string(),
109                    kind,
110                    started_ms: now_ms(),
111                });
112            }
113            ToolOutcome::Executing => {}
114        }
115    }
116
117    /// Answer a child's tool request.
118    pub(crate) fn reply_tool(&mut self, node: NodeId, req: u64, result: Value, is_error: bool) {
119        self.log.debug(
120            "tool.reply",
121            json!({"node": node.0, "req": req, "is_error": is_error}),
122        );
123        if !self.children.send(
124            node,
125            &ControlMsg::ToolResult {
126                id: req,
127                result,
128                is_error,
129            },
130        ) {
131            self.log
132                .debug("tool.reply.dropped", json!({"node": node.0, "req": req}));
133        }
134    }
135
136    /// Answer a deferred request wherever it came from.
137    pub(crate) fn reply(&mut self, target: &Target, result: Value, is_error: bool) {
138        match target {
139            Target::Child(node, req) => self.reply_tool(*node, *req, result, is_error),
140            Target::Step(run, step) => {
141                let (run, step) = (run.clone(), step.clone());
142                let error = is_error.then(|| match &result {
143                    Value::String(s) => s.clone(),
144                    other => other.to_string(),
145                });
146                self.on_step_done(&run, &step, result, is_error, error, 0);
147            }
148        }
149    }
150
151    /// Execute an internal (or mapped) tool for `caller`.
152    pub(crate) fn execute_tool(
153        &mut self,
154        caller: &ToolCaller,
155        name: &str,
156        args: Value,
157    ) -> ToolOutcome {
158        // Grant + availability.
159        let allowed = match (&caller.subagent, &caller.run) {
160            (Some(_), _) => self
161                .registry
162                .allowed(&crate::registry::Caller::Subagent { allow: None }, name),
163            (None, Some(_)) => self
164                .registry
165                .allowed(&crate::registry::Caller::Workflow, name),
166            _ => self.registry.allowed(&crate::registry::Caller::Root, name),
167        };
168        if !allowed {
169            let reason = match self.registry.get(name) {
170                None => format!("no such tool {name:?}"),
171                Some(t) if t.disabled => format!("tool {name:?} is disabled by configuration"),
172                Some(t) if !t.is_available() => {
173                    format!("tool {name:?} has no implementation (map it with tools.overrides)")
174                }
175                Some(_) => format!("tool {name:?} is not granted to {}", caller.label()),
176            };
177            return ToolOutcome::Ready(Value::String(reason), true);
178        }
179        if let Err(e) = self.registry.validate_args(name, &args) {
180            return ToolOutcome::Ready(Value::String(e), true);
181        }
182        let route = self.registry.route(name).map(|r| match r {
183            Route::Internal => RouteKind::Internal,
184            Route::Mapped(m) => RouteKind::Mapped(m.clone()),
185            Route::Code => RouteKind::Code,
186            Route::Mcp { server, tool } => RouteKind::Mcp(server.to_string(), tool.to_string()),
187        });
188        let out = match route {
189            None => {
190                ToolOutcome::Ready(Value::String(format!("tool {name:?} is unavailable")), true)
191            }
192            Some(RouteKind::Internal) => self.builtin(caller, name, args),
193            Some(RouteKind::Mapped(m)) => self.run_mapped(caller, name, &m, args),
194            Some(RouteKind::Code) => match crate::tools::call(name, &args) {
195                Some(Ok(v)) => ToolOutcome::Ready(v, false),
196                Some(Err(e)) => ToolOutcome::Ready(Value::String(e), true),
197                None => {
198                    ToolOutcome::Ready(Value::String(format!("code tool {name:?} vanished")), true)
199                }
200            },
201            Some(RouteKind::Mcp(server, tool)) => {
202                self.run_mcp_call(caller, name, &server, &tool, args)
203            }
204        };
205        // Output validation for ready results.
206        match out {
207            ToolOutcome::Ready(v, false) => match self.registry.validate_result(name, &v) {
208                Ok(()) => ToolOutcome::Ready(v, false),
209                Err(e) => {
210                    self.log
211                        .warn("tool.result.schema", json!({"tool": name, "err": e}));
212                    ToolOutcome::Ready(Value::String(e), true)
213                }
214            },
215            other => other,
216        }
217    }
218
219    // ---- executors ---------------------------------------------------------
220
221    /// A mapped (override) tool: render args → MCP call on an executor thread → map result.
222    fn run_mapped(
223        &mut self,
224        caller: &ToolCaller,
225        name: &str,
226        m: &crate::registry::Mapping,
227        args: Value,
228    ) -> ToolOutcome {
229        let ctx = caller.ctx_value(&self.instance);
230        let mcp_args = match Registry::map_args(m, &args, &ctx) {
231            Ok(a) => a,
232            Err(e) => return ToolOutcome::Ready(Value::String(e), true),
233        };
234        let Some(client) = self.mcp.get(&m.server).cloned() else {
235            return ToolOutcome::Ready(
236                Value::String(format!("server {:?} for {name} is not connected", m.server)),
237                true,
238            );
239        };
240        let mapping = m.clone();
241        let tool_name = name.to_string();
242        let meta = json!({"agent/idempotency_key": format!("{}/{}#{}", self.instance, caller.label(), caller.req), "agent/instance": self.instance});
243        let timeout = self
244            .settings
245            .mcp
246            .default_timeout
247            .map(|d| d.0)
248            .unwrap_or(Duration::from_secs(60));
249        let tx = self.events_tx.clone();
250        let target = ExecTarget::from(caller);
251        let call_ctx = ctx.clone();
252        std::thread::Builder::new()
253            .name(format!("tool:{tool_name}"))
254            .spawn(move || {
255                let res =
256                    client.call_tool_with_meta_within(&mapping.tool, Some(mcp_args), meta, timeout);
257                let (result, is_error) = match res {
258                    Ok(r) => {
259                        // The result mapping sees `result`, the original `args` and `ctx`.
260                        let mut ctx = crate::store::mcp::result_ctx(&r);
261                        ctx["args"] = args;
262                        ctx["ctx"] = call_ctx;
263                        if r.is_error() {
264                            (Value::String(format!("{tool_name}: {}", r.text())), true)
265                        } else {
266                            match Registry::map_result(&mapping, &ctx) {
267                                Ok(v) => (v, false),
268                                Err(e) => (Value::String(e), true),
269                            }
270                        }
271                    }
272                    Err(e) => (
273                        Value::String(format!("{tool_name}: transport error: {e}")),
274                        true,
275                    ),
276                };
277                target.send(&tx, result, is_error);
278            })
279            .ok();
280        ToolOutcome::Executing
281    }
282
283    /// A plain MCP tool called through the runtime (workflow steps / A2A commands).
284    fn run_mcp_call(
285        &mut self,
286        caller: &ToolCaller,
287        name: &str,
288        server: &str,
289        tool: &str,
290        args: Value,
291    ) -> ToolOutcome {
292        let Some(client) = self.mcp.get(server).cloned() else {
293            return ToolOutcome::Ready(
294                Value::String(format!("server {server:?} for {name} is not connected")),
295                true,
296            );
297        };
298        let tool = tool.to_string();
299        let meta = json!({"agent/idempotency_key": format!("{}/{}#{}", self.instance, caller.label(), caller.req), "agent/instance": self.instance});
300        let timeout = self
301            .settings
302            .mcp
303            .default_timeout
304            .map(|d| d.0)
305            .unwrap_or(Duration::from_secs(60));
306        let tx = self.events_tx.clone();
307        let target = ExecTarget::from(caller);
308        std::thread::Builder::new()
309            .name(format!("mcp:{server}.{tool}"))
310            .spawn(move || {
311                let (result, is_error) =
312                    match client.call_tool_with_meta_within(&tool, Some(args), meta, timeout) {
313                        Ok(r) => (super::worker::tool_result_value(&r), r.is_error()),
314                        Err(e) => (Value::String(format!("transport error: {e}")), true),
315                    };
316                target.send(&tx, result, is_error);
317            })
318            .ok();
319        ToolOutcome::Executing
320    }
321
322    // ---- built-ins ---------------------------------------------------------
323
324    fn builtin(&mut self, caller: &ToolCaller, name: &str, args: Value) -> ToolOutcome {
325        let ok = |v: Value| ToolOutcome::Ready(v, false);
326        let err = |e: String| ToolOutcome::Ready(Value::String(e), true);
327        let by = caller.label();
328        match name {
329            // ---- instruction ----
330            "instruction.read" => ok(
331                json!({"text": self.instruction.text, "source": self.instruction.source, "uri": self.instruction.uri, "version": self.instruction.version.to_string()}),
332            ),
333            "instruction.subscribe" => {
334                let uri = args
335                    .get("uri")
336                    .and_then(Value::as_str)
337                    .map(str::to_string)
338                    .or_else(|| self.instruction.uri.clone());
339                match uri {
340                    None => err(
341                        "instruction.subscribe: the instruction is static text; give a uri".into(),
342                    ),
343                    Some(u) => match self.subscribe_instruction(&u) {
344                        Ok(()) => ok(json!({"subscribed": true, "uri": u})),
345                        Err(e) => err(e),
346                    },
347                }
348            }
349            // ---- memory ----
350            "memory.get" => match self
351                .memory
352                .get(&self.durable, args["key"].as_str().unwrap_or(""))
353            {
354                Ok(v) => ok(v),
355                Err(e) => err(e),
356            },
357            "memory.set" => {
358                let ttl = match args.get("ttl").and_then(Value::as_str) {
359                    Some(t) => match crate::config::parse_duration(t) {
360                        Ok(d) => Some(d.as_millis() as u64),
361                        Err(e) => return err(format!("memory.set: ttl: {e}")),
362                    },
363                    None => None,
364                };
365                match self.memory.set(
366                    &self.durable,
367                    args["key"].as_str().unwrap_or(""),
368                    args["value"].clone(),
369                    ttl,
370                    Some(&by),
371                ) {
372                    Ok(v) => ok(v),
373                    Err(e) => err(e),
374                }
375            }
376            "memory.list" => match self.memory.list(
377                &self.durable,
378                args.get("prefix").and_then(Value::as_str),
379                args.get("limit")
380                    .and_then(Value::as_u64)
381                    .map(|l| l as usize),
382            ) {
383                Ok(v) => ok(v),
384                Err(e) => err(e),
385            },
386            "memory.delete" => match self
387                .memory
388                .delete(&self.durable, args["key"].as_str().unwrap_or(""))
389            {
390                Ok(v) => ok(v),
391                Err(e) => err(e),
392            },
393            // ---- artifacts ----
394            "artifact.create" => {
395                let content = match (
396                    args.get("content"),
397                    args.get("from_step").and_then(Value::as_str),
398                ) {
399                    (Some(c), _) => c.clone(),
400                    (None, Some(step)) => match caller
401                        .run
402                        .as_ref()
403                        .and_then(|r| self.runs.get(r))
404                        .and_then(|r| r.steps.get(step))
405                        .and_then(|s| s.output.clone())
406                    {
407                        Some(o) => o,
408                        None => {
409                            return err(format!(
410                                "artifact.create: from_step {step:?} has no output"
411                            ));
412                        }
413                    },
414                    (None, None) => {
415                        return err("artifact.create: content or from_step is required".into());
416                    }
417                };
418                let owner = caller.run.clone().or_else(|| caller.ctx.clone());
419                match self.artifacts.create(
420                    &self.durable,
421                    super::artifacts::NewArtifact {
422                        name: args["name"].as_str().unwrap_or(""),
423                        mime: args.get("mime").and_then(Value::as_str),
424                        content,
425                        created_by: Some(&by),
426                        sensitive: args
427                            .get("sensitive")
428                            .and_then(Value::as_bool)
429                            .unwrap_or(false),
430                        owner: owner.as_deref(),
431                    },
432                ) {
433                    Ok(v) => ok(v),
434                    Err(e) => err(e),
435                }
436            }
437            "artifact.get" => match self.artifacts.get_value(args["id"].as_str().unwrap_or("")) {
438                Ok(v) => ok(v),
439                Err(e) => err(e),
440            },
441            "artifact.delete" => match self
442                .artifacts
443                .delete(&self.durable, args["id"].as_str().unwrap_or(""))
444            {
445                Ok(v) => ok(v),
446                Err(e) => err(e),
447            },
448            "artifact.list" => ok(self.artifacts.list(
449                args.get("prefix").and_then(Value::as_str),
450                args.get("limit")
451                    .and_then(Value::as_u64)
452                    .map(|l| l as usize),
453                None,
454            )),
455            // ---- plan ----
456            "plan.create" => {
457                let ctx_id = caller.context_id();
458                let max = self
459                    .settings
460                    .context
461                    .plan
462                    .max_items
463                    .unwrap_or(plan::DEFAULT_MAX_ITEMS as u32) as usize;
464                let items: Vec<Value> = args["items"].as_array().cloned().unwrap_or_default();
465                match Plan::create(args["goal"].as_str().unwrap_or(""), &items, max) {
466                    Ok(p) => {
467                        let v = p.to_value();
468                        self.context_for(&ctx_id, caller.principal.as_deref()).plan = Some(p);
469                        self.context_for(&ctx_id, caller.principal.as_deref())
470                            .touch();
471                        self.log
472                            .info("plan.updated", json!({"ctx": ctx_id, "op": "create"}));
473                        ok(v)
474                    }
475                    Err(e) => err(e),
476                }
477            }
478            "plan.get" => {
479                let ctx_id = caller.context_id();
480                match self.contexts.get(&ctx_id).and_then(|c| c.plan.as_ref()) {
481                    Some(p) => ok(json!({"plan": p.to_value(), "progress": p.progress()})),
482                    None => ok(json!({"plan": null, "progress": "no plan"})),
483                }
484            }
485            "plan.update" => {
486                let ctx_id = caller.context_id();
487                let max = self
488                    .settings
489                    .context
490                    .plan
491                    .max_items
492                    .unwrap_or(plan::DEFAULT_MAX_ITEMS as u32) as usize;
493                let c = self.context_for(&ctx_id, caller.principal.as_deref());
494                match c.plan.as_mut() {
495                    None => err("plan.update: no plan (call plan.create first)".into()),
496                    Some(p) => match p.update(&args, max) {
497                        Ok(()) => {
498                            let mut v = p.to_value();
499                            v["progress"] = json!(p.progress());
500                            c.touch();
501                            self.log
502                                .info("plan.updated", json!({"ctx": ctx_id, "op": "update"}));
503                            ok(v)
504                        }
505                        Err(e) => err(e),
506                    },
507                }
508            }
509            "plan.clear" => {
510                let ctx_id = caller.context_id();
511                let c = self.context_for(&ctx_id, caller.principal.as_deref());
512                let had = c.plan.take().is_some();
513                c.touch();
514                self.log
515                    .info("plan.updated", json!({"ctx": ctx_id, "op": "clear"}));
516                ok(json!({"ok": had}))
517            }
518            // ---- skills ----
519            "skills.list" => ok(self.skills.list_value()),
520            "skills.load" => {
521                let ctx_id = caller.context_id();
522                let name = args["name"].as_str().unwrap_or("").to_string();
523                let mcp = self.mcp.clone();
524                let resolver = move |server: &str| -> Option<
525                    std::sync::Arc<dyn crate::context::skills::SkillServer>,
526                > {
527                    mcp.get(server).map(|c| {
528                        c.clone() as std::sync::Arc<dyn crate::context::skills::SkillServer>
529                    })
530                };
531                match self
532                    .skills
533                    .load(&name, args.get("arguments").cloned(), &resolver)
534                {
535                    Ok(body) => {
536                        let max_loaded = self.settings.skills.max_loaded.unwrap_or(8) as usize;
537                        let c = self.context_for(&ctx_id, caller.principal.as_deref());
538                        match c.load_skill(&name, &body.hash, max_loaded) {
539                            Ok(()) => ok(
540                                json!({"loaded": true, "name": name, "hash": body.hash, "body": body.body}),
541                            ),
542                            Err(e) => err(e),
543                        }
544                    }
545                    Err(e) => err(e),
546                }
547            }
548            "skills.unload" => {
549                let ctx_id = caller.context_id();
550                let c = self.context_for(&ctx_id, caller.principal.as_deref());
551                ok(json!({"ok": c.unload_skill(args["name"].as_str().unwrap_or(""))}))
552            }
553            // ---- status ----
554            "status" => ok(self.status_value()),
555            // ---- time ----
556            "sleep" => {
557                let d = match crate::config::parse_duration(args["duration"].as_str().unwrap_or(""))
558                {
559                    Ok(d) => d,
560                    Err(e) => return err(format!("sleep: {e}")),
561                };
562                let deadline = now_ms() + d.as_millis() as u64;
563                let owner = match caller.node {
564                    Some(n) => {
565                        json!({"kind": "tool", "node": n.0, "req": caller.req, "tool": "sleep"})
566                    }
567                    None => json!({"kind": "step", "run": caller.run, "step": caller.step}),
568                };
569                match self.timers.arm(
570                    &self.durable,
571                    deadline,
572                    owner,
573                    json!({"slept_ms": d.as_millis() as u64}),
574                ) {
575                    Ok(id) => ToolOutcome::Deferred(PendingKind::Timer { id }),
576                    Err(e) => err(format!("sleep: {e}")),
577                }
578            }
579            "await" => {
580                let cond = args["condition"].as_str().unwrap_or("").to_string();
581                if let Err(e) =
582                    crate::cel::compile_check(cond.trim().trim_start_matches("CEL:").trim())
583                {
584                    return err(format!("await: {e}"));
585                }
586                let timeout = args
587                    .get("timeout")
588                    .and_then(Value::as_str)
589                    .and_then(|t| crate::config::parse_duration(t).ok())
590                    .unwrap_or(Duration::from_secs(600));
591                ToolOutcome::Deferred(PendingKind::Await {
592                    condition: cond,
593                    deadline_ms: now_ms() + timeout.as_millis() as u64,
594                })
595            }
596            // ---- context ----
597            "context.compact" => {
598                let ctx_id = caller.context_id();
599                let keep_last = args
600                    .get("keep_last")
601                    .and_then(Value::as_u64)
602                    .map(|k| k as usize)
603                    .unwrap_or(self.settings.context.keep_last.unwrap_or(12) as usize);
604                let target = args.get("target_tokens").and_then(Value::as_u64);
605                match caller.node {
606                    Some(node) => {
607                        self.start_compaction(&ctx_id, keep_last, target, Some((node, caller.req)));
608                        ToolOutcome::Deferred(PendingKind::Think {
609                            child: NodeId(u64::MAX),
610                        })
611                    }
612                    None => err("context.compact needs a calling turn".into()),
613                }
614            }
615            "think" => match caller.node {
616                Some(node) => match self.start_think(caller, &args, Some((node, caller.req))) {
617                    Ok(child) => ToolOutcome::Deferred(PendingKind::Think { child }),
618                    Err(e) => err(e),
619                },
620                None => err("think as a step is the `think` kind".into()),
621            },
622            // ---- lifecycle ----
623            "finish" => {
624                // The turn worker records the finish itself (RFC 0026 §3.2); the
625                // runtime acknowledges. Steps: the `finish` kind.
626                ok(json!({"ok": true}))
627            }
628            // Human-in-the-loop (RFC 0032 §16): gate through the interface, or
629            // apply the configured fallback (fail | wait | auto judge).
630            "ask_human" => self.ask_human_tool(caller, args),
631            // ---- subagents ----
632            "subagent.run" | "subagent.send" | "subagent.kill" | "subagent.status"
633            | "subagent.await" | "subagent.list" => self.subagent_tool(caller, name, args),
634            // ---- workflows ----
635            "workflow.run" | "workflow.list" | "workflow.status" | "workflow.cancel"
636            | "workflow.wait" | "workflow.create" | "workflow.update" | "workflow.delete"
637            | "workflow.pause" | "workflow.resume" | "workflow.signal" => {
638                self.workflow_tool(caller, name, args)
639            }
640            // ---- guarded local command runner (RFC 0028 §exec; default-OFF) ----
641            #[cfg(feature = "exec")]
642            "exec" => self.exec_tool(caller, args),
643            other => err(format!(
644                "internal tool {other:?} has no built-in implementation"
645            )),
646        }
647    }
648
649    /// The `exec` tool: run one allow-listed command with the `security.exec`
650    /// controls on an executor thread (never the reactor). Reached only when the
651    /// runner is enabled — otherwise `exec` is mapping-only and this never routes
652    /// here. Every guard is re-checked here (defense in depth), not just at build.
653    #[cfg(feature = "exec")]
654    fn exec_tool(&mut self, caller: &ToolCaller, args: Value) -> ToolOutcome {
655        use super::exec;
656        let cfg = self.settings.security.exec.clone();
657        if !cfg.enabled {
658            return ToolOutcome::Ready(
659                Value::String("exec: local execution is disabled (security.exec.enabled)".into()),
660                true,
661            );
662        }
663        let cmd = args["cmd"].as_str().unwrap_or_default().to_string();
664        if cmd.is_empty() {
665            return ToolOutcome::Ready(Value::String("exec: `cmd` is required".into()), true);
666        }
667        // Allow-list (argv[0]); empty allow-list denies everything.
668        if !cfg.allow.iter().any(|a| a == &cmd) {
669            return ToolOutcome::Ready(
670                Value::String(format!(
671                    "exec: command {cmd:?} is not in security.exec.allow"
672                )),
673                true,
674            );
675        }
676        let Some(workdir) = cfg.workdir.clone() else {
677            return ToolOutcome::Ready(
678                Value::String("exec: security.exec.workdir must be set".into()),
679                true,
680            );
681        };
682        let cwd = match exec::resolve_cwd(std::path::Path::new(&workdir), args["cwd"].as_str()) {
683            Ok(c) => c,
684            Err(e) => return ToolOutcome::Ready(Value::String(format!("exec: {e}")), true),
685        };
686        let argv: Vec<String> = args["args"]
687            .as_array()
688            .map(|a| {
689                a.iter()
690                    .filter_map(|v| v.as_str().map(String::from))
691                    .collect()
692            })
693            .unwrap_or_default();
694        let stdin = args["cmd_stdin"]
695            .as_str()
696            .or_else(|| args["stdin"].as_str())
697            .map(String::from);
698        // Timeout: min(requested, configured max); output cap; env passthrough.
699        let max_timeout = cfg.timeout.map(|d| d.0).unwrap_or(Duration::from_secs(30));
700        let req = args["timeout"]
701            .as_str()
702            .and_then(|s| crate::config::parse_duration(s).ok());
703        let timeout = req.map(|d| d.min(max_timeout)).unwrap_or(max_timeout);
704        let max_output = cfg.max_output.unwrap_or(1 << 20) as usize;
705        let env_pass = cfg.env.clone();
706
707        self.log.info(
708            "exec.run",
709            json!({"cmd": cmd, "argc": argv.len(), "cwd": cwd.display().to_string(), "timeout_ms": timeout.as_millis() as u64, "caller": caller.label()}),
710        );
711        let tx = self.events_tx.clone();
712        let target = ExecTarget::from(caller);
713        std::thread::Builder::new()
714            .name("tool:exec".into())
715            .spawn(move || {
716                let (result, is_error) = match exec::run_command(
717                    &cmd,
718                    &argv,
719                    &cwd,
720                    stdin.as_deref(),
721                    timeout,
722                    max_output,
723                    &env_pass,
724                ) {
725                    Ok(v) => (v, false),
726                    Err(e) => (Value::String(format!("exec: {e}")), true),
727                };
728                target.send(&tx, result, is_error);
729            })
730            .ok();
731        ToolOutcome::Executing
732    }
733
734    /// The context a caller addresses (created on demand).
735    pub(crate) fn context_for(
736        &mut self,
737        ctx_id: &str,
738        principal: Option<&str>,
739    ) -> &mut crate::context::ContextState {
740        if ctx_id == ROOT {
741            self.contexts.root()
742        } else {
743            self.contexts.conversation(ctx_id, principal)
744        }
745    }
746
747    /// Launch a `think` child for a tool request / a step.
748    pub(crate) fn start_think(
749        &mut self,
750        caller: &ToolCaller,
751        args: &Value,
752        reply_to: Option<(NodeId, u64)>,
753    ) -> Result<NodeId, String> {
754        let prompt = args["prompt"].as_str().unwrap_or("").to_string();
755        if prompt.trim().is_empty() {
756            return Err("think: prompt must be non-empty".into());
757        }
758        let ctx_id = caller.context_id();
759        let mut messages = Vec::new();
760        // `reads`: memory keys folded into the prompt.
761        if let Some(reads) = args.get("reads").and_then(Value::as_array) {
762            for k in reads.iter().filter_map(Value::as_str) {
763                if let Ok(v) = self.memory.get(&self.durable, k)
764                    && v["found"] == json!(true)
765                {
766                    messages.push(crate::context::Msg::system(format!(
767                        "memory[{k}] = {}",
768                        v["value"]
769                    )));
770                }
771            }
772        }
773        messages.push(crate::context::Msg::user(prompt, None));
774        let output_schema = args.get("output_schema").cloned();
775        let system = format!(
776            "You are the reasoning module of {}. Think carefully about the request and reply with {}. No tools are available.",
777            self.instance,
778            if output_schema.is_some() {
779                "ONLY one JSON object matching the schema"
780            } else {
781                "your conclusion (a JSON object when the request asks for structure)"
782            }
783        );
784        let spec = crate::subagent::protocol::TurnSpec {
785            kind: crate::subagent::protocol::TurnKind::Think,
786            system,
787            messages,
788            tools: Vec::new(),
789            internal: Vec::new(),
790            mcp_routes: Default::default(),
791            output_schema,
792            max_rounds: 3,
793            budget_admission: self.governor.is_active(),
794            idempotency_prefix: String::new(),
795            tool_meta: None,
796            temperature: Some(0.0),
797            max_tokens_per_call: 0,
798            turn_id: self.next_id("think"),
799        };
800        let launch = super::turns::TurnLaunch {
801            spec,
802            kind: ChildKind::Think {
803                purpose: "tool".into(),
804                ctx: Some(ctx_id.clone()),
805                reply_to,
806                extra: Value::Null,
807                reservation: None,
808            },
809            servers: Vec::new(),
810            max_steps: 4,
811            max_tokens: self.settings.limits.run.tokens(),
812            deadline_ms: 300_000,
813            agent_path: format!("think/{ctx_id}"),
814        };
815        self.spawn_turn(launch)
816    }
817
818    /// Resolve deferred requests: timers are answered on fire (`on_timer`),
819    /// subagents on their result, thinks on their TurnDone; `await`
820    /// conditions and run waits are polled here.
821    pub(crate) fn poll_pending(&mut self) {
822        if self.pending.is_empty() {
823            return;
824        }
825        let now = now_ms();
826        // Collect by TARGET, never by index: `reply` re-enters the reactor (a
827        // step outcome cascades through `finish_step` into
828        // `cancel_scoped_children`, which prunes `pending` itself), so an index
829        // remembered across a reply addresses a different entry by the time we
830        // get to it — or one past the end, panicking the reactor thread and
831        // taking the daemon with it. Two `race` branches waiting on the same
832        // deadline are the live case: both land in `done` in one pass, the
833        // winner's reply cancels the loser's branch.
834        let mut done: Vec<(Target, Value, bool)> = Vec::new();
835        for p in self.pending.iter() {
836            let t = &p.target;
837            match &p.kind {
838                PendingKind::Await { condition, deadline_ms } => {
839                    let data = self.await_data();
840                    let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
841                    match crate::cel::eval_bool(condition.trim().trim_start_matches("CEL:").trim(), &vars) {
842                        Ok(true) => done.push((t.clone(), json!({"satisfied": true}), false)),
843                        Ok(false) if now >= *deadline_ms => done.push((t.clone(), json!({"satisfied": false, "timed_out": true}), false)),
844                        Ok(false) => {}
845                        Err(e) => done.push((t.clone(), Value::String(format!("await: {e}")), true)),
846                    }
847                }
848                PendingKind::Run { run, deadline_ms } => match self.runs.get(run) {
849                    Some(r) if r.status.is_terminal() => done.push((t.clone(), json!({"run": run, "status": r.status, "output": r.output, "error": r.error}), false)),
850                    Some(_) if now >= *deadline_ms => done.push((t.clone(), json!({"run": run, "status": "running", "timed_out": true}), false)),
851                    Some(_) => {}
852                    None => done.push((t.clone(), Value::String(format!("run {run:?} does not exist")), true)),
853                },
854                PendingKind::Subagent { handle } => {
855                    if let Some(s) = self.subagents.get(handle)
856                        && super::reactor::is_terminal_status(&s.status)
857                    {
858                        done.push((t.clone(), json!({"handle": handle, "status": s.status, "result": s.result, "error": s.error}), false));
859                    }
860                }
861                // Human gates run their own pass (auto-judge + prune + timeout).
862                PendingKind::Timer { .. }
863                | PendingKind::Think { .. }
864                | PendingKind::Human { .. } => {}
865            }
866        }
867        for (target, v, e) in done {
868            // A reentrant prune may already have removed (and cancelled) this
869            // entry while we were replying to an earlier one — it is no longer
870            // waiting, so answering it would resurrect a cancelled branch.
871            let len = self.pending.len();
872            self.pending.retain(|p| p.target != target);
873            if self.pending.len() == len {
874                continue;
875            }
876            self.reply(&target, v, e);
877        }
878        self.poll_pending_human();
879    }
880
881    /// The variables an `await` condition sees: memory (by key), runs, subagents.
882    fn await_data(&self) -> crate::engine::template::Data {
883        let mut d = crate::engine::template::Data::new();
884        d.insert(
885            "runs".into(),
886            Value::Object(
887                self.runs
888                    .iter()
889                    .map(|(k, r)| (k.clone(), json!({"status": r.status, "output": r.output})))
890                    .collect(),
891            ),
892        );
893        d.insert(
894            "subagents".into(),
895            Value::Object(
896                self.subagents
897                    .iter()
898                    .map(|(k, s)| (k.clone(), json!({"status": s.status, "result": s.result})))
899                    .collect(),
900            ),
901        );
902        d.insert("now_ms".into(), json!(now_ms()));
903        d
904    }
905
906    /// A durable timer fired.
907    pub(crate) fn on_timer(&mut self, t: crate::state::TimerRecord) {
908        let owner = &t.owner;
909        match owner["kind"].as_str() {
910            Some("tool") => {
911                let node = NodeId(owner["node"].as_u64().unwrap_or(0));
912                let req = owner["req"].as_u64().unwrap_or(0);
913                self.pending
914                    .retain(|p| p.target != Target::Child(node, req));
915                self.reply_tool(node, req, t.payload.clone(), false);
916            }
917            Some("step") | Some("step_budget") => {
918                let run = owner["run"].as_str().unwrap_or("").to_string();
919                let step = owner["step"].as_str().unwrap_or("").to_string();
920                self.on_step_timer(
921                    &run,
922                    &step,
923                    owner["kind"].as_str() == Some("step_budget"),
924                    &t.payload,
925                );
926            }
927            Some("goal") => self.on_goal_check(&t.payload),
928            other => self
929                .log
930                .warn("timer.unknown_owner", json!({"id": t.id, "owner": other})),
931        }
932    }
933
934    /// An executor thread answered a child's mapped/MCP request.
935    pub(crate) fn on_tool_done(&mut self, node: NodeId, req: u64, result: Value, is_error: bool) {
936        self.reply_tool(node, req, result, is_error);
937    }
938}
939
940#[derive(Debug, Clone)]
941enum RouteKind {
942    Internal,
943    Mapped(crate::registry::Mapping),
944    Code,
945    Mcp(String, String),
946}
947
948/// Where an executor thread's result goes.
949enum ExecTarget {
950    Tool { node: NodeId, req: u64 },
951    Step { run: String, step: String },
952}
953
954impl ExecTarget {
955    fn from(caller: &ToolCaller) -> ExecTarget {
956        match (caller.node, &caller.run, &caller.step) {
957            (Some(node), _, _) => ExecTarget::Tool {
958                node,
959                req: caller.req,
960            },
961            (None, Some(run), Some(step)) => ExecTarget::Step {
962                run: run.clone(),
963                step: step.clone(),
964            },
965            _ => ExecTarget::Tool {
966                node: NodeId(0),
967                req: caller.req,
968            },
969        }
970    }
971    fn send(self, tx: &std::sync::mpsc::Sender<Event>, result: Value, is_error: bool) {
972        let ev = match self {
973            ExecTarget::Tool { node, req } => Event::ToolDone {
974                node,
975                req,
976                result,
977                is_error,
978            },
979            ExecTarget::Step { run, step } => Event::StepDone {
980                run,
981                step,
982                error: is_error.then(|| result.to_string()),
983                output: result,
984                is_error,
985                tokens: 0,
986            },
987        };
988        let _ = tx.send(ev);
989    }
990}