Aleph
Architecture

Teams

Team management: SQLite persistence, CoordTask DAG dispatch, leader strategy planning, ACP members, Snapshot / Workflow Canvas.

The teams module provides team management for multi-agent collaboration, supporting structured teamwork with lifecycle management, Coord-Task DAG dispatch, plan approval (Team Strata), and task tracking.

The historical standalone modules/enums that earlier docs referenced — TeamLifecycle, TeamPlan, ApprovalState, plus teams/lifecycle.rs, teams/plans.rs, teams/sessions.rs, teams/artifacts.rs, teams/messages.rs — no longer exist as separate constructs. This page describes the code as it lives in src/teams/ today.

Overview

Teams enables:

  • Team creation — Define teams of agents with a designated leader and members; members may be in-process agents or ACP sessions (TeamMemberKind::Agent | AcpSession)
  • Strategic coordination ("Team Strata") — The leader plans first; the plan is welded into every member's prompt
  • Coord Tasks — Per-member task assignment through the unified coord_tasks DAG in src/agents/swarm/tasks/, with status Pending | Blocked | InProgress | WaitingReview | Completed | Failed | Cancelled | Skipped | Paused | Unsatisfiable
  • Verification loop — Leader accepts or rejects member deliverables via the task_review tool (see Coord Tasks)
  • Team protocol — Each team can set Team.protocol (≤32 KiB); the dispatcher injects it into every member's launch context
  • ACP membersteams.acp_member.add / list / remove wire external CLIs (Claude Code / Codex / Gemini)
  • Snapshot / Kanbanteams.snapshot.* records team state; team_workflow_canvas tool exposes a drag-and-drop board
  • JSON-RPC driven — Full teams.* surface (lifecycle, tasks, messages, snapshots, workflow)

Architecture

src/teams/
├── mod.rs              # entry; re-exports core types
├── store.rs            # SqliteTeamStore: teams / team_members tables
├── types.rs            # Team / TeamMember / TeamMemberKind / TeamStatus / TeamSummary
├── plans.rs            # PlanSubmission / PlanManager
├── context.rs          # TeamInboxContextProvider
├── events.rs           # SqliteEventLogStore / TeamEventLogger
├── artifacts.rs        # ArtifactStore + ArtifactType / TaskArtifact / TaskStatus
├── snapshots.rs        # SqliteSnapshotStore + capture/restore
├── workflow_canvas.rs  # workflow canvas
├── run_mode.rs         # team run-mode (usage_mode pin)
├── leader_prompt.rs    # leader-prompt construction
├── member_provision.rs # member provisioning
├── notifier.rs         # TeamNotifier
├── dispatcher/         # TeamDispatcher (autonomous) + handoff + runner + clarify + acp_bridge + schedule
├── broadcast/          # GroupChatBroadcaster (deterministic fan-out; targets / transcript / member_prompt)
├── messages/           # TeamMessage persistence (store / inbox / router / mentions / aggregator / types)
├── sessions/           # collaborative session (coordinator / store / types)
└── templates/          # builtin team-template loader + materialize + substitute

Core types (src/teams/types.rs)

pub struct Team {
    pub id: TeamId,
    pub name: String,
    pub description: String,
    pub leader_id: String,
    pub status: TeamStatus,         // actually only Active | Disbanded
    pub created_at: i64,
    pub disbanded_at: Option<i64>,
    pub protocol: Option<String>,   // leader-authored operating protocol (≤32 KiB)
}

pub enum TeamStatus { Active, Disbanded }   // snake_case on the wire

pub enum TeamMemberKind { Agent, AcpSession }

pub struct TeamMember {
    pub team_id: TeamId,
    pub agent_id: String,           // Agent: registry id; AcpSession: synthetic `acp:<harness>:<cwd>[:name]`
    pub role: String,
    pub joined_at: i64,
    pub kind: TeamMemberKind,
    pub acp_harness_id: Option<String>,
    pub acp_cwd: Option<String>,
    pub acp_session_name: Option<String>,
}

