harness-tools + app: task tool, subagent spawner, board wiring (M4)

The `task` tool delegates to a SubagentSpawner (owned by the composition root):
resolves the agent, enforces the depth limit, applies permission intersection,
filters tools per-agent, and runs the child session foreground or background.
Foreground returns the child's final text; background registers on the job board
and detaches under the parent run token. The run loop injects the board into
primary-agent requests and reconciles terminal jobs each step.

Integration tests cover foreground run + alias reuse continuing the same child
session, depth-limit and unknown-agent rejection, and board prompt injection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 06:29:12 +02:00
co-authored by Claude Opus 4.8
parent 8c859d91c9
commit ecb3267a50
18 changed files with 1137 additions and 67 deletions
+15 -2
View File
@@ -10,10 +10,13 @@ use crate::event::{AppEvent, EventBus};
use crate::llm::{FinishReason, LlmEvent, LlmEventStream, ProviderError};
use crate::permission::{PermissionService, Ruleset};
use crate::store::Store;
use crate::tool::{MetadataSink, PermissionHandle, Tool, ToolCtx, ToolError, ToolRegistry};
use crate::tool::{
MetadataSink, PermissionHandle, SubagentSpawner, Tool, ToolCtx, ToolError, ToolRegistry,
};
use crate::types::{Message, MessageId, Part, PartBody, PartId, SessionId, TokenUsage, ToolState};
use super::doomloop::DoomLoopGuard;
use super::jobs::JobBoard;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepResult {
@@ -47,6 +50,9 @@ pub struct StepContext {
pub permissions: Arc<PermissionService>,
pub static_rules: Ruleset,
pub extra_rules: Arc<Mutex<Ruleset>>,
/// Parent-effective ruleset for a subagent session; empty for a root session. Enables
/// permission intersection on this session's tool calls.
pub parent_rules: Ruleset,
pub session_id: SessionId,
pub cwd: PathBuf,
/// Session's `tool-output` spill directory (see `tool::truncate`).
@@ -54,6 +60,11 @@ pub struct StepContext {
pub cancel: CancellationToken,
/// Wall-clock for `created_at` stamps — passed in so tests stay deterministic.
pub now: i64,
/// Lets the `task` tool spawn subagents. `None` disables delegation (headless/tests).
pub spawner: Option<Arc<dyn SubagentSpawner>>,
/// This session's background job board (as a parent). Injected into requests when the
/// running agent can delegate; `None` disables the board.
pub job_board: Option<Arc<JobBoard>>,
}
struct FlushTracker {
@@ -483,7 +494,8 @@ impl<'a> Run<'a> {
self.ctx.static_rules.clone(),
self.ctx.extra_rules.clone(),
call_cancel.clone(),
);
)
.with_parent_rules(self.ctx.parent_rules.clone());
let tool_ctx = ToolCtx {
session_id: self.ctx.session_id.clone(),
message_id: self.message_id(),
@@ -493,6 +505,7 @@ impl<'a> Run<'a> {
cancel: call_cancel.clone(),
ask,
metadata: metadata_sink,
spawner: self.ctx.spawner.clone(),
};
let result = tokio::select! {