Skip to main content

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}