TeamMember.kind discriminates runtime dispatch — in-process agents flow through MemberDispatchTarget::Agent, ACP sessions through MemberDispatchTarget::AcpSession (src/teams/dispatcher/runner.rs).

TeamSummary is the list view (id / name / description / leader / status / member_count / timestamps) and does not include task counts — those live in the coord_tasks DAG (a separate database); use the team_status tool for an accurate task breakdown.

Storage (SqliteTeamStore)

#[async_trait]
pub trait TeamStore: Send + Sync {
    async fn create_team(&self, input: NewTeam) -> Result<Team>;
    async fn get_team(&self, id: &str) -> Result<Option<Team>>;
    async fn get_team_by_name(&self, name: &str) -> Result<Option<Team>>;
    async fn list_teams(&self) -> Result<Vec<TeamSummary>>;
    async fn disband_team(&self, id: &str) -> Result<()>;
    async fn delete_team(&self, id: &str) -> Result<()>;          // Disbanded only
    async fn add_member(&self, input: NewTeamMember) -> Result<TeamMember>;
    async fn get_members(&self, team_id: &str) -> Result<Vec<TeamMember>>;
    async fn remove_member(&self, team_id: &str, agent_id: &str) -> Result<()>;  // cannot remove leader
    async fn get_agent_teams(&self, agent_id: &str) -> Result<Vec<TeamSummary>>;
    async fn set_protocol(&self, team_id: &str, protocol: Option<String>) -> Result<()>;
    async fn rename_team(&self, id: &str, name: &str) -> Result<()>;
    async fn set_name_auto(&self, id: &str, value: bool) -> Result<()>;
    async fn take_auto_name_flag(&self, id: &str) -> Result<bool>;  // atomic once-only fire
}

pub struct SqliteTeamStore { /* Arc<Mutex<Connection>> */ }

MAX_PROTOCOL_LEN = 32 KiB is enforced — the protocol is rendered verbatim into the leader's launch context, so an unbounded value would let whoever can call set_protocol inject arbitrary instructions into the leader context.

Strategic coordination ("Team Strata")

Teams have a leader and members. On a team's first message, the leader plans first — before any member begins work. This planning pass fires once per team (a fire-once strategy keyed by the team, race-safe via atomic put_if_absent), and the resulting plan is welded into every member's prompt so that all members share the same strategic context.

First message to a team


  Leader plans first  ──►  Strategy (fire-once, keyed by team; atomic put_if_absent)


  Plan welded into every member's prompt


  Members work under assigned Coord Tasks

team_key(team_id) is one of the composite-key namespaces (alongside loop / goal / session); see Loop / Goal / Strategy.

Coord Tasks

coord_tasks is the unified DAG (in src/agents/swarm/tasks/), not a table owned by src/teams/. Members' work is expressed through it:

StatusMeaning
Pendingwaiting on deps + dispatcher pickup
BlockedDerived (never stored): deps unresolved
InProgressdispatcher claimed, running
WaitingReviewrun finished, awaiting lead review; downstream stays blocked
Completedterminal-success (satisfies deps)
Failedterminal-failure (does not satisfy deps; triggers derived Unsatisfiable)
Cancelledcancelled
Skippedmanually marked not-required (still satisfies deps)
Pausedmanually paused; dispatcher does not claim; does not satisfy deps
UnsatisfiableDerived: a dependency terminally failed; the task can never run on its own

Blocked and Unsatisfiable are derived at query time; from_stored deliberately rejects both. CoordTaskStatus::is_terminal() (stored-terminal) and is_settled() (observer-side) give the predicates used by team / workflow completion checks.

Dependency edges are stored in coord_task.blocked_by. The dispatcher in src/teams/dispatcher/schedule/ (select / settle / reclaim / failure) keeps readiness, reschedule, zombie reclamation, and failure propagation correct.

Verification loop: task_review

// src/builtin_tools/team/task_review.rs
#[derive(Clone)] pub struct TaskReviewTool { /* … */ }
impl AlephTool for TaskReviewTool {
    const NAME: &'static str = "task_review";

