Skip to content

Commit bc09637

Browse files
authored
feat: add named subagent agent roster (#54)
* chore: ignore local worktrees * docs: design named subagent activity roster * docs: revise subagent roster panel design * docs: plan named subagent agents panel * feat: configure subagent name pools * feat: name and track subagent lifecycles * fix: preserve reusable subagent lifecycles * feat: emit subagent lifecycle events * fix: preserve subagent lifecycle event integrity * feat: track agents in the TUI * fix: make agent cleanup terminal * feat: render the subagent agents panel * fix: address agents panel review * feat: forward nested subagent rosters * test: cover abrupt nested agent cleanup * docs: document the subagent agents panel * docs: clarify failed agent row grace * fix: retire stale subagent roster state * docs: report whole-branch review fixes * feat: simplify subagent naming * docs: align roster design with petnames * feat: use curated subagent name catalog * feat: render subagents as an agent tree * docs: describe curated names and agent tree * chore: bump version to 0.1.109 * docs: align agents plan version * feat: generate subagent first names with fake * docs: describe faker subagent names * docs: record faker naming implementation * docs: align naming test strategy * fix: restore current-main tui graph behavior * docs: align agents panel with current-main layout * docs: report graph parity simplifications * chore: remove agents development artifacts * feat: prefer AI-selected subagent names * fix: align agents roster naming * rename to agent roster * test: fix agent roster title width
1 parent 4bad70e commit bc09637

15 files changed

Lines changed: 3695 additions & 266 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/dist/
33
/.pi-subagents/
44
/.kit/
5+
/worktrees/
56
/.kit-codesign-identity
67
/.env
78
.DS_Store

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "kit"
3-
version = "0.1.108"
3+
version = "0.1.111"
44
edition = "2024"
55
rust-version = "1.94.0"
66
publish = false

docs/user/subagents-and-acp-harnesses.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,37 @@ Kit can start parent-owned nested agents through the Agent Client Protocol (ACP)
77
Use the object form of the hidden tools inside `compose`:
88

99
```text
10-
first = subagent({ prompt: "Inspect the parser and identify the smallest risk." })
10+
first = subagent({
11+
name: "Implementer",
12+
prompt: "Inspect the parser and identify the smallest risk."
13+
})
1114
second = prompt({
1215
subagent: first,
1316
prompt: "Now propose a minimal fix."
1417
})
1518
branch = fork({
1619
subagent: second,
20+
name: "Alternative Reviewer",
1721
prompt: "Explore an alternative without changing the original session."
1822
})
1923
return { main: second.output, alternative: branch.output }
2024
```
2125

22-
Each successful turn returns a session value with `id`, `output`, and `generation`. `subagent` creates an ID at generation 1. `prompt` keeps that ID and increments its generation. `fork` creates a different ID whose generation is one greater than the supplied source value; it does not advance the source session. `subagents({})` returns active sessions in ID order. A session whose initial prompt is still in progress appears as `{ id, status: "starting" }`; it can be closed by ID but cannot be prompted or forked until the initial `subagent` call returns its complete handle. Completed sessions retained for reuse appear as their latest values. Close a session with either `close(value)` or `close({ id: value.id })`; the latter is useful when only an ID is available. Closing an unknown ID fails with `unknown subagent session`. Kit sends ACP `session/close` when the harness advertises it. A standalone process without that capability is terminated when its handle is dropped. If native-fork siblings share a process and the harness cannot close one logical session, `close` fails rather than claiming success or disrupting the siblings.
26+
Each successful turn returns a session value with `id`, `name`, `output`, and `generation`. `subagent` creates an ID at generation 1. `prompt` keeps that ID and name while incrementing its generation. `fork` creates a different ID and uses its own preferred or fallback name; its generation is one greater than the supplied source value, and it does not advance the source session. Close a session with either `close(value)` or `close({ id: value.id })`; the latter is useful when only an ID is available. Closing an unknown ID fails with `unknown subagent session`. Kit sends ACP `session/close` when the harness advertises it. A standalone process without that capability is terminated when its handle is dropped. If native-fork siblings share a process and the harness cannot close one logical session, `close` fails rather than claiming success or disrupting the siblings.
2327

2428
Always pass the latest completed value back to `prompt` or `fork`. Reusing an older value fails with `stale subagent generation N; current generation is M`. This prevents two continuations from silently racing on one session. Calls on an individual ACP session are serialized, while separate forked sessions can be prompted concurrently.
2529

26-
The optional `harness` and `model` arguments belong only on `subagent`. `harness` overrides the user's configured harness preference. `model` selects an exact model value ID advertised by that harness through its ACP session configuration, or a model alias configured for that harness. Omit either argument to retain the configured preference. `prompt` and `fork` retain the original session's harness and model. An explicit model fails before the first prompt if the harness does not advertise a selectable `model` option or rejects the value.
30+
The optional `name` argument is preferred on `subagent` and `fork`; `prompt` has no naming input and preserves the session name. The optional `harness` and `model` arguments belong only on `subagent`. `harness` overrides the user's configured harness preference. `model` selects an exact model value ID advertised by that harness through its ACP session configuration, or a model alias configured for that harness. Omit either argument to retain the configured preference. `prompt` and `fork` retain the original session's harness and model. An explicit model fails before the first prompt if the harness does not advertise a selectable `model` option or rejects the value.
31+
32+
## Inspect display names
33+
34+
The calling model should give each new `subagent` and `fork` a concise role-oriented display name based on its task, such as `Round 2 Implementer` or `Reviewer`. This uses the model already making the tool call; Kit does not make a separate naming request. The name is display metadata and never changes the prompt sent to the child. Omitting `name`, or supplying an invalid name, uses the lowest available `Agent N` label.
35+
36+
Kit trims preferred names and accepts 1–32 bytes of printable ASCII. Names compare case-insensitively among one parent's direct live children; clashes receive the lowest available numeric suffix, with the base shortened as needed to stay within 32 bytes. The immutable `s-…` ID remains authoritative; names never select handles.
37+
38+
A name is reserved when creation starts. A failed creation releases it; otherwise the reservation survives starting, working, idle, and reusable failures until `close` or terminal retirement. Nested Kit processes allocate independently, so separate descendant branches can generate duplicate visible names in the agent roster even though each parent keeps its direct-child names unique.
39+
40+
Use `subagents({})` to inspect live direct children. Each row contains `id`, `name`, `status`, `generation`, and the bounded current task summary. Closed and terminally retired children are omitted.
2741

2842
## Require structured JSON output with `output_schema`
2943

docs/user/tui-and-sessions.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ A session ID must be 1–128 ASCII letters, digits, `-`, or `_`. `kit prompt` us
3737
| `Up` / `Down` | Move through prompt lines, then prompt history |
3838
| `Shift+Up` / `Shift+Down`, `PageUp` / `PageDown`, mouse wheel | Scroll the transcript |
3939
| `Ctrl+Home` / `Ctrl+End` | Jump to transcript top / bottom |
40-
| `Ctrl+L` / `Ctrl+T` | Toggle the agent log / reasoning |
40+
| `Ctrl+R` / `Ctrl+L` / `Ctrl+T` | Toggle the agent roster / agent log / reasoning |
4141
| `Ctrl+K` | Kill the selected running background tool call; otherwise delete to the end of the editor line |
4242
| `Ctrl+O`, or click a tool card | Fold or unfold raw tool output |
4343
| `Ctrl+Y` | Copy the latest agent response as original Markdown |
@@ -73,9 +73,19 @@ Press `Command+B` to detach the newest running foreground top-level compose call
7373

7474
At an idle, non-empty editor, `Ctrl+C` clears the prompt instead of unexpectedly discarding it and quitting in one step; press it again with the empty editor to quit.
7575

76+
## Monitor subagents in the agent roster
77+
78+
Press `Ctrl+R` or enter the exact local command `/agents` while idle to toggle the agent roster. The roster keeps its own visibility and scroll position. The roster covers every subagent observable to the current top-level Kit process tree, whether its call is foreground or background and regardless of the focused transcript block or tool call. Direct children are tree roots, and nested Kit descendants appear immediately beneath their parent at arbitrary depth in an always-expanded tree. Siblings retain lifecycle/creation/ID ordering within each parent, so an active child remains grouped beneath an idle parent instead of moving across subtrees. A descendant whose parent event has not arrived temporarily appears as a root with `Name · via Parent` and automatically reparents when the parent arrives. Generic ACP has no portable child-session enumeration, so agents created privately inside a generic harness cannot appear unless the harness forwards compatible Kit runtime events.
79+
80+
Each row uses two lines, with tree connectors and indentation continuing across both. The first contains a glyph and display name; the second contains the bounded task summary and a duration right-aligned to the panel's inner edge. When starting or forking a subagent, the parent model preferably supplies a concise role-oriented name such as `Round 2 Implementer` or `Reviewer`; omitted or invalid names fall back to `Agent N`, and case-insensitive sibling collisions receive a numeric suffix. Task text truncates before the reserved duration column. The glyph palette is yellow `Pulse::Child` for `starting`, cyan `Pulse::Tool` for `working`, and dim `○` for ordinary or successful `idle`. A failed reusable idle row shows a red `✗` for four seconds after its failure timestamp, then returns to dim `○`; a failed terminal or removed tombstone shows the red `✗` for four seconds, then its row is deleted. Active durations update on animation ticks, freeze when the generation becomes idle or fails, and restart for a later prompt.
81+
82+
A fixed footer remains visible while rows scroll, for example `3 agents · 2 working · 1 idle`; it includes the total and only nonzero `starting`, `working`, and `idle` buckets. Foreground and background are not separate buckets. Footer accounting remains lifecycle-based during the four-second grace: reusable failures count as idle immediately, while closed and terminally retired handles leave the live total immediately even while a tombstone remains visible. Idle rows remain until their handles are closed.
83+
84+
When the agent roster is visible, terminals at least 108 columns wide show the transcript beside a fixed 46-column panel. Terminals at most 107 columns wide stack the transcript and roster with a 55/45 height split. Hiding the roster restores the transcript to the full main area.
85+
7686
## Manage sessions and compact from the TUI
7787

78-
The TUI handles `/new`, `/resume`, `/sessions`, `/close`, `/model`, and `/effort` as exact local slash-command tokens. It also discovers agent commands through ACP and highlights them without interpreting them locally:
88+
The TUI handles `/new`, `/resume`, `/sessions`, `/close`, `/model`, `/effort`, and `/agents` as exact local slash-command tokens. It also discovers agent commands through ACP and highlights them without interpreting them locally:
7989

8090
```text
8191
/new
@@ -88,9 +98,10 @@ The TUI handles `/new`, `/resume`, `/sessions`, `/close`, `/model`, and `/effort
8898
/model
8999
/effort
90100
/effort high
101+
/agents
91102
```
92103

93-
These local commands are available only while the session is idle. `/new` closes the current session and starts a fresh persisted session. It clears the visible transcript but does not delete or alter the previous session, which remains resumable by its ID. Text following `/new` becomes the new session's first prompt. `/resume <session-id>` closes the current session, resumes the requested durable session, and replays its transcript; selecting the already-active ID is a no-op. `/sessions` opens a visible newest-first selector for the same workspace; Up and Down move, Enter uses the existing resume flow, and Esc closes the dialog. `/close` closes the current session and exits the TUI.
104+
These local commands are available only while the session is idle. `/agents` toggles the agent roster without starting a model turn. `/new` closes the current session and starts a fresh persisted session. It clears the visible transcript but does not delete or alter the previous session, which remains resumable by its ID. Text following `/new` becomes the new session's first prompt. `/resume <session-id>` closes the current session, resumes the requested durable session, and replays its transcript; selecting the already-active ID is a no-op. `/sessions` opens a visible newest-first selector for the same workspace; Up and Down move, Enter uses the existing resume flow, and Esc closes the dialog. `/close` closes the current session and exits the TUI.
94105

95106
`/model` opens the model selector. `/effort` opens the advertised ACP reasoning-effort selector; `/effort default|low|medium|high` selects directly. In either dialog, Tab toggles saving the selection to `~/.kit/config.toml`, Enter selects, and Esc closes. Saving `default` removes top-level `reasoning_effort`; other values update it without replacing unrelated TOML. A new or resumed process starts from the resolved CLI/TOML default unless the selection was saved.
96107

fixtures/mock-acp.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
22
import json
3+
import os
34
import sys
45
import threading
56
import time
@@ -11,6 +12,9 @@
1112
selected_models = {}
1213
model_ids = ["mock/default", "mock/requested"]
1314

15+
if "--fail-start" in sys.argv:
16+
sys.exit(2)
17+
1418

1519
def send(message):
1620
with write_lock:
@@ -31,6 +35,9 @@ def prompt(request):
3135
text = selected_models.get(session_id, model_ids[0])
3236
if "MOCK_STRUCTURED_OUTPUT" in text:
3337
text = json.dumps({"approved": True, "reason": "mock approved"})
38+
if "MOCK_REFUSAL" in text:
39+
respond(request["id"], {"stopReason": "refusal"})
40+
return
3441
if "MOCK_RICH_OUTPUT" in text:
3542
updates = [
3643
{
@@ -82,6 +89,9 @@ def prompt(request):
8289
},
8390
})
8491
respond(request["id"], {"stopReason": "end_turn"})
92+
if "--exit-after-prompt" in sys.argv:
93+
time.sleep(0.05)
94+
os._exit(0)
8595

8696

8797
for line in sys.stdin:
@@ -112,6 +122,13 @@ def prompt(request):
112122
}]
113123
respond(request["id"], result)
114124
elif method == "session/fork":
125+
if "--fail-fork" in sys.argv:
126+
send({
127+
"jsonrpc": "2.0",
128+
"id": request["id"],
129+
"error": {"code": -32000, "message": "fork failed"},
130+
})
131+
continue
115132
if not supports_fork:
116133
send({
117134
"jsonrpc": "2.0",
@@ -141,4 +158,6 @@ def prompt(request):
141158
elif method == "session/cancel":
142159
pass
143160
elif method == "session/close":
161+
if "--slow-close" in sys.argv:
162+
time.sleep(0.40)
144163
respond(request["id"], {})

0 commit comments

Comments
 (0)