From c8e75b02e638b056d20afe8e2efbf30d9a6f52d3 Mon Sep 17 00:00:00 2001 From: huynhgiabuu Date: Wed, 25 Mar 2026 18:16:37 +0700 Subject: [PATCH] feat: inject dependency results into cascade prompts and forward model option - Inject completed dependency results (up to 4KB per dep) into buildTaskPrompt - Forward model option to direct spawn and auto-cascade spawn calls - Add integration tests covering injected prerequisite context and truncation All tests pass. --- src/index.ts | 29 ++++++++- test/subagent-integration.test.ts | 103 ++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 12327c4..b203856 100644 --- a/src/index.ts +++ b/src/index.ts @@ -159,9 +159,32 @@ export default function (pi: ExtensionAPI) { checkSubagentsVersion(); pi.events.on("subagents:ready", () => checkSubagentsVersion()); - /** Build a prompt for a task being executed by a subagent. */ - function buildTaskPrompt(task: { id: string; subject: string; description: string }, additionalContext?: string): string { + /** Build a prompt for a task being executed by a subagent. + * Injects completed dependency results so cascaded agents have context from prerequisites. + */ + function buildTaskPrompt( + task: { id: string; subject: string; description: string; blockedBy?: string[] }, + additionalContext?: string, + ): string { let prompt = `You are executing task #${task.id}: "${task.subject}"\n\n${task.description}`; + + // Inject completed dependency results so cascaded agents have full context + if (task.blockedBy && task.blockedBy.length > 0) { + const depResults: string[] = []; + for (const depId of task.blockedBy) { + const dep = store.get(depId); + if (dep?.metadata?.result) { + const result = dep.metadata.result.length > 4000 + ? dep.metadata.result.slice(0, 4000) + "\n\n[... truncated — use TaskGet for full output]" + : dep.metadata.result; + depResults.push(`### Task #${depId}: ${dep.subject}\n${result}`); + } + } + if (depResults.length > 0) { + prompt += `\n\n## Prerequisite task results\n\n${depResults.join("\n\n")}`; + } + } + if (additionalContext) prompt += `\n\n${additionalContext}`; prompt += `\n\nComplete this task fully. Do not attempt to manage tasks yourself.`; return prompt; @@ -200,6 +223,7 @@ export default function (pi: ExtensionAPI) { description: next.subject, isBackground: true, maxTurns: cascadeConfig.maxTurns, + ...(cascadeConfig.model ? { model: cascadeConfig.model } : {}), }); agentTaskMap.set(agentId, next.id); store.update(next.id, { owner: agentId, metadata: { ...next.metadata, agentId } }); @@ -908,6 +932,7 @@ Set up task dependencies: description: task.subject, isBackground: true, maxTurns: params.max_turns, + ...(params.model ? { model: params.model } : {}), }); agentTaskMap.set(agentId, taskId); store.update(taskId, { owner: agentId, metadata: { ...task.metadata, agentId } }); diff --git a/test/subagent-integration.test.ts b/test/subagent-integration.test.ts index dd3c36f..2bdb36c 100644 --- a/test/subagent-integration.test.ts +++ b/test/subagent-integration.test.ts @@ -891,3 +891,106 @@ describe("Widget agent ID display", () => { expect(lines[1]).not.toContain("agent abc"); }); }); + +describe("Cascade data injection (buildTaskPrompt)", () => { + let mock: ReturnType; + let rpc: ReturnType; + + beforeEach(async () => { + // Enable autoCascade via config file in cwd + const fs = await import("node:fs"); + const path = await import("node:path"); + const configPath = path.join(process.cwd(), ".pi", "tasks-config.json"); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify({ autoCascade: true })); + + mock = mockPi(); + rpc = installSubagentsMock(mock.pi); + initExtension(mock.pi as any); + + // Set latestCtx via turn_start lifecycle event + await mock.fireLifecycle("turn_start", {}, mockCtx()); + }); + + afterEach(async () => { + rpc.unsub(); + const fs = await import("node:fs"); + const path = await import("node:path"); + try { fs.unlinkSync(path.join(process.cwd(), ".pi", "tasks-config.json")); } catch {} + }); + + it("injects prerequisite result into cascaded agent prompt", async () => { + await mock.executeTool("TaskCreate", { + subject: "Task A", + description: "Produce a result", + agentType: "general-purpose", + }); + await mock.executeTool("TaskCreate", { + subject: "Task B", + description: "Use Task A result", + agentType: "general-purpose", + }); + await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); + + await mock.executeTool("TaskExecute", { task_ids: ["1"] }); + expect(rpc.spawned).toHaveLength(1); + + mock.emitEvent("subagents:completed", { id: "agent-1", result: "The answer is 42" }); + + await vi.waitFor(() => expect(rpc.spawned).toHaveLength(2), { timeout: 1000 }); + + const bPrompt = rpc.spawned[1].prompt; + expect(bPrompt).toContain("Prerequisite task results"); + expect(bPrompt).toContain("Task #1"); + expect(bPrompt).toContain("The answer is 42"); + }); + + it("truncates long prerequisite results at 4KB", async () => { + await mock.executeTool("TaskCreate", { + subject: "Task A", + description: "Produce a long result", + agentType: "general-purpose", + }); + await mock.executeTool("TaskCreate", { + subject: "Task B", + description: "Use truncated result", + agentType: "general-purpose", + }); + await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); + + await mock.executeTool("TaskExecute", { task_ids: ["1"] }); + + const longResult = "x".repeat(5000); + mock.emitEvent("subagents:completed", { id: "agent-1", result: longResult }); + + await vi.waitFor(() => expect(rpc.spawned).toHaveLength(2), { timeout: 1000 }); + + const bPrompt = rpc.spawned[1].prompt; + expect(bPrompt).toContain("truncated"); + expect(bPrompt).toContain("TaskGet"); + expect(bPrompt.length).toBeLessThan(longResult.length); + }); + + it("handles dependencies with no stored result gracefully", async () => { + await mock.executeTool("TaskCreate", { + subject: "Task A", + description: "No result stored", + agentType: "general-purpose", + }); + await mock.executeTool("TaskCreate", { + subject: "Task B", + description: "Works without A result", + agentType: "general-purpose", + }); + await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); + + await mock.executeTool("TaskExecute", { task_ids: ["1"] }); + + mock.emitEvent("subagents:completed", { id: "agent-1" }); + + await vi.waitFor(() => expect(rpc.spawned).toHaveLength(2), { timeout: 1000 }); + + const bPrompt = rpc.spawned[1].prompt; + expect(bPrompt).not.toContain("Prerequisite task results"); + }); +});