agentd/agentloop/action.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Self-tool dispatch. RFC 0005 §self-tools, RFC 0007.
3//!
4//! The agentic loop's tools come from connected MCP servers *plus* agentd's own
5//! self-tools (`subagent.spawn`, …). A [`SelfHandler`] supplies those tool
6//! definitions and handles their calls in-process — distinct from the MCP
7//! dispatch path. This is the seam through which the model **self-orchestrates**:
8//! it calls `subagent.spawn` to split its instruction into delegated child
9//! agents (the supervisor enforces the caps + scope). RFC 0001 §self-orchestration.
10
11use crate::wire::intel::ToolDef;
12use serde_json::Value;
13
14/// The two classes of tool the agentic loop offers the model — the boundary that
15/// keeps agentd honest to target principle 1 (tools come ONLY from registered MCP
16/// servers) and principle 2 (no local code/command execution). EVERY tool in the
17/// loop's catalogue is exactly one of these; there is no third "general capability
18/// library".
19/// * [`Mcp`](ToolClass::Mcp) — a tool discovered from a connected MCP server
20/// (`tools/list`). Dispatched by routing the call BACK to its owning server
21/// ([`dispatch_tool`](crate::agentloop::runner)); agentd never runs it locally.
22/// * [`SelfControl`](ToolClass::SelfControl) — agentd's OWN orchestration
23/// primitives (see [`SELF_CONTROL_TOOLS`]): delegation (`subagent.*`,
24/// `a2a.delegate`), reactivity (root-only `schedule`/`subscribe`/`unsubscribe`),
25/// and resource attention (`resource.read`). These are handled in-process by a
26/// [`SelfHandler`] / the runner — NONE shells out. This is the named
27/// "self/control" class: the agent's own control surface, structurally distinct
28/// from the MCP task-tool catalogue (a different code path assembles each).
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ToolClass {
31 /// A tool from a connected MCP server; dispatched back to that server.
32 Mcp,
33 /// One of agentd's own self/control orchestration primitives; handled in-process.
34 SelfControl,
35 /// A CODE-REGISTERED tool (RFC 0022 §4): native Rust the embedder
36 /// registered via [`crate::tools::register`] — first-party by definition,
37 /// dispatched in-process, and it WINS a name collision with a remote MCP
38 /// tool (a server cannot steal a registered tool's calls).
39 Code,
40}
41
42/// The authoritative membership of the [`ToolClass::SelfControl`] class: every
43/// self/control primitive name agentd may offer the model. The
44/// [`SelfHandler`] advertises a depth-/feature-conditioned SUBSET of this set
45/// (`a2a.delegate` only with peers; `schedule`/`subscribe`/`unsubscribe` only at
46/// the root; the `subagent.*` delegation tools only within the depth budget), and
47/// the runner adds `resource.read` when any resource is readable. A drift-guard
48/// test asserts everything a handler can advertise is listed here — so a new
49/// self-tool cannot silently escape the class boundary (and, by construction, this
50/// set contains NO local-exec primitive: principle 2).
51pub const SELF_CONTROL_TOOLS: &[&str] = &[
52 "subagent.spawn",
53 "subagent.status",
54 "subagent.await",
55 "schedule",
56 "subscribe",
57 "unsubscribe",
58 "await_resource",
59 "workflow.define",
60 "workflow.patch",
61 "workflow.run",
62 "a2a.delegate",
63 "resource.read",
64];
65
66/// Provides agentd's in-process self-tools to the loop. The loop tries the
67/// self-handler first; a `None` result means "not a self-tool — fall through to
68/// MCP".
69pub trait SelfHandler {
70 /// The self-tool definitions to advertise to the model (added to the MCP
71 /// catalogue).
72 fn tools(&self) -> Vec<ToolDef>;
73
74 /// Handle a tool call. Returns `Some((observation, is_error))` if `name` is
75 /// one of this handler's self-tools; `None` to fall through to MCP.
76 fn handle(&mut self, name: &str, args: &Value) -> Option<(String, bool)>;
77
78 /// Read an `agentd://` self-resource (e.g. `agentd://subagent/<handle>` — an
79 /// async child's completion). A `resource.read` for an `agentd://` URI routes
80 /// here instead of to MCP. `Some((content, is_error))` if this handler serves
81 /// the URI; `None` (the default) means it does not. RFC 0009 §async.
82 fn read_resource(&mut self, _uri: &str) -> Option<(String, bool)> {
83 None
84 }
85
86 /// Whether this handler exposes any `agentd://` self-resources — so the loop
87 /// offers the `resource.read` tool even when no MCP resources exist. Default
88 /// `false`.
89 fn serves_self_resources(&self) -> bool {
90 false
91 }
92
93 /// Drain any future wake-ups the agent scheduled for itself this run
94 /// (RFC 0008 §self-scheduling). Default: none. The loop attaches these to
95 /// the run's [`Outcome`](crate::agentloop::stop::Outcome) so a daemon
96 /// supervisor can arm them.
97 fn take_scheduled(&mut self) -> Vec<crate::agentloop::stop::ScheduleRequest> {
98 Vec::new()
99 }
100
101 /// Drain any resource (un)subscriptions the agent requested for itself this
102 /// run (RFC 0008). Default: none. Attached to the run's `Outcome`.
103 fn take_subscriptions(&mut self) -> Vec<crate::agentloop::stop::SubscriptionRequest> {
104 Vec::new()
105 }
106}