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

Commit f087525

Browse files
committed
feat: structured context handoff between parent and child tasks (Phase 3c)
1 parent c0a84c0 commit f087525

11 files changed

Lines changed: 620 additions & 7 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { contextHandoffSummarySchema } from "../context-handoff.js"
2+
3+
describe("ContextHandoffSummary schema", () => {
4+
it("validates a complete summary", () => {
5+
const summary = {
6+
mode: "code",
7+
filesModified: ["src/app.ts", "src/utils.ts"],
8+
filesRead: ["src/config.ts"],
9+
commandsExecuted: ["npm test"],
10+
toolUsageCounts: { write_to_file: 2, read_file: 1 },
11+
apiRequestCount: 5,
12+
result: "Task completed successfully",
13+
}
14+
const result = contextHandoffSummarySchema.safeParse(summary)
15+
expect(result.success).toBe(true)
16+
if (result.success) {
17+
expect(result.data.filesModified).toEqual(["src/app.ts", "src/utils.ts"])
18+
expect(result.data.mode).toBe("code")
19+
}
20+
})
21+
22+
it("accepts minimal summary with only result", () => {
23+
const summary = { result: "Done" }
24+
const result = contextHandoffSummarySchema.safeParse(summary)
25+
expect(result.success).toBe(true)
26+
if (result.success) {
27+
expect(result.data.filesModified).toEqual([])
28+
expect(result.data.filesRead).toEqual([])
29+
expect(result.data.commandsExecuted).toEqual([])
30+
expect(result.data.toolUsageCounts).toEqual({})
31+
expect(result.data.apiRequestCount).toBe(0)
32+
}
33+
})
34+
35+
it("rejects summary without result", () => {
36+
const summary = { mode: "code", filesModified: [] }
37+
const result = contextHandoffSummarySchema.safeParse(summary)
38+
expect(result.success).toBe(false)
39+
})
40+
41+
it("applies defaults for optional array fields", () => {
42+
const summary = { result: "Done", mode: "debug" }
43+
const result = contextHandoffSummarySchema.safeParse(summary)
44+
expect(result.success).toBe(true)
45+
if (result.success) {
46+
expect(result.data.mode).toBe("debug")
47+
expect(result.data.filesModified).toEqual([])
48+
expect(result.data.commandsExecuted).toEqual([])
49+
}
50+
})
51+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { DEFAULT_MODES } from "../mode.js"
2+
3+
describe("Orchestrator context handoff prompt", () => {
4+
const orchestratorMode = DEFAULT_MODES.find((m: { slug: string }) => m.slug === "orchestrator")
5+
6+
it("should have an orchestrator mode", () => {
7+
expect(orchestratorMode).toBeDefined()
8+
})
9+
10+
it("should include context handoff guidance in customInstructions", () => {
11+
expect(orchestratorMode!.customInstructions).toContain("structured context handoff summary")
12+
})
13+
14+
it("should mention files modified in context handoff guidance", () => {
15+
expect(orchestratorMode!.customInstructions).toContain("files modified")
16+
})
17+
18+
it("should mention passing context to subsequent subtasks", () => {
19+
expect(orchestratorMode!.customInstructions).toContain("subsequent subtasks")
20+
})
21+
22+
it("should mention identifying potential conflicts", () => {
23+
expect(orchestratorMode!.customInstructions).toContain("potential conflicts")
24+
})
25+
})
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { z } from "zod"
2+
3+
/**
4+
* ContextHandoffSummary
5+
*
6+
* Structured summary of what a subtask accomplished during execution.
7+
* Automatically collected when a subtask completes via attempt_completion
8+
* and passed back to the parent task alongside the freeform result string.
9+
*
10+
* This gives the parent (typically the Orchestrator) structured visibility
11+
* into the child's work without requiring the child to manually enumerate
12+
* every file it touched or command it ran.
13+
*/
14+
export const contextHandoffSummarySchema = z.object({
15+
/** Mode the subtask ran in (e.g., "code", "debug", "architect") */
16+
mode: z.string().optional(),
17+
/** Files that were created or modified by the subtask */
18+
filesModified: z.array(z.string()).default([]),
19+
/** Files that were read (but not modified) by the subtask */
20+
filesRead: z.array(z.string()).default([]),
21+
/** Shell commands that were executed by the subtask */
22+
commandsExecuted: z.array(z.string()).default([]),
23+
/** Count of each tool type used (e.g., { write_to_file: 3, read_file: 5 }) */
24+
toolUsageCounts: z.record(z.string(), z.number()).default({}),
25+
/** Total number of API requests made during the subtask */
26+
apiRequestCount: z.number().default(0),
27+
/** The freeform completion result from attempt_completion */
28+
result: z.string(),
29+
})
30+
31+
export type ContextHandoffSummary = z.infer<typeof contextHandoffSummarySchema>

packages/types/src/history.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from "zod"
22

3+
import { contextHandoffSummarySchema } from "./context-handoff.js"
34
import { taskPermissionsSchema } from "./task-permissions.js"
45

56
/**
@@ -28,6 +29,7 @@ export const historyItemSchema = z.object({
2829
awaitingChildId: z.string().optional(), // Child currently awaited (set when delegated)
2930
completedByChildId: z.string().optional(), // Child that completed and resumed this parent
3031
completionResultSummary: z.string().optional(), // Summary from completed child
32+
contextHandoffSummary: contextHandoffSummarySchema.optional(), // Structured context from completed child
3133
taskPermissions: taskPermissionsSchema.optional(), // Permission boundaries set by parent task
3234
})
3335

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from "./experiment.js"
1111
export * from "./followup.js"
1212
export * from "./git.js"
1313
export * from "./global-settings.js"
14+
export * from "./context-handoff.js"
1415
export * from "./history.js"
1516
export * from "./image-generation.js"
1617
export * from "./ipc.js"

packages/types/src/mode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,6 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
222222
description: "Coordinate tasks across multiple modes",
223223
groups: [],
224224
customInstructions:
225-
'Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\n\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\n\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask\'s specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\n * All necessary context from the parent task or previous subtasks required to complete the work.\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\n * A statement that these specific instructions supersede any conflicting general instructions the subtask\'s mode might have.\n\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\n\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you\'re delegating specific tasks to specific modes.\n\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\n\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\n\n7. Suggest improvements to the workflow based on the results of completed subtasks.\n\n8. When delegating subtasks, consider using the optional `permissions` parameter on `new_task` to restrict what the subtask can do. This is especially useful when:\n * The subtask should only modify files in a specific directory (use `filePatterns`, e.g. `["src/components/.*"]`).\n * The subtask should only run certain commands (use `commandPatterns`, e.g. `["npm test.*", "npm run lint"]`).\n * The subtask should be limited to specific tools (use `allowedTools`, e.g. `["read_file", "search_files"]` for read-only research tasks).\n * Certain tools should be explicitly blocked (use `deniedTools`, e.g. `["execute_command"]` to prevent shell access).\n Permissions are enforced at runtime and follow most-restrictive-wins semantics when subtasks are nested. Use them to keep subtasks focused and safe.\n\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating a subtask rather than overloading the current one.',
225+
'Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\n\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\n\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask\'s specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\n * All necessary context from the parent task or previous subtasks required to complete the work.\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\n * A statement that these specific instructions supersede any conflicting general instructions the subtask\'s mode might have.\n\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\n\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you\'re delegating specific tasks to specific modes.\n\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\n\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\n\n7. Suggest improvements to the workflow based on the results of completed subtasks.\n\n8. When delegating subtasks, consider using the optional `permissions` parameter on `new_task` to restrict what the subtask can do. This is especially useful when:\n * The subtask should only modify files in a specific directory (use `filePatterns`, e.g. `["src/components/.*"]`).\n * The subtask should only run certain commands (use `commandPatterns`, e.g. `["npm test.*", "npm run lint"]`).\n * The subtask should be limited to specific tools (use `allowedTools`, e.g. `["read_file", "search_files"]` for read-only research tasks).\n * Certain tools should be explicitly blocked (use `deniedTools`, e.g. `["execute_command"]` to prevent shell access).\n Permissions are enforced at runtime and follow most-restrictive-wins semantics when subtasks are nested. Use them to keep subtasks focused and safe.\n\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating a subtask rather than overloading the current one.\\n\\n9. When a subtask completes, you will receive a structured context handoff summary alongside the completion result. This summary includes the files modified, files read, commands executed, and tool usage counts from the subtask. Use this structured data to:\\n * Verify the subtask accomplished what was requested by checking the files modified list.\\n * Pass relevant context to subsequent subtasks (e.g., "The previous subtask modified `src/components/Button.tsx` and `src/styles/button.css`").\\n * Identify potential conflicts when multiple subtasks touch the same files.\\n * Provide accurate progress summaries to the user.',
226226
},
227227
] as const
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import type { ClineMessage } from "@roo-code/types"
2+
import { collectContextSummary, formatContextSummaryForParent } from "../collectContextSummary"
3+
4+
describe("collectContextSummary", () => {
5+
it("extracts files modified from tool messages", () => {
6+
const messages: ClineMessage[] = [
7+
{
8+
ts: 1,
9+
type: "ask",
10+
ask: "tool",
11+
text: JSON.stringify({ tool: "editedExistingFile", path: "src/app.ts" }),
12+
},
13+
{
14+
ts: 2,
15+
type: "ask",
16+
ask: "tool",
17+
text: JSON.stringify({ tool: "newFileCreated", path: "src/utils.ts" }),
18+
},
19+
]
20+
21+
const summary = collectContextSummary(messages, "code", "Done")
22+
expect(summary.filesModified).toEqual(["src/app.ts", "src/utils.ts"])
23+
expect(summary.mode).toBe("code")
24+
expect(summary.result).toBe("Done")
25+
})
26+
27+
it("extracts files read from tool messages", () => {
28+
const messages: ClineMessage[] = [
29+
{ ts: 1, type: "ask", ask: "tool", text: JSON.stringify({ tool: "readFile", path: "src/config.ts" }) },
30+
]
31+
32+
const summary = collectContextSummary(messages, "code", "Done")
33+
expect(summary.filesRead).toEqual(["src/config.ts"])
34+
})
35+
36+
it("removes files from filesRead if they were also modified", () => {
37+
const messages: ClineMessage[] = [
38+
{ ts: 1, type: "ask", ask: "tool", text: JSON.stringify({ tool: "readFile", path: "src/app.ts" }) },
39+
{
40+
ts: 2,
41+
type: "ask",
42+
ask: "tool",
43+
text: JSON.stringify({ tool: "editedExistingFile", path: "src/app.ts" }),
44+
},
45+
]
46+
47+
const summary = collectContextSummary(messages, "code", "Done")
48+
expect(summary.filesModified).toEqual(["src/app.ts"])
49+
expect(summary.filesRead).toEqual([])
50+
})
51+
52+
it("extracts executed commands", () => {
53+
const messages: ClineMessage[] = [
54+
{ ts: 1, type: "ask", ask: "command", text: "npm test" },
55+
{ ts: 2, type: "ask", ask: "command", text: "npm run build" },
56+
]
57+
58+
const summary = collectContextSummary(messages, "code", "Done")
59+
expect(summary.commandsExecuted).toEqual(["npm test", "npm run build"])
60+
expect(summary.toolUsageCounts["execute_command"]).toBe(2)
61+
})
62+
63+
it("counts API requests", () => {
64+
const messages: ClineMessage[] = [
65+
{ ts: 1, type: "say", say: "api_req_started" },
66+
{ ts: 2, type: "say", say: "api_req_started" },
67+
{ ts: 3, type: "say", say: "api_req_started" },
68+
]
69+
70+
const summary = collectContextSummary(messages, "debug", "Fixed it")
71+
expect(summary.apiRequestCount).toBe(3)
72+
})
73+
74+
it("counts tool usage correctly", () => {
75+
const messages: ClineMessage[] = [
76+
{ ts: 1, type: "ask", ask: "tool", text: JSON.stringify({ tool: "readFile", path: "a.ts" }) },
77+
{ ts: 2, type: "ask", ask: "tool", text: JSON.stringify({ tool: "readFile", path: "b.ts" }) },
78+
{
79+
ts: 3,
80+
type: "ask",
81+
ask: "tool",
82+
text: JSON.stringify({ tool: "editedExistingFile", path: "c.ts" }),
83+
},
84+
{ ts: 4, type: "ask", ask: "tool", text: JSON.stringify({ tool: "searchFiles" }) },
85+
]
86+
87+
const summary = collectContextSummary(messages, "code", "Done")
88+
expect(summary.toolUsageCounts["read_file"]).toBe(2)
89+
expect(summary.toolUsageCounts["write_to_file"]).toBe(1)
90+
expect(summary.toolUsageCounts["search_files"]).toBe(1)
91+
})
92+
93+
it("deduplicates modified files", () => {
94+
const messages: ClineMessage[] = [
95+
{
96+
ts: 1,
97+
type: "ask",
98+
ask: "tool",
99+
text: JSON.stringify({ tool: "editedExistingFile", path: "src/app.ts" }),
100+
},
101+
{
102+
ts: 2,
103+
type: "ask",
104+
ask: "tool",
105+
text: JSON.stringify({ tool: "editedExistingFile", path: "src/app.ts" }),
106+
},
107+
]
108+
109+
const summary = collectContextSummary(messages, "code", "Done")
110+
expect(summary.filesModified).toEqual(["src/app.ts"])
111+
})
112+
113+
it("handles empty messages array", () => {
114+
const summary = collectContextSummary([], "code", "Nothing done")
115+
expect(summary.filesModified).toEqual([])
116+
expect(summary.filesRead).toEqual([])
117+
expect(summary.commandsExecuted).toEqual([])
118+
expect(summary.apiRequestCount).toBe(0)
119+
expect(summary.result).toBe("Nothing done")
120+
})
121+
122+
it("handles malformed tool JSON gracefully", () => {
123+
const messages: ClineMessage[] = [
124+
{ ts: 1, type: "ask", ask: "tool", text: "not valid json" },
125+
{ ts: 2, type: "ask", ask: "tool", text: undefined },
126+
]
127+
128+
// Should not throw
129+
const summary = collectContextSummary(messages, "code", "Done")
130+
expect(summary.filesModified).toEqual([])
131+
})
132+
133+
it("sorts files alphabetically", () => {
134+
const messages: ClineMessage[] = [
135+
{
136+
ts: 1,
137+
type: "ask",
138+
ask: "tool",
139+
text: JSON.stringify({ tool: "editedExistingFile", path: "z-file.ts" }),
140+
},
141+
{
142+
ts: 2,
143+
type: "ask",
144+
ask: "tool",
145+
text: JSON.stringify({ tool: "editedExistingFile", path: "a-file.ts" }),
146+
},
147+
]
148+
149+
const summary = collectContextSummary(messages, "code", "Done")
150+
expect(summary.filesModified).toEqual(["a-file.ts", "z-file.ts"])
151+
})
152+
})
153+
154+
describe("formatContextSummaryForParent", () => {
155+
it("formats a complete summary into readable text", () => {
156+
const summary = {
157+
mode: "code",
158+
filesModified: ["src/app.ts"],
159+
filesRead: ["src/config.ts"],
160+
commandsExecuted: ["npm test"],
161+
toolUsageCounts: { write_to_file: 1, read_file: 1 },
162+
apiRequestCount: 3,
163+
result: "Task completed",
164+
}
165+
166+
const formatted = formatContextSummaryForParent(summary)
167+
expect(formatted).toContain("Result:\nTask completed")
168+
expect(formatted).toContain("Mode: code")
169+
expect(formatted).toContain("Files Modified:")
170+
expect(formatted).toContain("src/app.ts")
171+
expect(formatted).toContain("Files Read:")
172+
expect(formatted).toContain("src/config.ts")
173+
expect(formatted).toContain("Commands Executed:")
174+
expect(formatted).toContain("npm test")
175+
expect(formatted).toContain("Tool Usage:")
176+
expect(formatted).toContain("API Requests: 3")
177+
})
178+
179+
it("omits empty sections", () => {
180+
const summary = {
181+
mode: undefined,
182+
filesModified: [],
183+
filesRead: [],
184+
commandsExecuted: [],
185+
toolUsageCounts: {},
186+
apiRequestCount: 0,
187+
result: "Done",
188+
}
189+
190+
const formatted = formatContextSummaryForParent(summary)
191+
expect(formatted).toContain("Result:\nDone")
192+
expect(formatted).not.toContain("Files Modified:")
193+
expect(formatted).not.toContain("Files Read:")
194+
expect(formatted).not.toContain("Commands Executed:")
195+
expect(formatted).not.toContain("Tool Usage:")
196+
expect(formatted).toContain("API Requests: 0")
197+
})
198+
})

0 commit comments

Comments
 (0)