    async fn call(&self, args: TaskReviewArgs) -> Result<TaskReviewOutput> {
        // soft leader-only guard (prompt-gating is the primary gate; this is defense-in-depth)
        // Approve → CoordTaskStatus::Completed (satisfies deps)
        // Reject  → CoordTaskStatus::InProgress (owner redoes)
        // when task metadata.require_grounding=true and approve has no grounding → bounce
    }
}
// task_review tool call
{
  "task_id": "<coord-task-id>",
  "decision": "approve",       // or "reject"
  "feedback": "…",             // written back to owner on reject
  "grounding": {               // optional; required on approve when require_grounding=true
    "kind": "exit_code",       // exit_code | numeric | line_count
    "source": "cargo test -p alephcore --lib",
    "value": "0"
  }
}

The leader's accept/reject decision goes through task_review (not a JSON-RPC method). Members work under Coord Tasks created/updated via teams.create_task / teams.update_task. workflow_step_review is the equivalent tool for workflow steps (src/builtin_tools/team/workflow_step.rs).

G2 — per-task git-worktree isolation

execute_member_task (src/teams/dispatcher/runner.rs) optionally wraps each member task in a detached-HEAD git worktree, wrapping WorktreeSandbox and pointing the run's workspace_override at the worktree so concurrent workers cannot corrupt each other's index. Outside a git repo the call silently falls back to the pre-G2 behaviour and logs a warning.

Team run-mode pin

26.7.25+: when teams.run runs, the leader stamps usage_mode (chat / work / code) and inherits it to sub-members:

  • Sub-agents cannot invoke session_set_mode — inheritance is one-way
  • teams.run rejects any session_set_mode request from a sub-agent
  • When the leader switches mode via task_review, the entire team re-aligns
// src/teams/run_mode.rs
if turn.is_subagent() {
    ensure!(turn.parent_mode.is_some(), "team run mode pin must be carried");
    ensure!(request.action != Some("session_set_mode"),
        "subagents inherit parent mode; cannot switch mid-team-run");
}

Configuration

[teams]
enabled = true
max_team_size = 10
require_plan_approval = true

[team_broadcast] exposes tunable BroadcastConfig parameters (max_chain_depth / max_fanout_width / max_total_activations / transcript_token_budget / member_run_timeout_secs).

JSON-RPC surface

GroupMethods
Lifecycleteams.create (leader + members + optional auto_name), teams.get, teams.list, teams.rename, teams.disband, teams.delete
Work-threadteams.chat.send, teams.chat.thread, teams.chat.history, teams.chat.cancel
Coord Tasksteams.create_task, teams.update_task, teams.list_tasks, teams.add_task_comment, teams.list_task_comments, teams.list_task_events, teams.list_task_runs
Task controlteams.task.pause, teams.task.resume, teams.task.retry, teams.task.skip, teams.task.trace, teams.task.journal.get, teams.task.journal.list
Snapshotsteams.snapshot.create, teams.snapshot.get, teams.snapshot.list, teams.snapshot.restore, teams.snapshot.delete
Workflowteams.workflow.export_canvas, teams.workflow.import_canvas, teams.workflow.approve_step, teams.workflow.reject_step, teams.workflow.retry_step
ACP membersteams.acp_member.add, teams.acp_member.list, teams.acp_member.remove
Miscteams.list_templates, teams.usage

The durable per-team group-chat thread is exposed through teams.chat.send / teams.chat.thread / teams.chat.history / teams.chat.cancel; this history powers the attribution-bubble replay in the group chat window. The agents.teams method returns a team list augmented with members_preview + last_message.

Team templates (src/teams/templates/)

Members of built-in team templates can declare their tool surface (sharing agents.tools_schema); no longer hardcoded:

  • templates/builtin/ — built-in templates
  • templates/loader.rs — template loader
  • templates/materialize.rs — materialize a template into Team + member rows
  • templates/substitute.rs${var} substitution
  • templates/types.rs — template types

loop-auditor is the 26.7.21+ built-in audit / supervision template; it defaults to independent-context evidence.

See Also

On this page