Skip to main content

SubAgentManager

Struct SubAgentManager 

Source
pub struct SubAgentManager { /* private fields */ }
Expand description

Manages sub-agent lifecycle: definitions, spawning, cancellation, and result collection.

SubAgentManager is the central coordinator for all sub-agent tasks. It tracks active SubAgentHandles, enforces the global concurrency limit, and stores loaded SubAgentDefs.

§Concurrency model

The concurrency limit counts agents whose SubAgentState is Submitted or Working. Reserved slots (via reserve_slots) also count against this limit to allow orchestration schedulers to guarantee capacity before spawning.

§Examples

use zeph_subagent::SubAgentManager;

let manager = SubAgentManager::new(4);
assert_eq!(manager.definitions().len(), 0);

Implementations§

Source§

impl SubAgentManager

Source

pub async fn collect(&mut self, task_id: &str) -> Result<String, SubAgentError>

Collect the result from a completed sub-agent, removing it from the active set.

Writes a final TranscriptMeta sidecar with the terminal state and turn count.

§Errors

Returns SubAgentError::NotFound if the task ID is unknown, SubAgentError::Spawn if the task panicked.

Source

pub async fn def_name_for_resume( &self, id_prefix: &str, config: &SubAgentConfig, ) -> Result<String, SubAgentError>

Look up the definition name for a resumable transcript without spawning.

Used by callers that need to resolve skills before calling resume(). Offloads the blocking FS reads to a spawn_blocking thread.

§Errors

Returns the same errors as crate::transcript::TranscriptReader::find_by_prefix and crate::transcript::TranscriptReader::load_meta.

Source

pub fn statuses(&self) -> Vec<(String, SubAgentStatus)>

Return a snapshot of all active sub-agent statuses.

Source

pub fn is_task_finished(&self, task_id: &str) -> bool

Returns whether the background task backing task_id has finished at the runtime level, independent of whether its status_rx channel ever published a terminal SubAgentState.

