Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
- System-originated inbound (`message.received` without the operator flag —
background shell exits, mailbox mail, fleet-dry and compact continuations)
paints as a visible system row instead of a user prompt.
- After a parked `ask_director` is surfaced (TUI idle-send wake, nested
`wait_agents` with a question payload, or a successful `list_agents`),
further `list_agents` calls fail closed until `send_input` answers or the
ask is dropped. Do not poll `list_agents`.


## [0.3.21] - 2026-09-11

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ Three distinct concepts (do not conflate them):

The **`spawn_agent`** tool starts a fleet agent on a separate inference source (tier/profile resolved from settings) and returns immediately with an `agent_id`. On the TUI primary, Skywalker idles after spawn and occupancy delivers mailbox mail as system inbound when a worker finishes or fails (including while siblings still run). Nested orchestrators still collect with **`wait_agents`**. Declared fan-out is unlimited: excess dispatches enqueue rather than fail. `run()` is admitted by `src/subagent/admission.ts` (default burst window of 8 is race-avoidance so a 429 freeze can fire before a herd — not a declared-spawn cap). Occupancy is the whole first `run()`, including `wait_agents`. Nested children of an already-admitted parent bypass **capacity** so a nested orchestrator cannot deadlock while holding a slot; they still wait on a provider 429 pause. Drain is FIFO among currently admissible jobs (a paused provider is skipped, not head-of-line for every provider). Resume and followup inference re-enter the same queue. Queued workers report wait/list status `queued` (live, not failed). Lowering capacity never cancels in-flight work. Retryable provider 429s freeze new admits via the shared retry remapper in `createCorbitsRetryPolicy`; `quota_exhausted` does not freeze. `list_agents` remains mailbox-scoped. The dispatch brief separates durable `context`, actionable `prompt`, and optional `goals` (checklist seeds for the _child's_ own `manage_tasks` list). Implement/review dispatches (and their default directors) fail closed without non-empty `success_criteria`. The child returns a structured report (`Summary` / `Findings` / `Blockers` / `Paths`) plus a tools-used footer. Parent and child never share a `manage_tasks` list.

Workers ask the spawning parent with **`ask_director`** (not the human). That parks a question while the worker stays `running`. On the TUI primary that arrives as an idle-send wake (and yields an in-flight `wait_agents` as a timeout without the question payload). Nested **`wait_agents`** still returns `awaiting_director` with a question payload — that is not terminal. The parent answers with **`send_input`**, then continues. Escalate to the human with **`ask_operator`** only when the parent cannot resolve it.
Workers ask the spawning parent with **`ask_director`** (not the human). That parks a question while the worker stays `running`. On the TUI primary that arrives as an idle-send wake (and yields an in-flight `wait_agents` as a timeout without the question payload). Nested **`wait_agents`** still returns `awaiting_director` with a question payload — that is not terminal. Once a parked ask is surfaced (TUI wake, nested `wait_agents` with a question payload, or a successful `list_agents`), further `list_agents` calls fail closed until **`send_input`** answers or the ask is dropped — `list_agents` is not a poll. The parent answers with **`send_input`**, then continues. Escalate to the human with **`ask_operator`** only when the parent cannot resolve it.

When the parent TUI is not blocked in `wait_agents`, the runner publishes an authoritative snapshot of currently pending top-level questions on each store notification, including empty snapshots before fleet-count updates. During synchronous session rotation, a runner-owned barrier suppresses both publications before delivery-generation invalidation, transcript clearing, and worker cancellation; successful reset reconciles a fresh snapshot before resuming asynchronous backend rebuild. The bridge drops resolved, cancelled, replaced, terminal, and removed asks and delivers each session/question identity once while pending. A coalesced wake starts only when the parent is not processing and every operator gate is closed, including parent-idle fleet holds where the shell stays busy. Worker gates do not manufacture parent processing. Replies use `send_input`'s `target` field with the worker session ID, never its shared catalog ID. Synthetic wakes use `SessionPort.deliver` through queued-delivery's idle-send path without entering the user follow-up queue or composer `/feedback` capture.

Expand Down
2 changes: 1 addition & 1 deletion docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Corbits Code fans work out to short-lived **fleet agents** — workers with thei

- **Agents** are runtime entities (primary session or child).
- **Tasks** are checklist items owned by one agent via `manage_tasks`.
- **Fleet agents** are spawned with `spawn_agent`. On the TUI primary, mailbox mail arrives as inbound when a worker finishes or fails — do not poll `wait_agents`. Nested orchestrators still collect with `wait_agents`. Workers ask the parent with `ask_director`. That parks a question while the worker stays `running`. Nested `wait_agents` returns `awaiting_director` with a question payload — that is not terminal. The parent answers with `send_input` (`target` = the worker's session id). When the parent TUI is not blocked in `wait_agents`, a parked question arrives as a synthetic idle-send wake. Escalate to the human only with `ask_operator`.
- **Fleet agents** are spawned with `spawn_agent`. On the TUI primary, mailbox mail arrives as inbound when a worker finishes or fails — do not poll `wait_agents`. Nested orchestrators still collect with `wait_agents`. Workers ask the parent with `ask_director`. That parks a question while the worker stays `running`. Nested `wait_agents` returns `awaiting_director` with a question payload — that is not terminal. Once a parked ask is surfaced, `list_agents` fails closed until `send_input` answers or the ask is dropped — do not poll `list_agents`. The parent answers with `send_input` (`target` = the worker's session id). When the parent TUI is not blocked in `wait_agents`, a parked question arrives as a synthetic idle-send wake. Escalate to the human only with `ask_operator`.

Dispatch uses a structured brief (context / goal / optional goals seed) and returns a structured report. The TUI Agents strip and fleet board show who is running; live tool progress updates the status bar without dumping the child transcript into the parent chat. There is no turn budget. A tool-less final turn completes only with the four-heading report envelope. Printed `<tool_call>` markup in assistant text gets one corrective nudge to issue a real tool call and does not count as the wrap-up; without the envelope, one incomplete-report nudge is given and a second tool-less turn without the envelope salvages as `incomplete-report-stop`. A silent worker (no activity for `stallTimeoutMs`, opt-in) gets one continuation nudge, then salvages as `stalled` only after a full `stallTimeoutMs` grace with still no activity — queued checks inside that window wait, they do not salvage. An opt-in `deadlineMs`, or an operator cancel, can also end a run early. Each of these returns a salvage report so a runaway or idle child cannot quietly burn a large token budget or look done after prose alone.

Expand Down
3 changes: 2 additions & 1 deletion src/agent/directors/skywalker/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ describe("skywalkerPackage", () => {
const p = skywalkerPackage.systemPrompt;
expect(p).toContain("ask_director");
expect(p).toContain("send_input");
expect(p).toContain("awaiting_director");
expect(p).toContain("Do not poll list_agents");
expect(p).not.toContain("list_agents shows awaiting_director");
expect(p).toContain("idle-send");
expect(p).toMatch(/target = (that worker's |worker )session id/);
expect(p).not.toMatch(
Expand Down
4 changes: 2 additions & 2 deletions src/agent/directors/skywalker/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ You do not do the specialists' jobs by default. For tiny bounded product edits,

Do not run long-blocking jobs on the parent (evals, full test suites, long installs, long-running implementation). Dispatch intern (mechanical shell), tester (suite / repro), or builder (substantial code). Path tools (write_file/edit_file/delete_file) are the DIY surface; shell file-writes stay denied.

Idle-orchestrator: fire one or more spawn_agent calls in a turn — each returns immediately with an agent_id and does not hold the parent. Then **reply to the operator** with who is running and **end the turn**. Workers keep running while you are idle; mailbox mail arrives as inbound when a worker finishes or fails — read it and decide the next action. Do not poll wait_agents. wait_agents is optional/deprecated on this primary parent (nested orchestrators such as greybeard still collect with it). list_agents shows the fleet without blocking. interrupt_agent unblocks an in-flight wait immediately. Enter mid-run delivers at the next parent tool.boundary — a long parent foreground run_shell or awaiting wait_agents holds those steers (start long commands with run_shell background:true instead). A bare spawn_agent does not. When the fleet goes dry the runtime re-enters with collected reports.
Idle-orchestrator: fire one or more spawn_agent calls in a turn — each returns immediately with an agent_id and does not hold the parent. Then **reply to the operator** with who is running and **end the turn**. Workers keep running while you are idle; mailbox mail arrives as inbound when a worker finishes or fails — read it and decide the next action. Do not poll wait_agents. wait_agents is optional/deprecated on this primary parent (nested orchestrators such as greybeard still collect with it). list_agents shows the fleet without blocking; do not poll list_agents. interrupt_agent unblocks an in-flight wait immediately. Enter mid-run delivers at the next parent tool.boundary — a long parent foreground run_shell or awaiting wait_agents holds those steers (start long commands with run_shell background:true instead). A bare spawn_agent does not. When the fleet goes dry the runtime re-enters with collected reports.

# Operator updates (mandatory while fleet is live)

You are the chat surface. Workers cannot ask_operator; they ask_director. A parked question arrives as an idle-send wake (list_agents shows awaiting_director) — answer with send_input using target = that worker's session id. Escalate with ask_operator only when you cannot resolve it. While any specialist is running:
You are the chat surface. Workers cannot ask_operator; they ask_director. A parked question arrives as an idle-send wake — answer with send_input using target = that worker's session id. Do not poll list_agents. Escalate with ask_operator only when you cannot resolve it. While any specialist is running:
- After every spawn wave: short status (who, goal, what you are waiting on) then end the turn.
- On mailbox mail or a finished report: short update — do not go silent.
- When the operator messages mid-run: answer them first (COMMUNICATION). Do not make them wait on an in-flight wait_agents if you can end/timeout the wait and reply.
Expand Down
4 changes: 3 additions & 1 deletion src/agent/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function buildGuidelines(
: [
"",
"Orchestration:",
"- Break multi-step or parallel work into focused worker dispatches with distinct lenses; prefer `spawn_agent` (fire several in one turn when jobs are independent), then reply with who is running and end the turn — workers keep running while you are idle. Mailbox mail arrives as inbound when a worker finishes; read it and do not poll `wait_agents`. Nested orchestrators still collect with `wait_agents`. `list_agents` shows the fleet without blocking.",
"- Break multi-step or parallel work into focused worker dispatches with distinct lenses; prefer `spawn_agent` (fire several in one turn when jobs are independent), then reply with who is running and end the turn — workers keep running while you are idle. Mailbox mail arrives as inbound when a worker finishes; read it and do not poll `wait_agents`. Nested orchestrators still collect with `wait_agents`. `list_agents` shows the fleet without blocking; after a parked ask is surfaced, answer with `send_input` and do not poll `list_agents`.",
"- Pass the typed spawn contract: `intent`, `success_criteria` (done-when; required for implement/review and their default directors), `do_not` (scope fence), and `report_focus`. Free-form `prompt` without `success_criteria` fail-closes for implement/review and their default directors.",
"- After workers return, classify fail / incomplete-report vs parent-initiated interrupt vs operator-cancel vs clean complete. Fail-path (`status: failed` or salvage `incomplete-report`): diagnose from the report or error and MAY spawn one successor with a changed brief. Parent-initiated interrupt (`interrupt_agent` / `send_input` with `interrupt:true` unblocks wait with `stop_reason: interrupted`): the worker is often still running and often has no report — `resume_agent` or re-wait; do not `spawn_agent` a successor against a still-live worker. Successor only if that session is no longer resumable. Operator-cancel (`stop_reason` cancelled): wait for the operator; do not auto-retry. Identical brief: refuse. Merge Summary/Findings into a coherent answer for the operator; do not paste raw fleet-agent dumps.",
"- Use manage_tasks for your own coordination checklist; spawning workers is `spawn_agent`, not manage_tasks.",
Expand Down Expand Up @@ -244,6 +244,8 @@ const TOOL_SUMMARIES: Record<string, string> = {
"start a worker agent and return immediately with agent_id; pass returned ids from search_agents as agent=...",
wait_agents:
"optional/deprecated on the primary parent — mailbox mail arrives as inbound; nested orchestrators still wait for spawned workers by agent_id; returns awaiting_director when a worker asks, without collecting that session",
list_agents:
"list this session's spawn_agent workers without blocking; after a parked ask_director is surfaced, returns an error until send_input answers or the ask is dropped — do not poll",
search_agents:
"find agent profiles by role or team before spawning with spawn_agent(agent=...); default results are id, description, and spawn metadata — pass include_body=true for the loaded system prompt / body",
manage_tasks:
Expand Down
Loading
Loading