217 lines
8.3 KiB
Markdown
217 lines
8.3 KiB
Markdown
# Subagent Live Progress Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
|
|
|
|
**Goal:** Make running subagents show assistant intent text or humanized tool activity instead of raw `tool_call` / `tool_result` labels.
|
|
|
|
**Architecture:** Add one shared event-to-status formatter in `src/` so parent live updates and wrapper transcript lines use the same wording. Keep wrapper protocol, runner behavior, artifact layout, and `result.json` unchanged; only change formatting and the tiny call sites that consume it.
|
|
|
|
**Tech Stack:** TypeScript, Node ESM `.mjs`, Node test runner via `tsx --test`
|
|
|
|
---
|
|
|
|
## File structure
|
|
|
|
- Create: `src/progress.mjs` — shared, stateful formatter for normalized child events
|
|
- Modify: `src/tool.ts` — use formatter for parent `onUpdate` progress messages
|
|
- Modify: `src/wrapper/render.mjs` — use same formatter for transcript lines
|
|
- Modify: `src/wrapper/cli.mjs` — skip transcript/log output when formatter returns no line for blank assistant text
|
|
- Modify: `src/tool.test.ts` — regressions for parent live progress
|
|
- Modify: `src/wrapper/render.test.ts` — regressions for transcript rendering
|
|
|
|
### Task 1: Shared formatter + transcript rendering
|
|
|
|
**Files:**
|
|
- Create: `src/progress.mjs`
|
|
- Modify: `src/wrapper/render.mjs`
|
|
- Modify: `src/wrapper/cli.mjs`
|
|
- Test: `src/wrapper/render.test.ts`
|
|
|
|
- [x] **Step 1: Write the failing transcript-render tests**
|
|
|
|
Add tests to `src/wrapper/render.test.ts` for three behaviors:
|
|
|
|
```ts
|
|
import { createEventLineRenderer, renderHeader, renderEventLine } from "./render.mjs";
|
|
|
|
test("renderEventLine humanizes read and result lines", () => {
|
|
const render = createEventLineRenderer();
|
|
assert.equal(render({ type: "tool_call", toolName: "read", args: { path: "src/auth.ts" } }), "Reading src/auth.ts");
|
|
assert.equal(render({ type: "tool_result", toolName: "read", isError: false }), "Finished reading src/auth.ts");
|
|
});
|
|
|
|
test("renderEventLine prefers assistant text and drops blank assistant text", () => {
|
|
const render = createEventLineRenderer();
|
|
assert.equal(render({ type: "assistant_text", text: "Inspecting auth flow" }), "Inspecting auth flow");
|
|
assert.equal(render({ type: "assistant_text", text: " " }), null);
|
|
});
|
|
|
|
test("renderEventLine keeps bash commands concise and humanizes grep failures", () => {
|
|
const render = createEventLineRenderer();
|
|
assert.equal(render({ type: "tool_call", toolName: "bash", args: { command: "rg -n authentication src" } }), "$ rg -n authentication src");
|
|
assert.equal(render({ type: "tool_call", toolName: "grep", args: { pattern: "auth" } }), "Searching code for auth");
|
|
assert.equal(render({ type: "tool_result", toolName: "grep", isError: true }), "Search failed");
|
|
});
|
|
```
|
|
|
|
- [x] **Step 2: Run transcript-render tests to verify they fail**
|
|
|
|
Run: `cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/wrapper/render.test.ts`
|
|
Expected: FAIL because `createEventLineRenderer` does not exist and current renderer emits JSON-like tool lines / `(no assistant text)`.
|
|
|
|
- [x] **Step 3: Write the minimal shared formatter and transcript integration**
|
|
|
|
Implement `src/progress.mjs` with a stateful formatter API and wire `src/wrapper/render.mjs` to use it:
|
|
|
|
```js
|
|
export function createProgressFormatter() {
|
|
let lastTool = null;
|
|
|
|
return {
|
|
format(event) {
|
|
if (event.type === "assistant_text") {
|
|
const text = typeof event.text === "string" ? event.text.trim() : "";
|
|
return text || null;
|
|
}
|
|
if (event.type === "tool_call") {
|
|
lastTool = { toolName: event.toolName, args: event.args ?? {} };
|
|
return formatToolCall(lastTool.toolName, lastTool.args);
|
|
}
|
|
if (event.type === "tool_result") {
|
|
return formatToolResult(event, lastTool);
|
|
}
|
|
return JSON.stringify(event);
|
|
},
|
|
};
|
|
}
|
|
```
|
|
|
|
In `src/wrapper/render.mjs`, export both:
|
|
|
|
```js
|
|
export function createEventLineRenderer() {
|
|
const formatter = createProgressFormatter();
|
|
return (event) => formatter.format(event);
|
|
}
|
|
|
|
const defaultRenderEventLine = createEventLineRenderer();
|
|
export function renderEventLine(event) {
|
|
return defaultRenderEventLine(event);
|
|
}
|
|
```
|
|
|
|
In `src/wrapper/cli.mjs`, only append/log transcript lines when the rendered value is not `null`.
|
|
|
|
- [x] **Step 4: Run transcript-render tests to verify they pass**
|
|
|
|
Run: `cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/wrapper/render.test.ts`
|
|
Expected: PASS.
|
|
|
|
### Task 2: Parent live progress updates
|
|
|
|
**Files:**
|
|
- Modify: `src/tool.ts`
|
|
- Test: `src/tool.test.ts`
|
|
|
|
- [x] **Step 1: Write the failing parent-progress tests**
|
|
|
|
Add tests to `src/tool.test.ts` for assistant-text priority and humanized fallback:
|
|
|
|
```ts
|
|
test("single-mode subagent prefers assistant text for live progress", async () => {
|
|
const updates: string[] = [];
|
|
const tool = createSubagentTool({
|
|
runSingleTask: async ({ onEvent, meta }: any) => {
|
|
onEvent?.({ type: "assistant_text", text: "Inspecting auth flow" });
|
|
onEvent?.({ type: "tool_call", toolName: "read", args: { path: "src/auth.ts" } });
|
|
onEvent?.({ type: "tool_result", toolName: "read", isError: false });
|
|
return { runId: "run-1", task: meta.task, exitCode: 0, finalText: "done" };
|
|
},
|
|
} as any);
|
|
|
|
await tool.execute(/* existing ctx setup */, (partial: any) => {
|
|
const first = partial.content?.[0];
|
|
if (first?.type === "text") updates.push(first.text);
|
|
}, /* existing ctx */);
|
|
|
|
assert.deepEqual(updates.slice(0, 3), [
|
|
"Inspecting auth flow",
|
|
"Reading src/auth.ts",
|
|
"Finished reading src/auth.ts",
|
|
]);
|
|
assert.doesNotMatch(updates.join("\n"), /tool_call|tool_result/);
|
|
});
|
|
|
|
test("single-mode subagent ignores blank assistant text and falls back to tool text", async () => {
|
|
const updates: string[] = [];
|
|
const tool = createSubagentTool({
|
|
runSingleTask: async ({ onEvent, meta }: any) => {
|
|
onEvent?.({ type: "assistant_text", text: " " });
|
|
onEvent?.({ type: "tool_call", toolName: "grep", args: { pattern: "auth" } });
|
|
return { runId: "run-1", task: meta.task, exitCode: 0, finalText: "done" };
|
|
},
|
|
} as any);
|
|
|
|
await tool.execute(/* existing ctx setup */, (partial: any) => {
|
|
const first = partial.content?.[0];
|
|
if (first?.type === "text") updates.push(first.text);
|
|
}, /* existing ctx */);
|
|
|
|
assert.deepEqual(updates, ["Searching code for auth"]);
|
|
});
|
|
```
|
|
|
|
- [x] **Step 2: Run parent-progress tests to verify they fail**
|
|
|
|
Run: `cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/tool.test.ts`
|
|
Expected: FAIL because `src/tool.ts` still emits `Running subagent: <event.type>`.
|
|
|
|
- [x] **Step 3: Write the minimal parent integration**
|
|
|
|
Update `src/tool.ts` to create one formatter per child run and emit only formatted lines:
|
|
|
|
```ts
|
|
import { createProgressFormatter } from "./progress.mjs";
|
|
|
|
const formatter = createProgressFormatter();
|
|
|
|
onEvent(event) {
|
|
const text = formatter.format(event);
|
|
if (!text) return;
|
|
onUpdate?.({
|
|
content: [{ type: "text", text }],
|
|
details: makeDetails(input.mode, []),
|
|
});
|
|
}
|
|
```
|
|
|
|
Keep parallel/chain completion summaries and all result handling unchanged.
|
|
|
|
- [x] **Step 4: Run parent-progress tests to verify they pass**
|
|
|
|
Run: `cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/tool.test.ts`
|
|
Expected: PASS.
|
|
|
|
### Task 3: Verification
|
|
|
|
**Files:**
|
|
- Verify only
|
|
|
|
- [x] **Step 1: Run touched regression tests together**
|
|
|
|
Run: `cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/tool.test.ts src/wrapper/render.test.ts src/wrapper/cli.test.ts`
|
|
Expected: PASS.
|
|
|
|
- [x] **Step 2: Run full package test suite**
|
|
|
|
Run: `cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npm test`
|
|
Expected: PASS. If it fails, stop and investigate before claiming completion.
|
|
|
|
- [ ] **Step 3: Commit implementation**
|
|
|
|
```bash
|
|
cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress
|
|
git add src/progress.mjs src/tool.ts src/tool.test.ts src/wrapper/render.mjs src/wrapper/render.test.ts src/wrapper/cli.mjs docs/superpowers/plans/2026-04-12-subagent-live-progress.md
|
|
git commit -m "feat(progress): humanize subagent live updates"
|
|
```
|