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
impl SubAgentManager
Sourcepub async fn collect(&mut self, task_id: &str) -> Result<String, SubAgentError>
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.
Sourcepub async fn def_name_for_resume(
&self,
id_prefix: &str,
config: &SubAgentConfig,
) -> Result<String, SubAgentError>
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.
Sourcepub fn statuses(&self) -> Vec<(String, SubAgentStatus)>
pub fn statuses(&self) -> Vec<(String, SubAgentStatus)>
Return a snapshot of all active sub-agent statuses.
Sourcepub fn is_task_finished(&self, task_id: &str) -> bool
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).
Sourcepub fn agents_def(&self, task_id: &str) -> Option<&SubAgentDef>
pub fn agents_def(&self, task_id: &str) -> Option<&SubAgentDef>
Return the definition for a specific agent by task_id.
Sourcepub fn agent_transcript_dir(&self, task_id: &str) -> Option<&Path>
pub fn agent_transcript_dir(&self, task_id: &str) -> Option<&Path>
Return the transcript directory for a specific agent by task_id.
Sourcepub fn transcript_path_for(
&self,
config: &SubAgentConfig,
agent_id: &str,
) -> PathBuf
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
impl SubAgentManager
Sourcepub fn approve_secret(
&mut self,
task_id: &str,
secret_key: &str,
ttl: Duration,
) -> Result<(), SubAgentError>
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.
Sourcepub fn deliver_secret(
&mut self,
task_id: &str,
key: &str,
value: Secret,
) -> Result<(), SubAgentError>
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.
Sourcepub fn deny_secret(&mut self, task_id: &str) -> Result<(), SubAgentError>
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.
Sourcepub fn try_recv_secret_request(&mut self) -> Option<(String, SecretRequest)>
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.
Sourcepub fn try_recv_secret_request_for(
&mut self,
task_id: &str,
) -> Option<SecretRequest>
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
impl SubAgentManager
Sourcepub 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>
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).
Sourcepub fn shutdown_all(&mut self)
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.
Sourcepub fn cancel(&mut self, task_id: &str) -> Result<(), SubAgentError>
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.
Sourcepub fn cancel_all(&mut self)
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.
Sourcepub 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>
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.
Sourcepub 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>
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>
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
impl SubAgentManager
Sourcepub fn new(max_concurrent: usize) -> Self
pub fn new(max_concurrent: usize) -> Self
Create a new manager with the given concurrency limit.
Sourcepub fn session_budget(&self) -> &SessionSpawnBudget
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.
Sourcepub fn set_task_supervisor(&mut self, supervisor: TaskSupervisor)
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.
Sourcepub fn set_forward_surfaces(&mut self, surfaces: ForwardSurfaces)
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).
Sourcepub fn set_delegation_mode(&mut self, mode: DelegationMode)
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.
Sourcepub fn delegation_mode(&self) -> DelegationMode
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).
Sourcepub fn set_secret_registry(&mut self, registry: Arc<SecretMaskRegistry>)
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.
Sourcepub fn set_pii_filter(&mut self, filter: PiiFilter)
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.
Sourcepub fn forwarded_tail(&self, task_id: &str, n: usize) -> Vec<String>
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.
Sourcepub fn set_worktree_manager(&mut self, wm: Arc<DefaultWorktreeManager>)
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.
Sourcepub fn worktree_manager(&self) -> Option<&Arc<DefaultWorktreeManager>>
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.
Sourcepub fn reserve_slots(&mut self, n: usize)
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.
Sourcepub fn release_reservation(&mut self, n: usize)
pub fn release_reservation(&mut self, n: usize)
Release n previously reserved concurrency slots.
Sourcepub fn set_transcript_config(&mut self, dir: Option<PathBuf>, max_files: usize)
pub fn set_transcript_config(&mut self, dir: Option<PathBuf>, max_files: usize)
Configure transcript storage settings.
Sourcepub fn set_stop_hooks(&mut self, hooks: Vec<HookDef>)
pub fn set_stop_hooks(&mut self, hooks: Vec<HookDef>)
Set config-level lifecycle stop hooks (fired when any agent finishes or is cancelled).
Sourcepub fn set_fleet_registry(&mut self, registry: SharedFleetRegistry)
pub fn set_fleet_registry(&mut self, registry: SharedFleetRegistry)
Sourcepub fn load_definitions(
&mut self,
dirs: &[PathBuf],
) -> Result<(), SubAgentError>
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.
Sourcepub async fn load_definitions_with_sources(
&mut self,
ordered_paths: &[PathBuf],
cli_agents: &[PathBuf],
config_user_dir: Option<&PathBuf>,
extra_dirs: &[PathBuf],
) -> Result<(), SubAgentError>
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.
Sourcepub fn definitions(&self) -> &[SubAgentDef]
pub fn definitions(&self) -> &[SubAgentDef]
Return all loaded definitions.
Sourcepub fn definitions_mut(&mut self) -> &mut Vec<SubAgentDef>
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.
Sourcepub fn insert_handle_for_test(&mut self, id: String, handle: SubAgentHandle)
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§
Auto Trait Implementations§
impl !Freeze for SubAgentManager
impl !RefUnwindSafe for SubAgentManager
impl !UnwindSafe for SubAgentManager
impl Send for SubAgentManager
impl Sync for SubAgentManager
impl Unpin for SubAgentManager
impl UnsafeUnpin for SubAgentManager
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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 moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request