A code path that exits run_agent_loop without sending a terminal status first — most notably a panic — leaves status_rx stuck on the last observed state (typically Working) forever. Callers such as collect_finished_subagents in zeph-core use this as a defense-in-depth reap signal for that case (issue #6408).

Returns false for an unknown task_id or a handle with no join_handle (already collected, or a test-constructed handle).

Source

pub fn agents_def(&self, task_id: &str) -> Option<&SubAgentDef>

Return the definition for a specific agent by task_id.

Source

pub fn agent_transcript_dir(&self, task_id: &str) -> Option<&Path>

Return the transcript directory for a specific agent by task_id.

Source

pub fn transcript_path_for( &self, config: &SubAgentConfig, agent_id: &str, ) -> PathBuf

Resolve the transcript file path for agent_id from config, independent of whether the agent’s handle is still resident in this manager.

Unlike Self::agent_transcript_dir (which only returns a path for agents still tracked in self.agents), this is safe to call after Self::collect has already removed the handle — the path is fully determined by config and agent_id, matching exactly what handle.transcript_dir held at spawn time (see the handle_transcript_dir construction in manager/spawn.rs).

§Examples
use zeph_config::SubAgentConfig;
use zeph_subagent::SubAgentManager;

let mgr = SubAgentManager::new(4);
let config = SubAgentConfig::default();
let path = mgr.transcript_path_for(&config, "task-123");
assert!(path.ends_with("task-123.jsonl"));
Source§

impl SubAgentManager

Source

pub fn approve_secret( &mut self, task_id: &str, secret_key: &str, ttl: Duration, ) -> Result<(), SubAgentError>

Approve a secret request for a running sub-agent.

Called after the user approves a vault secret access prompt. The secret key must appear in the sub-agent definition’s allowed secrets list; otherwise the request is auto-denied.

§Errors

Returns SubAgentError::NotFound if the task ID is unknown, SubAgentError::Invalid if the key is not in the definition’s allowed list.

Source

pub fn deliver_secret( &mut self, task_id: &str, key: &str, value: Secret, ) -> Result<(), SubAgentError>

Deliver a resolved secret value to a waiting sub-agent loop.

Should be called after the user approves the request and the caller has resolved key to its actual vault value (see approve_secret). Requires an active grant for key — delivery is refused if approve_secret was never called or the grant’s TTL has already elapsed, making PermissionGrants::is_active load-bearing rather than unused bookkeeping.

The delivered value is stamped with the grant’s expiry (see GrantedSecret) so the sub-agent loop can keep re-validating the TTL locally on every subsequent tool call, rather than trusting this one-time gate for the remainder of a long-running turn loop.

§Errors

Returns SubAgentError::NotFound if the task ID is unknown, or SubAgentError::Invalid if there is no active grant for key.

Source

pub fn deny_secret(&mut self, task_id: &str) -> Result<(), SubAgentError>

Deny a pending secret request — sends None to unblock the waiting sub-agent loop.

§Errors

Returns SubAgentError::NotFound if the task ID is unknown, SubAgentError::Channel if the channel is full or closed.

Source

pub fn try_recv_secret_request(&mut self) -> Option<(String, SecretRequest)>

Try to receive a pending secret request from any sub-agent (non-blocking).

Polls each active agent’s request channel once. Returns Some((task_id, request)) if any agent has a pending request, or None if all channels are empty. Call this from the main agent loop to surface approval prompts to the user.

Source

pub fn try_recv_secret_request_for( &mut self, task_id: &str, ) -> Option<SecretRequest>

Try to receive a pending secret request from one specific sub-agent (non-blocking).

Unlike try_recv_secret_request, this only polls task_id’s own request channel, so it never pops and discards an unrelated sibling sub-agent’s pending request. Use this when the caller already knows which sub-agent it wants to act on (e.g. an explicit /agent approve <id> command), instead of the pop-then-filter pattern of polling try_recv_secret_request and discarding non-matching results — a discarded result is popped off the channel and lost forever, silently starving the sub-agent that actually sent it.

Returns None if task_id is unknown or has no pending request.

Source§

impl SubAgentManager

Source

pub async fn spawn( &mut self, def_name: &str, task_prompt: &str, provider: AnyProvider, tool_executor: Arc<dyn ErasedToolExecutor>, skills: Option<Vec<String>>, config: &SubAgentConfig, ctx: SpawnContext, ) -> Result<String, SubAgentError>

Spawn a sub-agent by definition name with real background execution.

Returns the task_id (UUID string) that can be used with cancel and collect.

§Errors

Returns SubAgentError::NotFound if no definition with the given name exists, SubAgentError::ConcurrencyLimit if the concurrency limit is exceeded, SubAgentError::SessionSpawnLimit if the session-wide cumulative spawn cap has been reached, or SubAgentError::Invalid if the agent requests bypass_permissions but the config does not allow it (allow_bypass_permissions: false).

Source

pub fn shutdown_all(&mut self)

Cancel all active sub-agents gracefully.

Iterates every agent ID and calls cancel on each. Unlike cancel_all, this method goes through the normal cancel path including hook firing. Prefer this during planned shutdown.

Source

pub fn cancel(&mut self, task_id: &str) -> Result<(), SubAgentError>

Cancel a running sub-agent by task ID.

§Errors

Returns SubAgentError::NotFound if the task ID is unknown.

Source

pub fn cancel_all(&mut self)

Cancel all active sub-agents immediately, revoking their grants.

Used during main agent shutdown or Ctrl+C handling when DagScheduler may not be running. For coordinated scheduler-aware cancellation, prefer DagScheduler::cancel_all.

Source

pub async fn resume( &mut self, id_prefix: &str, task_prompt: &str, provider: AnyProvider, tool_executor: Arc<dyn ErasedToolExecutor>, skills: Option<Vec<String>>, config: &SubAgentConfig, spawn_context: Option<&SpawnContext>, ) -> Result<(String, String), SubAgentError>

Resume a previously completed (or failed/cancelled) sub-agent session.

Loads the transcript from the original session into memory and spawns a new agent loop with that history prepended. The new session gets a fresh UUID.

Returns (new_task_id, def_name) on success so the caller can resolve skills by name.

When spawn_context is Some, constraint propagation is applied identically to spawn: max_trust_level and inherited_tool_allowlist are enforced on the resumed session so resumed agents cannot receive higher privileges than the orchestration policy originally allowed. Pass None to skip constraint propagation (equivalent to the previous behavior before this fix).

The three initial FS reads (prefix lookup, meta load, jsonl load) are offloaded to a spawn_blocking thread so the Tokio executor is not stalled.

§Errors

Returns SubAgentError::StillRunning if the agent is still active, SubAgentError::NotFound if no transcript with the given prefix exists, SubAgentError::AmbiguousId if the prefix matches multiple agents, SubAgentError::Transcript on I/O or parse failure, SubAgentError::ConcurrencyLimit if the concurrency limit is exceeded, or SubAgentError::SessionSpawnLimit if the session-wide cumulative spawn cap has been reached.

Source

pub async fn spawn_for_task<F>( &mut self, def_name: &str, task_prompt: &str, provider: AnyProvider, tool_executor: Arc<dyn ErasedToolExecutor>, skills: Option<Vec<String>>, config: &SubAgentConfig, ctx: SpawnContext, on_done: F, ) -> Result<String, SubAgentError>
where F: FnOnce(String, Result<String, SubAgentError>) + Send + 'static,

Spawn a sub-agent for an orchestrated task.

Identical to spawn but wraps the JoinHandle to send a TaskEvent on the provided channel when the agent loop terminates. This allows the DagScheduler to receive completion notifications without polling (ADR-027).

The event_tx channel is best-effort: if the scheduler is dropped before all agents complete, the send will fail silently with a warning log.

§Errors

Same error conditions as spawn.

§Panics

Panics if the internal agent entry is missing after a successful spawn call. This is a programming error and should never occur in normal operation.

Source§

impl SubAgentManager

Source

pub fn new(max_concurrent: usize) -> Self

Create a new manager with the given concurrency limit.

Source

pub fn session_budget(&self) -> &SessionSpawnBudget

The session-wide cumulative subagent-spawn budget this manager originates (issue #6545).

Returns a reference to this manager’s own budget instance — not a fresh, independent one — so a caller reading through this accessor observes the same cumulative count as every spawn through this manager. See SessionSpawnBudget’s doc comment for why the type itself has no Clone/Arc.

Source

pub fn set_task_supervisor(&mut self, supervisor: TaskSupervisor)

Inject a TaskSupervisor so subagent lifecycle tasks are registered and visible.

Must be called before the first spawn. When set, each spawned agent loop task is registered under its task ID and is observable in TUI status panels and TaskSupervisor::snapshot.

Source

pub fn set_forward_surfaces(&mut self, surfaces: ForwardSurfaces)

Declare which forwarding consumer surfaces are active for this session (issue #6359).

Fixed at session start — call once during bootstrap, before the first spawn. When surfaces.any() is false (the default) or SubAgentConfig::forward_transcript is false, no forwarding sender or drain is ever constructed for any subagent (FR-007). A TaskSupervisor must also be wired via set_task_supervisor — unlike other subagent lifecycle tasks, the forward drain never falls back to an untracked spawn (NFR-002).

Source

pub fn set_delegation_mode(&mut self, mode: DelegationMode)

Set the effective delegation mode gating every future spawn call (spec 042-subagent-delegation-mode-parity, issue #5857).

Call during bootstrap, before the first spawn. The caller MUST fold in the enabled outer kill switch before calling this: pass zeph_config::DelegationMode::Disabled when config.agents.enabled == false, regardless of config.agents.delegation_mode’s configured value (FR-002). The manager itself performs no enabled check — it only sees the already-resolved effective mode.

Source

pub fn delegation_mode(&self) -> DelegationMode

The effective delegation mode currently in force, as set by set_delegation_mode. Exposed for status/observability surfaces (e.g. /agent list) so operators can confirm the active mode without inspecting config.toml directly (spec 042 NFR-004).

Source

pub fn set_secret_registry(&mut self, registry: Arc<SecretMaskRegistry>)

Wire the bootstrap-level secret-mask registry into the forwarding pipeline (issue #6359, security Finding 1 / NFR-005).

When set, every forwarded Text/Thinking chunk has known vault secrets replaced with opaque placeholders before reaching any sink (TUI ring, --bare stdout) — the same registry already applied to the outbound-LLM path via apply_secret_masking. Call during bootstrap, before the first spawn; has no effect on already-spawned drains.

Source

pub fn set_pii_filter(&mut self, filter: PiiFilter)

Wire a PII filter into the forwarding pipeline (issue #6359, security Finding 1 / NFR-005).

When set, every forwarded Text/Thinking chunk is scrubbed for emails, phone numbers, SSNs, etc. before reaching any sink — mirroring the optional PiiFilter layer sub-agent debug dumps already get via PiiScrubbingDumpSink (#6407). The filter itself is unconditionally constructible and self-gates on its own enabled config field, matching the top-level agent’s own PiiFilter construction convention. Call during bootstrap, before the first spawn.

Source

pub fn forwarded_tail(&self, task_id: &str, n: usize) -> Vec<String>

Read the current forwarded-transcript tail for task_id (up to the last n lines).

Returns an empty vector when forwarding is disabled, no surface is active, or the task has not forwarded any lines yet. Backing store for crate::manager::SubAgentManager’s TUI runtime detail view integration (FR-005) — see zeph-core’s refresh_subagent_metrics.

Source

pub fn set_worktree_manager(&mut self, wm: Arc<DefaultWorktreeManager>)

Inject a DefaultWorktreeManager into the manager.

Must be called at most once, before the first spawn. When set, every spawned task acquires the process-level cwd mutex (INV-1) and agents with permissions.worktree = true receive a dedicated git worktree.

Source

pub fn worktree_manager(&self) -> Option<&Arc<DefaultWorktreeManager>>

Returns the live worktree manager, if the worktree subsystem is enabled for this session.

This is the same instance spawn uses to create per-subagent worktrees, so callers (e.g. the /worktree slash command) observe this session’s actual live state rather than a fresh disk scan. Its own prune_branch_on_remove() reflects WorktreeConfig::prune_branch_on_remove — no need to retain a separate copy on SubAgentManager.

Source

pub fn reserve_slots(&mut self, n: usize)

Reserve n concurrency slots for the orchestration scheduler.

Reserved slots count against the concurrency limit in spawn so that the scheduler can guarantee capacity for tasks it is about to launch. Call release_reservation when the scheduler finishes.

Source

pub fn release_reservation(&mut self, n: usize)

Release n previously reserved concurrency slots.

Source

pub fn set_transcript_config(&mut self, dir: Option<PathBuf>, max_files: usize)

Configure transcript storage settings.

Source

pub fn set_stop_hooks(&mut self, hooks: Vec<HookDef>)

Set config-level lifecycle stop hooks (fired when any agent finishes or is cancelled).

Source

pub fn set_fleet_registry(&mut self, registry: SharedFleetRegistry)

Inject a fleet registry so spawned sub-agents appear in the fleet dashboard.

When set, spawn registers the session as Active and collect / cancel mark it terminal. Errors from the registry are logged at warn level and never propagate to callers.

Source

pub fn load_definitions( &mut self, dirs: &[PathBuf], ) -> Result<(), SubAgentError>

Load sub-agent definitions from the given directories.

Higher-priority directories should appear first. Name conflicts are resolved by keeping the first occurrence. Non-existent directories are silently skipped.

§Errors

Returns SubAgentError if any definition file fails to parse.

Source

pub async fn load_definitions_with_sources( &mut self, ordered_paths: &[PathBuf], cli_agents: &[PathBuf], config_user_dir: Option<&PathBuf>, extra_dirs: &[PathBuf], ) -> Result<(), SubAgentError>

Load definitions with full scope context for source tracking and security checks.

The blocking filesystem scan runs on a dedicated thread via tokio::task::spawn_blocking so the tokio worker thread is not stalled (#5108).

§Errors

Returns SubAgentError if a CLI-sourced definition file fails to parse.

Source

pub fn definitions(&self) -> &[SubAgentDef]

Return all loaded definitions.

Source

pub fn definitions_mut(&mut self) -> &mut Vec<SubAgentDef>

Return mutable access to the loaded definitions list.

Intended for test harnesses and dynamic definition registration. Production code should prefer load_definitions.

Source

pub fn insert_handle_for_test(&mut self, id: String, handle: SubAgentHandle)

Insert a pre-built handle directly into the active agents map.

Used in tests to simulate an agent that has already run and left a pending secret request in its channel without going through the full spawn lifecycle.

Trait Implementations§

Source§

impl Debug for SubAgentManager

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<L> LayerExt<L> for L

Source§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in Layered.
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more