Skip to content

Commit cb6cb49

Browse files
FulAppiOSclaude
andcommitted
feat(server): richer subagent labels from sibling meta.json
The CLI writes agent-<id>.meta.json next to every subagent transcript. Classic Task subagents carry description + agentType; workflow agents carry agentType only (often the generic 'workflow-subagent'). resolveSubagentLabel now resolves in tiers: meta description, parent Agent tool_use match, meta agentType + prompt first sentence, prompt first sentence. On real data this turns eight identical truncated 'Repo root: /Users/...' workflow heroes into 'Explore: <task>' labels and gives classic subagents their exact Agent-tool description without scanning the parent transcript. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 369c4d5 commit cb6cb49

2 files changed

Lines changed: 105 additions & 12 deletions

File tree

server/src/parsers/subagent-label.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,60 @@ describe('resolveSubagentLabel', () => {
153153
expect(label).toBe('Audit Y');
154154
});
155155

156+
test('prefers description from the sibling meta.json over everything', async () => {
157+
await writeFile(subagentPath, jsonline({
158+
type: 'user',
159+
message: { role: 'user', content: 'Run a thorough audit of X' },
160+
}));
161+
await writeFile(subagentPath.replace('.jsonl', '.meta.json'), JSON.stringify({
162+
agentType: 'Explore',
163+
description: 'Verify Claude Code lifecycle hooks',
164+
toolUseId: 'toolu_123',
165+
}));
166+
await writeFile(parentPath, jsonline({
167+
type: 'assistant',
168+
message: {
169+
role: 'assistant',
170+
content: [{
171+
type: 'tool_use',
172+
name: 'Agent',
173+
input: { description: 'Audit X for regressions', prompt: 'Run a thorough audit of X' },
174+
}],
175+
},
176+
}));
177+
178+
const label = await resolveSubagentLabel(subagentPath);
179+
expect(label).toBe('Verify Claude Code lifecycle hooks');
180+
});
181+
182+
test('combines meta agentType with the prompt first sentence for workflow agents', async () => {
183+
const workflowDir = join(projectDir, parentSessionId, 'subagents', 'workflows', 'wf_0f3513d0-3cc');
184+
await mkdir(workflowDir, { recursive: true });
185+
const workflowAgentPath = join(workflowDir, 'agent-a2282a1d2c6a1932f.jsonl');
186+
await writeFile(workflowAgentPath, jsonline({
187+
type: 'user',
188+
message: { role: 'user', content: 'Esplora il backend Laravel. Rispondi con snippet.' },
189+
}));
190+
await writeFile(workflowAgentPath.replace('.jsonl', '.meta.json'), JSON.stringify({ agentType: 'Explore' }));
191+
192+
const label = await resolveSubagentLabel(workflowAgentPath);
193+
expect(label).toBe('Explore: Esplora il backend Laravel');
194+
});
195+
196+
test('drops the generic workflow-subagent agentType from meta.json', async () => {
197+
const workflowDir = join(projectDir, parentSessionId, 'subagents', 'workflows', 'wf_2ca7ddd8-324');
198+
await mkdir(workflowDir, { recursive: true });
199+
const workflowAgentPath = join(workflowDir, 'agent-afa8678eb14a03831.jsonl');
200+
await writeFile(workflowAgentPath, jsonline({
201+
type: 'user',
202+
message: { role: 'user', content: 'Write the plan. Then stop.' },
203+
}));
204+
await writeFile(workflowAgentPath.replace('.jsonl', '.meta.json'), JSON.stringify({ agentType: 'workflow-subagent' }));
205+
206+
const label = await resolveSubagentLabel(workflowAgentPath);
207+
expect(label).toBe('Write the plan');
208+
});
209+
156210
test('ignores parent Agent invocations with non-matching prompt', async () => {
157211
await writeFile(subagentPath, jsonline({
158212
type: 'user',

server/src/parsers/subagent-label.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,34 @@ function firstSentenceFallback(prompt: string): string | undefined {
3434
return cleaned.length > 0 ? truncate(cleaned) : undefined;
3535
}
3636

37+
interface SubagentMeta {
38+
description?: string;
39+
agentType?: string;
40+
}
41+
42+
/**
43+
* Read the sibling `agent-*.meta.json` the CLI writes next to each subagent
44+
* transcript. Classic Task subagents carry `description` + `agentType`;
45+
* workflow agents usually only `agentType` (the generic `workflow-subagent`
46+
* value carries no information and is dropped).
47+
*/
48+
async function readSubagentMeta(filePath: string): Promise<SubagentMeta> {
49+
const metaFile = Bun.file(filePath.replace(/\.jsonl$/, '.meta.json'));
50+
try {
51+
if (!(await metaFile.exists())) return {};
52+
const meta = (await metaFile.json()) as { description?: unknown; agentType?: unknown };
53+
const description = typeof meta.description === 'string' && meta.description.length > 0
54+
? meta.description
55+
: undefined;
56+
const agentType = typeof meta.agentType === 'string' && meta.agentType.length > 0 && meta.agentType !== 'workflow-subagent'
57+
? meta.agentType
58+
: undefined;
59+
return { description, agentType };
60+
} catch {
61+
return {};
62+
}
63+
}
64+
3765
function readFirstLineContent(text: string): string | undefined {
3866
const [firstLine] = text.split('\n', 1);
3967
if (firstLine === undefined || firstLine.trim() === '') return undefined;
@@ -74,11 +102,14 @@ function matchAgentLabel(parentText: string, targetPrompt: string): string | und
74102
/**
75103
* Given a subagent JSONL path (`.../<parentSessionId>/subagents/agent-*.jsonl`,
76104
* or the ultra-mode variant `.../<parentSessionId>/subagents/workflows/wf_<runId>/agent-*.jsonl`),
77-
* return a human-readable label by correlating the child's first user prompt
78-
* against the parent's `Agent` tool_use invocations. Falls back to the first
79-
* sentence of the subagent prompt when the parent can't be found or doesn't
80-
* carry an exact prompt match — always the case for workflow agents, whose
81-
* prompts live in the Workflow script rather than in the parent transcript.
105+
* return a human-readable label. Tiers, best first:
106+
*
107+
* 1. `description` from the sibling `agent-*.meta.json`
108+
* 2. the parent's matching `Agent` tool_use (description / subagent_type)
109+
* 3. `agentType` from meta.json combined with the prompt's first sentence —
110+
* the only informative pair for workflow agents, whose prompts live in
111+
* the Workflow script rather than in the parent transcript
112+
* 4. the first sentence of the subagent prompt
82113
*
83114
* Returns `undefined` for non-subagent paths or when nothing usable is available.
84115
*/
@@ -90,6 +121,9 @@ export async function resolveSubagentLabel(filePath: string): Promise<string | u
90121
}
91122
if (basename(subagentsDir) !== 'subagents') return undefined;
92123

124+
const meta = await readSubagentMeta(filePath);
125+
if (meta.description !== undefined) return truncate(meta.description);
126+
93127
const parentSessionDir = dirname(subagentsDir);
94128
const projectDir = dirname(parentSessionDir);
95129
const parentSessionId = basename(parentSessionDir);
@@ -99,14 +133,19 @@ export async function resolveSubagentLabel(filePath: string): Promise<string | u
99133
if (!(await childFile.exists())) return undefined;
100134
const childText = await childFile.text();
101135
const prompt = readFirstLineContent(childText);
102-
if (prompt === undefined) return undefined;
103136

104-
const parentFile = Bun.file(parentPath);
105-
if (await parentFile.exists()) {
106-
const parentText = await parentFile.text();
107-
const matched = matchAgentLabel(parentText, prompt);
108-
if (matched !== undefined) return matched;
137+
if (prompt !== undefined) {
138+
const parentFile = Bun.file(parentPath);
139+
if (await parentFile.exists()) {
140+
const parentText = await parentFile.text();
141+
const matched = matchAgentLabel(parentText, prompt);
142+
if (matched !== undefined) return matched;
143+
}
109144
}
110145

111-
return firstSentenceFallback(prompt);
146+
const sentence = prompt !== undefined ? firstSentenceFallback(prompt) : undefined;
147+
if (meta.agentType !== undefined) {
148+
return truncate(sentence !== undefined ? `${meta.agentType}: ${sentence}` : meta.agentType);
149+
}
150+
return sentence;
112151
}

0 commit comments

Comments
 (0)