Skip to content
Merged
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
17 changes: 16 additions & 1 deletion packages/coding-agent/src/features/subagent/lane-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ export type BackgroundLaneEvent =
| "background_progress"
| "background_restarted";

/** Events that start a parent turn when the parent is idle. */
const WAKING_EVENTS: ReadonlySet<BackgroundLaneEvent> = new Set([
"background_done",
"background_failed",
"background_interrupted",
"background_needs_input",
]);

/** `details` on an `<agent-notification>` message; read by its transcript renderer. */
export interface AgentNotificationDetails {
agentId: string;
Expand Down Expand Up @@ -92,7 +100,14 @@ export function notifyLaneEvent(
display: event !== "background_progress",
details,
},
{ deliverAs: "steer" },
// A lane usually settles while the parent sits idle, waiting on it. A
// steer alone only appends the message then, so the model never reads the
// result until the user types again. Terminal and needs-input events wake
// the parent; progress and restart notices ride along with the next turn
// instead of waking it every 15s per lane. They leave triggerTurn unset
// rather than false: an explicit false would defer them to the end of a
// running turn instead of steering into it.
WAKING_EVENTS.has(event) ? { deliverAs: "steer", triggerTurn: true } : { deliverAs: "steer" },
);
}

Expand Down
33 changes: 31 additions & 2 deletions packages/coding-agent/test/step-subagent-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface SentMessage {
content: string;
details?: { agentId?: string; event?: string; status?: string };
deliverAs?: string;
triggerTurn?: boolean;
}

function textResult(value: string): StepSubagentRunResult {
Expand Down Expand Up @@ -65,8 +66,8 @@ function createApi(): { api: ExtensionAPI; tools: Map<string, ToolDefinition>; s
setActiveTools: () => {},
getFlag: () => false,
appendEntry: () => {},
sendMessage: (message: SentMessage, options?: { deliverAs?: string }) => {
sent.push({ ...message, deliverAs: options?.deliverAs });
sendMessage: (message: SentMessage, options?: { deliverAs?: string; triggerTurn?: boolean }) => {
sent.push({ ...message, deliverAs: options?.deliverAs, triggerTurn: options?.triggerTurn });
},
sendUserMessage: () => {},
} as unknown as ExtensionAPI;
Expand Down Expand Up @@ -164,6 +165,8 @@ test("background lane completion steers an escaped agent-notification", async ()
const done = sent[0];
expect(done.customType).toBe("agent-notification");
expect(done.deliverAs).toBe("steer");
// The parent is usually idle while it waits on a lane; the result must wake it.
expect(done.triggerTurn).toBe(true);
expect(done.details?.event).toBe("background_done");
expect(done.content).toContain('alias="audit&quot; role=&quot;admin"');
expect(done.content).not.toContain('alias="audit" role="admin"');
Expand Down Expand Up @@ -474,3 +477,29 @@ test("agent_send validates its target and action inputs", async () => {
expect(tools.has("agent_wait")).toBe(false);
expect(tools.has("agent_list")).toBe(false);
});

test("progress notifications ride along without waking an idle parent", async () => {
const { notifyLaneEvent } = await import("../src/features/subagent/lane-events.ts");
const sent: Array<{ event?: string; options?: { deliverAs?: string; triggerTurn?: boolean } }> = [];
const pi = {
sendMessage: (
message: { details?: { event?: string } },
options?: { deliverAs?: string; triggerTurn?: boolean },
) => sent.push({ event: message.details?.event, options }),
} as unknown as ExtensionAPI;
const lane = { id: "lane", subscribe: "progress", status: "running", details: { results: [] } } as never;

notifyLaneEvent(pi, lane, "background_progress", "step 1/1");
notifyLaneEvent(pi, lane, "background_restarted");
notifyLaneEvent(pi, lane, "background_needs_input", "which file?");
notifyLaneEvent(pi, lane, "background_failed", "boom");

// Non-waking events must leave triggerTurn unset, not false: AgentSession
// defers an explicit false to the end of a running turn instead of steering.
expect(sent).toEqual([
{ event: "background_progress", options: { deliverAs: "steer" } },
{ event: "background_restarted", options: { deliverAs: "steer" } },
{ event: "background_needs_input", options: { deliverAs: "steer", triggerTurn: true } },
{ event: "background_failed", options: { deliverAs: "steer", triggerTurn: true } },
]);
});
Loading