Skip to content

fix(tui): name agents in the spawn tree and keep teammates past their turn - #956

Merged
ericleepi314 merged 1 commit into
mainfrom
fix/tui-subagent-names
Sep 25, 2026
Merged

ericleepi314 merged 1 commit into
mainfrom
fix/tui-subagent-names

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

Summary

This fixes two problems in the TUI's agents overlay (the "Spawn tree"). Both came up while running a math team of named, persistent teammates.

1. Rows didn't name the agent

Each row showed only the task description, so a team of named specialists looked like a list of anonymous tasks.

  • Cause: the backend sends name, description and subagent_type as separate fields. The TUI lost the name in three places:
    • gatewayClient used name only as a fallback for the goal (goal: description || name). The Agent tool always requires a description, so the name never survived.
    • SubagentProgress had no field for the name or the type.
    • The overlay rendered only goal.
  • Fix: the TUI now stores name and agentType.
    • Each row starts with the spawn's name, or else the agent type. The default types (general-purpose, worker, fork) stay hidden, following the reference's userFacingName.
    • The detail pane shows both.
    • Replayed trees keep the label.
  • Backend: agent.py's per-message progress now sends the resolved agent_def.agent_type. It used to send the raw tool input, which was None when the call didn't specify a type. The sync end-of-run frame now sends the spawn's own name; it used to put the type there.

2. Teammates vanished after the turn that spawned them

  • Cause: turnState.subagents is turn-scoped. startMessage() and idle() both clear it, and the idle layout depends on that. gatewayClient.seenSubagents is never cleared, though. So a teammate's later frames arrive as subagent.progress / subagent.complete, which only update existing rows and never create them. A persistent teammate therefore disappeared from the overlay while it kept working.
  • Fix: a session-scoped roster, $sessionAgents, that every subagent event updates.
    • The overlay adds agents that are still running but that the current turn no longer lists (withCarriedAgents).
    • Turn state and the idle layout are unchanged.
    • Finished entries are pruned at each turn start.
    • The roster survives /clear, /new and resume, because those keep the same agent-server process and its teammates keep running. It is cleared only when the backend process exits (forgetSessionAgents()).
  • Interrupted agents now end their row. gatewayClient published subagent.complete only for completed, failed and killed. An interrupted frame fell through, and the row stayed running forever; with the roster, it would also have stayed pinned in the overlay. The backend reports interrupted for a foreground subagent stopped with ESC or from the overlay (agent.py), a killed teammate (team_runtime.py), and an aborted workflow agent (workflow/runner.py). Any status other than running or queued now ends the run.

Before / after

The real built TUI was run in a PTY against a fake agent-server.

installed build (before) this branch
Rows ● Pin environment encode statement ● fl-formalizer · Pin environment encode statement
Idle after turn 1 frozen snapshot, stale counts the live teammate, fresh counts
During turn 2 teammate gone ● Explore · Find cutoff… and ● fl-formalizer · …

Test plan

  • vitest:
    • overlay rendering: identity, a carried teammate, no Ink error screen
    • the gateway passing name and type through
    • the gateway ending a run on any non-live status, including interrupted
    • the handler keeping identity across partial events
    • a full turn cycle: the teammate stays in the roster, while finished and interrupted agents are pruned; the roster survives fullReset() and is emptied by forgetSessionAgents()
    • the spawn-history disk read
    • unit tests for subagentIdentity, subagentTitle, subagentAgentLabel, withCarriedAgents and isSubagentAlive
  • pytest: the running emit sends the resolved type and the name, and an unnamed spawn stays unnamed when it ends.
  • Mutation-checked: removing each part of the fix makes its tests fail. That includes restoring the old completed/failed/killed gate and moving the roster clear back into fullReset().
  • Full vitest: the same failures as main (7 pre-existing, plus 1 timing-sensitive test under load). Typecheck is clean. ESLint shows only pre-existing warnings, none on changed lines.
  • Full pytest: 10,812 passed. The one local failure (test_system_prompt_full::…damped_wording…) also fails on main; it comes from the developer's memory text.
  • Critic review loop.

Follow-ups (not in this PR)

  • Overlay:
    • When idle, the live list shows only carried teammates; the last turn's finished rows stay one [ away in history.
    • Once a carried agent finishes, its final state isn't shown.
    • The roster isn't reconciled with delegation.status.
  • Transcript rows for Agent calls still read Agent(<description>), not the agent's name.
  • ui-web's labelOf still uses name only as a fallback.
  • Background agents (both behaviors predate this PR):
    • One resumed after it finished keeps its terminal status (keepTerminalElseRunning).
    • One killed from the overlay reports killed. normalizeSubagentStatus doesn't recognize that status and falls back to completed, so the row shows ✓ instead of interrupted.

🤖 Generated with Claude Code

… turn

The agents overlay listed each spawn only by its task description. A
persistent teammate also dropped out of the overlay once the turn that
spawned it ended, even though it kept working.

- gatewayClient and types: keep the spawn's `name` and `subagent_type`
  next to its description. Any status that isn't live, including
  `interrupted`, now ends the run.
- Overlay rows start with the name, or else an agent type that isn't
  one of the defaults. The detail pane shows both, and replayed trees
  keep them.
- A session-scoped roster ($sessionAgents) outlives the turn state. The
  overlay carries over running agents that the current turn no longer
  lists. Finished entries are pruned at turn start, and the roster is
  cleared only when the backend exits.
- agent.py: per-message progress sends the resolved agent type. The
  sync end-of-run frame sends the spawn's own name instead of its type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Test Results

     5 files   1 021 suites   20m 38s ⏱️
16 083 tests 16 061 ✅ 22 💤 0 ❌
32 136 runs  32 065 ✅ 71 💤 0 ❌

Results for commit c3fcdb0.

@ericleepi314
ericleepi314 merged commit ded163b into main Sep 25, 2026
8 checks passed
@ericleepi314
ericleepi314 deleted the fix/tui-subagent-names branch September 25, 2026 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant