Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")}`;
}
Comment on lines +171 to +185
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior here (injecting prerequisite results into prompts) isn’t covered by the existing integration tests. Please add a test that completes a dependency task with a metadata.result and verifies the cascaded spawn prompt includes the injected prerequisite section (and truncation behavior for long results).

Copilot uses AI. Check for mistakes.
}

if (additionalContext) prompt += `\n\n${additionalContext}`;
prompt += `\n\nComplete this task fully. Do not attempt to manage tasks yourself.`;
return prompt;
Expand Down Expand Up @@ -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 } });
Expand Down Expand Up @@ -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 } });
Expand Down
103 changes: 103 additions & 0 deletions test/subagent-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,3 +891,106 @@ describe("Widget agent ID display", () => {
expect(lines[1]).not.toContain("agent abc");
});
});

describe("Cascade data injection (buildTaskPrompt)", () => {
let mock: ReturnType<typeof mockPi>;
let rpc: ReturnType<typeof installSubagentsMock>;

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");
});
});