Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 09a5570

Browse files
committed
feat: enrich subtask handoff with structured context summaries
Phase 1 of #12330 - improves context handoff visibility between parent and child tasks during delegation. Changes: - Add SubtaskSummary type to @roo-code/types for structured handoff data - Create buildSubtaskSummary utility that extracts files modified/read, commands executed, tool usage, and todo stats from task history - Modify AttemptCompletionTool to build structured summary on completion - Update reopenParentFromDelegation to format enriched API history text so the parent LLM gets better context about what the subtask did - Update ChatRow UI to render structured summaries with mode badge, file lists, command lists, and todo progress - Add i18n translation keys for new UI elements - Add 19 tests for buildSubtaskSummary and formatSubtaskSummaryForApi - Backward compatible: plain-text summaries still work as before
1 parent 22d845c commit 09a5570

7 files changed

Lines changed: 659 additions & 8 deletions

File tree

packages/types/src/history.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,34 @@ export const historyItemSchema = z.object({
2929
})
3030

3131
export type HistoryItem = z.infer<typeof historyItemSchema>
32+
33+
/**
34+
* SubtaskSummary
35+
*
36+
* Structured metadata produced when a subtask completes via attempt_completion
37+
* and hands off context back to its parent task. This enriches the handoff
38+
* with visibility into what the subtask actually did.
39+
*/
40+
export const subtaskSummarySchema = z.object({
41+
/** The completion result text from attempt_completion */
42+
result: z.string(),
43+
/** Mode slug the subtask ran in (e.g. "code", "architect") */
44+
mode: z.string().optional(),
45+
/** Files that were created or modified (write_to_file, apply_diff, insert_content) */
46+
filesModified: z.array(z.string()).optional(),
47+
/** Files that were read during the subtask */
48+
filesRead: z.array(z.string()).optional(),
49+
/** Shell commands that were executed */
50+
commandsExecuted: z.array(z.string()).optional(),
51+
/** Summary of tool usage counts: tool name -> number of attempts */
52+
toolUsageSummary: z.record(z.string(), z.number()).optional(),
53+
/** Todo list status at completion: [completed, total] */
54+
todoStats: z
55+
.object({
56+
completed: z.number(),
57+
total: z.number(),
58+
})
59+
.optional(),
60+
})
61+
62+
export type SubtaskSummary = z.infer<typeof subtaskSummarySchema>
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import { buildSubtaskSummary, formatSubtaskSummaryForApi, type SubtaskContext } from "../buildSubtaskSummary"
2+
3+
function createContext(overrides: Partial<SubtaskContext> = {}): SubtaskContext {
4+
return {
5+
apiConversationHistory: [],
6+
toolUsage: {},
7+
todoList: undefined,
8+
taskMode: "code",
9+
...overrides,
10+
}
11+
}
12+
13+
describe("buildSubtaskSummary", () => {
14+
it("should return a minimal summary with just result and mode", () => {
15+
const context = createContext()
16+
const summary = buildSubtaskSummary(context, "Task completed successfully")
17+
18+
expect(summary.result).toBe("Task completed successfully")
19+
expect(summary.mode).toBe("code")
20+
expect(summary.filesModified).toBeUndefined()
21+
expect(summary.filesRead).toBeUndefined()
22+
expect(summary.commandsExecuted).toBeUndefined()
23+
expect(summary.toolUsageSummary).toBeUndefined()
24+
expect(summary.todoStats).toBeUndefined()
25+
})
26+
27+
it("should extract files modified from write_to_file tool_use blocks", () => {
28+
const context = createContext({
29+
apiConversationHistory: [
30+
{
31+
role: "assistant",
32+
content: [
33+
{
34+
type: "tool_use",
35+
id: "toolu_1",
36+
name: "write_to_file",
37+
input: { path: "src/index.ts", content: "hello" },
38+
},
39+
],
40+
},
41+
{
42+
role: "user",
43+
content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "ok" }],
44+
},
45+
],
46+
})
47+
48+
const summary = buildSubtaskSummary(context, "Done")
49+
expect(summary.filesModified).toEqual(["src/index.ts"])
50+
})
51+
52+
it("should extract files modified from apply_diff tool_use blocks", () => {
53+
const context = createContext({
54+
apiConversationHistory: [
55+
{
56+
role: "assistant",
57+
content: [
58+
{
59+
type: "tool_use",
60+
id: "toolu_2",
61+
name: "apply_diff",
62+
input: { path: "src/utils.ts", diff: "--- a\n+++ b" },
63+
},
64+
],
65+
},
66+
],
67+
})
68+
69+
const summary = buildSubtaskSummary(context, "Done")
70+
expect(summary.filesModified).toEqual(["src/utils.ts"])
71+
})
72+
73+
it("should extract files read from read_file tool_use blocks", () => {
74+
const context = createContext({
75+
apiConversationHistory: [
76+
{
77+
role: "assistant",
78+
content: [
79+
{
80+
type: "tool_use",
81+
id: "toolu_3",
82+
name: "read_file",
83+
input: { path: "package.json" },
84+
},
85+
],
86+
},
87+
],
88+
})
89+
90+
const summary = buildSubtaskSummary(context, "Done")
91+
expect(summary.filesRead).toEqual(["package.json"])
92+
})
93+
94+
it("should extract commands from execute_command tool_use blocks", () => {
95+
const context = createContext({
96+
apiConversationHistory: [
97+
{
98+
role: "assistant",
99+
content: [
100+
{
101+
type: "tool_use",
102+
id: "toolu_4",
103+
name: "execute_command",
104+
input: { command: "npm test" },
105+
},
106+
],
107+
},
108+
],
109+
})
110+
111+
const summary = buildSubtaskSummary(context, "Done")
112+
expect(summary.commandsExecuted).toEqual(["npm test"])
113+
})
114+
115+
it("should truncate very long commands", () => {
116+
const longCmd = "a".repeat(200)
117+
const context = createContext({
118+
apiConversationHistory: [
119+
{
120+
role: "assistant",
121+
content: [
122+
{
123+
type: "tool_use",
124+
id: "toolu_5",
125+
name: "execute_command",
126+
input: { command: longCmd },
127+
},
128+
],
129+
},
130+
],
131+
})
132+
133+
const summary = buildSubtaskSummary(context, "Done")
134+
expect(summary.commandsExecuted![0].length).toBeLessThanOrEqual(120)
135+
expect(summary.commandsExecuted![0].endsWith("...")).toBe(true)
136+
})
137+
138+
it("should deduplicate modified files", () => {
139+
const context = createContext({
140+
apiConversationHistory: [
141+
{
142+
role: "assistant",
143+
content: [
144+
{
145+
type: "tool_use",
146+
id: "toolu_6",
147+
name: "write_to_file",
148+
input: { path: "src/index.ts", content: "v1" },
149+
},
150+
],
151+
},
152+
{ role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_6", content: "ok" }] },
153+
{
154+
role: "assistant",
155+
content: [
156+
{
157+
type: "tool_use",
158+
id: "toolu_7",
159+
name: "apply_diff",
160+
input: { path: "src/index.ts", diff: "diff" },
161+
},
162+
],
163+
},
164+
],
165+
})
166+
167+
const summary = buildSubtaskSummary(context, "Done")
168+
expect(summary.filesModified).toEqual(["src/index.ts"])
169+
})
170+
171+
it("should include tool usage summary from toolUsage", () => {
172+
const context = createContext({
173+
toolUsage: {
174+
write_to_file: { attempts: 3, failures: 0 },
175+
read_file: { attempts: 5, failures: 1 },
176+
} as any,
177+
})
178+
179+
const summary = buildSubtaskSummary(context, "Done")
180+
expect(summary.toolUsageSummary).toEqual({
181+
write_to_file: 3,
182+
read_file: 5,
183+
})
184+
})
185+
186+
it("should include todo stats when todoList is present", () => {
187+
const context = createContext({
188+
todoList: [
189+
{ id: "1", task: "Do A", status: "completed" },
190+
{ id: "2", task: "Do B", status: "completed" },
191+
{ id: "3", task: "Do C", status: "pending" },
192+
] as any,
193+
})
194+
195+
const summary = buildSubtaskSummary(context, "Done")
196+
expect(summary.todoStats).toEqual({ completed: 2, total: 3 })
197+
})
198+
199+
it("should skip user messages when scanning for tool_use blocks", () => {
200+
const context = createContext({
201+
apiConversationHistory: [
202+
{
203+
role: "user",
204+
content: [
205+
{
206+
type: "tool_result" as any,
207+
tool_use_id: "toolu_x",
208+
content: "ok",
209+
},
210+
],
211+
},
212+
],
213+
})
214+
215+
const summary = buildSubtaskSummary(context, "Done")
216+
expect(summary.filesModified).toBeUndefined()
217+
expect(summary.commandsExecuted).toBeUndefined()
218+
})
219+
220+
it("should handle empty conversation history", () => {
221+
const context = createContext({ apiConversationHistory: [] })
222+
const summary = buildSubtaskSummary(context, "Nothing happened")
223+
224+
expect(summary.result).toBe("Nothing happened")
225+
expect(summary.mode).toBe("code")
226+
})
227+
228+
it("should handle messages with non-array content (string content)", () => {
229+
const context = createContext({
230+
apiConversationHistory: [
231+
{
232+
role: "assistant",
233+
content: "Just text response",
234+
},
235+
],
236+
})
237+
238+
const summary = buildSubtaskSummary(context, "Done")
239+
expect(summary.filesModified).toBeUndefined()
240+
})
241+
})
242+
243+
describe("formatSubtaskSummaryForApi", () => {
244+
it("should format a minimal summary", () => {
245+
const text = formatSubtaskSummaryForApi({ result: "All done" })
246+
expect(text).toContain("## Result\nAll done")
247+
})
248+
249+
it("should include mode section", () => {
250+
const text = formatSubtaskSummaryForApi({ result: "Done", mode: "architect" })
251+
expect(text).toContain("## Mode\narchitect")
252+
})
253+
254+
it("should include files modified section", () => {
255+
const text = formatSubtaskSummaryForApi({
256+
result: "Done",
257+
filesModified: ["src/a.ts", "src/b.ts"],
258+
})
259+
expect(text).toContain("## Files Modified")
260+
expect(text).toContain("- src/a.ts")
261+
expect(text).toContain("- src/b.ts")
262+
})
263+
264+
it("should include files read section", () => {
265+
const text = formatSubtaskSummaryForApi({
266+
result: "Done",
267+
filesRead: ["package.json"],
268+
})
269+
expect(text).toContain("## Files Read")
270+
expect(text).toContain("- package.json")
271+
})
272+
273+
it("should include commands section", () => {
274+
const text = formatSubtaskSummaryForApi({
275+
result: "Done",
276+
commandsExecuted: ["npm test", "npm build"],
277+
})
278+
expect(text).toContain("## Commands Executed")
279+
expect(text).toContain("- `npm test`")
280+
expect(text).toContain("- `npm build`")
281+
})
282+
283+
it("should include todo stats", () => {
284+
const text = formatSubtaskSummaryForApi({
285+
result: "Done",
286+
todoStats: { completed: 3, total: 5 },
287+
})
288+
expect(text).toContain("## Todos\n3/5 completed")
289+
})
290+
291+
it("should format a comprehensive summary with all sections", () => {
292+
const text = formatSubtaskSummaryForApi({
293+
result: "Implemented the feature",
294+
mode: "code",
295+
filesModified: ["src/feature.ts"],
296+
filesRead: ["src/config.ts"],
297+
commandsExecuted: ["npm test"],
298+
todoStats: { completed: 2, total: 2 },
299+
})
300+
301+
expect(text).toContain("## Result")
302+
expect(text).toContain("## Mode")
303+
expect(text).toContain("## Files Modified")
304+
expect(text).toContain("## Files Read")
305+
expect(text).toContain("## Commands Executed")
306+
expect(text).toContain("## Todos")
307+
})
308+
})

0 commit comments

Comments
 (0)