Agent teams — message-based collaboration + proactive agents over the eval control plane
Status: doc-first SSOT (2026-07-24). The product direction for Everdict's own agent beyond the single conversational assistant. Successor/extension of agent-conversations.md (the single-agent runtime — kernel, tools, sub-agents) and the message-based generalization of notifications.md (the per-user event feed).
Vision (maintainer, verbatim intent): (1) the agent must be able to drive eval directly — Everdict evolves so its own agent runs harnesses/scorecards, not just talks about them; (2) collaboration between autonomous agents is central — teammate messaging; (3) everything Everdict monitors becomes message-based, so a proactive agent team can watch the platform and act.
Where we are (the seeds already in the kernel)
The single-agent runtime already ships the primitives this direction composes from — nothing here is a rewrite, it is a generalization:
drainInputseam (kernel/loop.ts) — the loop absorbs externally-supplied messages at each turn boundary. Today the host feeds it user steering; it is source-agnostic by construction.InputQueue(apps/agent) — a per-session inbox the streaming turn drains. The nascent mailbox.- Background sub-agents (
spawn_agent run_in_background) — detached agents whose results are folded back into a later turn as a message. Already message-shaped delivery, in-process. subagent_typeregistry — specialized delegate roles (explore/analyze). The seed of named teammates.- Permission modes + fine-grained rules (
3e48d9f3) — the governance an agent needs before it may drive eval (write tools) autonomously. notifications.mdfeed — the control plane already emits run/scorecard-completion events to a per-user feed. Those same events are the messages a proactive agent subscribes to.
The gap is not the loop — it is (a) a shared message substrate with addressing + attribution, (b) an agent-to-agent channel over it, (c) a bridge from monitoring events → agent messages, and (d) the capability + isolation for an agent to run eval work safely.
Principles
- One envelope, many sources. A user steering note, a teammate's message, and a "scorecard sc_123
regressed" event are the same kind of thing — an addressed message with a sender. They flow through one
substrate and one
drainInputseam, rendered with attribution so the model knows who is speaking. - Addresses, not call stacks. Collaboration is routing to a recipient (a session / a named teammate / a
team), not a nested function call.
spawn_agent(call-stack delegation) stays for scoped fan-out; teammates are longer-lived addressable participants. - Proactive = an event wakes an agent. Monitoring is message production; a subscribed agent is woken to react. The trigger is the same substrate — no special path.
- Autonomy is gated, not ungoverned. An agent driving eval uses write tools behind the permission modes/rules; file-mutating eval work runs under execution isolation (the orchestrator's existing sandbox).
- Reuse the control plane. Everdict already dispatches/isolates/schedules eval jobs. An agent driving
eval calls the same MCP tools a human would (
run_scorecard,pin_harness_images, …) as its own authenticated principal — the RBAC + tenancy already bound it.
Architecture
┌─────────────── message substrate (mailbox / bus) ───────────────┐
user ──────▶│ envelope { to, from: user|agent|event, sender?, content, kind } │
teammate ──▶│ addressed by recipient (session id / agent name / team) │──▶ drainInput ─▶ loop turn
monitoring ▶│ attribution-rendered on drain │
└──────────────────────────────────────────────────────────────────┘
▲ ▲
SendMessage tool (agent→agent) event bridge (notifications feed → agent inbox)
- Substrate.
AgentMailbox(generalizedInputQueue):enqueue(recipient, envelope)/drain(recipient) → ChatMessage[]. Envelope carriesfrom(user | agent | event), optionalsender, andcontent. Drain renders attribution: user → verbatim; agent →[message from teammate <sender>] …; event →[everdict event — <sender>] …. This slice is the foundation; it lands first (S1). - Agent-to-agent (
send_message). A kernel tool (host-routed) that posts an envelope{to, from:agent, sender:self}to a recipient's mailbox. The recipient absorbs it via its owndrainInput. Bounded like sub-agents (no unbounded broadcast without a cap). - Teammates. Longer-lived agent sessions with a name + address, spawned into a team (a named group).
Unlike a background sub-agent (fire-and-forget, folds one summary back), a teammate persists and exchanges
messages. Built on the substrate +
send_message. - Event bridge. The control plane's notification emitter (
notifications.md) also publishes to subscribed agent inboxes: a scorecard completes / regresses / a runtime queue backs up → aneventenvelope to the agents watching that workspace/harness. Opt-in subscription per agent/team. - Proactive trigger. When an event lands in an idle agent's mailbox, the host wakes a turn (a triggered run, like a schedule fire) so the agent reacts without a human prompt. Reuses the scheduling path; the turn drains the event as its first message.
- Agent drives eval. The agent's tool surface gains the write control-plane tools (run/pin/schedule) behind the permission modes/rules; a harness-authoring or file-mutating task runs the eval through the existing dispatch → isolated job path (no new sandbox — the orchestrator already isolates).
- Task ledger (SHIPPED — the durable half of coordination). The mailbox coordinates the conversation;
the workspace task ledger coordinates the work:
AgentTaskRecord(subject · description · status pending→in_progress→completed|cancelled · owner · informationalblockedBychains · agent-attributedorigin; mig 0102, workspace-shared by design — no private tier) behindTaskService, exposed on BOTH transports (/tasksroutes + the create_task/list_tasks/get_task/update_task/delete_task MCP family; authz reusesagents:read/agents:write, delete = creator·admin). Lifecycle FACTS are emitted at the service choke point —task.created/task.claimed/task.completed/task.cancelled— and created/completed are trigger-matchable: the "new work appeared" / "a dependency cleared" wake-up pair, so a teammate agent picks up work without a human prompt. Loop safety: an agent-created task stampscausedBy agent:<id>:<conv>(the creator never wakes on its own task); claiming (→ in_progress) without naming an owner records the claimer. Claude Code's TaskCreate/TaskList tool family, reinterpreted server-side. The web board is/[workspace]/agents/tasks(status chips · inline composer · claim/complete/cancel/reopen row verbs) — the fleet page shows the RUNS, the board shows the WORK.
Roadmap (staged; each stage is shippable + testable on its own)
-
S1 — message substrate (attribution-aware mailbox). Generalize
InputQueue→AgentMailboxwith typed envelopes + attribution rendering; addPOST /sessions/:id/event(anevent-attributed message). User/inputbecomesfrom:user. No kernel change — attribution is rendered host-side into the drained content. ← the first code slice, landing with this doc. -
S2 —
send_message(agent→agent over the substrate). Kernelsend_messagetool + host routing to a recipient mailbox; bounded. Enables two agents in one workspace to exchange messages. First increment LANDED — bidirectional background sub-agents: each running background sub-agent gets an in-kernel inbox; the parent'ssend_message(to: 'bg-N', message)routes into it and the sub-agent drains it at its next step (attributed[Message from the delegating agent]), turning fire-and-forget delegates into two-way collaborators. Deliveries to a finished/unknown id are a soft error. Reverse (sub→parent) is the existing result fold-in. Generalizingsend_messageto arbitrary session/teammate recipients is S3. -
S3 — teammates + teams. Named, longer-lived agent sessions in a team;
spawn_agentgains a persistent-teammate variant; team roster storage. Collaboration beyond fan-out-and-summarize. Execution-control core LANDED —TeammateSupervisor: a teammate is a session the supervisor watches; when a message lands in its mailbox the supervisor wakes a turn, and turns are serialized per teammate (one at a time; mid-turn wakes coalesce into a single follow-up — no pile-up, no lost wake). This is the same "a message/event wakes an agent" primitive S5 (proactive) reuses.runTurn(sessionId)is injected (drain mailbox → run one agent turn); the supervisor owns only WHEN a turn runs, so it stays pure + unit-tested. S3 is now LIVE end-to-end (docs/architecture/agent-execution-auth.md landed the auth):POST /agent/teammates {name, task}mints the teammate'sagt_token (issueAgentToken, acts AS the creator), creates its session, registers it with the supervisor, seeds the task, and wakes it — it runs arunTeammateTurn(authenticated request-less) that processes the task. A peer'ssend_messageor a platform/eventto a teammate's sessiondeliver()s into its mailbox AND wakes it, so it reacts autonomously. Thespawn_teammateagent TOOL is in too — an agent (not just the web) spawns a teammate (hostspawnTeammateseam → the samespawnTeammateFor), so agents autonomously grow their own team and coordinate oversend_message. The roster is in too:GET /agent/teammates(the caller's live teammates) +DELETE /agent/teammates/:id(stop — unregister + revoke its execution token; keeps the transcript). The web surface is in too: a Team menu in the chat header (apps/webfeatures/agent-chat/ui/team-menu.tsxover two same-origin BFF proxy routes) lists the caller's teammates (name + watched kinds), spawns one (name + standing task + watched completion kinds), and stops one — with a roster badge that refreshes after each turn so a self-spawned teammate appears. Remaining polish: a namedteamgrouping. The agent can also DISCOVER its team (alist_teammatestool over the roster) to coordinate over send_message. The core — persistent, addressable, autonomous, self-spawning, discoverable, collaborating, stoppable, human-visible agents — is in. -
S4 — event bridge (monitoring → agent inbox). Agent-side LANDED. A teammate subscribes to event kinds (spawn_teammate / POST /agent/teammates
watch: [...]), andPOST /agent/events {kind, source, message}fans the event out to the caller's teammates that watch that kind — delivering it (attributed) into each mailbox and waking it, so it reacts proactively (S5 too). Nothing watches the kind → notified:0./agent/eventsalso takes an INTERNAL path (x-internal-token,AGENT_INTERNAL_TOKEN): the control plane presents the shared secret + a{workspace, recipient}and the event fans to THAT recipient's watching teammates — so the emitter can drive events for any user without impersonating them. S4 is now COMPLETE end-to-end: the control plane'sNotificationService(its third channel beside the feed + Mattermost) pushes arun.completed/run.failed/scorecard.completed/scorecard.failedevent via anAgentEventSink(an HTTP client to/agent/events, wired whenAGENT_SERVICE_URL+AGENT_INTERNAL_TOKENare set) — best-effort, so an unreachable agent never affects the result. Everything Everdict monitors now auto-drives the proactive team: a run/scorecard completes → its creator's watching teammates wake and react. The web surface for spawning/watching teammates is in (S3 note). Remaining: only richer event kinds — and note that regression detection (scorecard.regressed) belongs in the AGENT (a watcher diffs via MCPdiff_scorecards), not the control plane, which emits only facts (completed/failed). So the event model is already complete. -
S5 — proactive triggers. An event in an idle subscribed agent's mailbox wakes a turn (reuse the schedule-fire path). The proactive agent team is live.
-
S6 — agent drives eval (write capability). Expose the write control-plane tools to the agent behind permission modes/rules; wire file-mutating eval work through the dispatch/isolation path (+ worktree isolation for local file work). The agent runs, not just discusses, evals.
Design (ready to drop in).
apps/agent/src/mcp-tools.tsalready gates the base surface with an allowlist: read verbs (skip the HITL gate) + a curatedINTEGRATION_ACTIONSset (Mattermost/CI/registry actions, bridgedisReadOnly:falseso the HITL gate approves each). S6 adds a siblingEVAL_ACTIONScurated allowlist — the eval-driving verbs — exposed opt-in (AGENT_ALLOW_EVAL_DRIVE, default off; the default agent stays read-only). When on,isDefaultBaseToolalso admitsEVAL_ACTIONSandisBaseToolReadOnlyexcludes them (→ every call is HITL/plan/rule-gated). ConcreteEVAL_ACTIONS:- runs/scorecards:
run_scorecard·retry_scorecard·rerun_scorecard·cancel_scorecard·ingest_scorecard·pull_scorecard·submit_run·backfill_scorecard_models - harness/dataset/judge/model/runtime authoring:
register_harness·register_harness_template·pin_harness_images·create_dataset·create_judge·create_model·create_runtime·set_{harness,dataset,judge,model,runtime}_version_tags·assign_harness_trace_{source,sink}·set_harness_span_attr_mapping - scheduling/ops/import/view:
create_schedule·update_schedule·control_runtime·import_benchmark·import_terminal_bench·apply_bundle·create_view·create_comment
Excluded even with eval-drive on (destructive/governance/secret — never the agent's job):
delete_*·remove_*·revoke_*·unlink_*·set_secret·set_workspace_*·create_workspace·delete_workspace·set_member_role·create_api_key·create_invite·pair_*·github_*·link_ci_repository·set_budget_limit. Backstop: the agent acts as its authenticated principal, so the control-plane RBAC blocks anything its role can't do regardless of the allowlist — the allowlist is defense-in-depth + intentional scoping, not the only guard.Status: S6 LANDED, then SUPERSEDED by bridge-all + permission modes. The curated
EVAL_ACTIONSopt-in (AGENT_ALLOW_EVAL_DRIVE) shipped first; the confirmed direction (2026-07-27, with the user) then replaced surface-shaping with mode-governed access: the agent's base surface is now the WHOLE control-plane catalog (mutations included; only the runner wire-protocol tools are excluded), and every mutation is decided by the session's permission mode —default(ask every time) ·auto(ask only guarded destructive/governance/ credential actions) ·bypass(never ask) ·plan(read-only until the plan is approved) — on top of the control-plane RBAC. The policy lives inapps/agent/src/action-policy.ts(protocol exclusion + the guarded class, which absorbs the old FORBIDDEN list as "ask even in auto" instead of "never see"); the mode lives on the session (AgentSessionRecord.permissionMode, chat-header picker, per-turnbody.modeoverride). Seedocs/architecture/agent-conversations.md. Remaining for a fuller S6: file-mutating eval work under worktree isolation (the write control-plane actions above already dispatch through the orchestrator's own isolation). - runs/scorecards:
Non-goals / guardrails
- Not a new execution engine — teammates and proactive runs reuse the loop + the scheduler/dispatch paths.
- Not ungoverned autonomy — write/eval-driving capability is always behind the permission layer (
3e48d9f3) and the control-plane RBAC; a proactive agent can never exceed its principal's role. - Not a broadcast free-for-all —
send_message/ event fan-out is capped like sub-agent concurrency.