1use 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#[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 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
57pub(crate) enum ToolOutcome {
59 Ready(Value, bool),
60 Deferred(PendingKind),
61 Executing,
63}
64
65impl Runtime {
66 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 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 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 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 pub(crate) fn execute_tool(
153 &mut self,
154 caller: &ToolCaller,
155 name: &str,
156 args: Value,
157 ) -> ToolOutcome {
158 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 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 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 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 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 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.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.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 "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.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.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" => ok(self.status_value()),
555 "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.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 "finish" => {
624 ok(json!({"ok": true}))
627 }
628 "ask_human" => self.ask_human_tool(caller, args),
631 "subagent.run" | "subagent.send" | "subagent.kill" | "subagent.status"
633 | "subagent.await" | "subagent.list" => self.subagent_tool(caller, name, args),
634 "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 #[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 #[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 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 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 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 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 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 pub(crate) fn poll_pending(&mut self) {
822 if self.pending.is_empty() {
823 return;
824 }
825 let now = now_ms();
826 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 PendingKind::Timer { .. }
863 | PendingKind::Think { .. }
864 | PendingKind::Human { .. } => {}
865 }
866 }
867 for (target, v, e) in done {
868 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 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 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 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
948enum 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}