Notify callers when watched children close and cap responses (#3192)

Wake caller agents when watched children close and cap embedded child responses at 4,000 characters.

Co-authored-by: Hanjun(Tony) <54878785+wilgon456@users.noreply.github.com>
Co-authored-by: Thong Van <minhthong@gmail.com>
This commit is contained in:
Hanjun(Tony)
2026-08-11 19:08:41 +02:00
committed by GitHub
co-authored by Thong Van
parent f24664b882
commit cdb1163145
3 changed files with 62 additions and 8 deletions
+1
View File
@@ -51,6 +51,7 @@ Users can also detach an existing subagent from the subagents track. Detach is d
`notifyOnFinish` defaults to `true` for agent-scoped creation and background prompt follow-ups because most delegated work needs to report back to the creating agent. Set it to `false` only for truly fire-and-forget agents or prompts.
Permission requests are notification checkpoints, not the end of that subscription. The caller is notified again after a permission response when the child finishes, errors, or requests another permission.
The permission notification includes the normalized request plus the child and request IDs, so the caller can inspect it and respond without fetching agent status.
A watched child that closes before its finish event also notifies the caller so delegated work cannot disappear silently during archive or workspace teardown.
## Provider-managed child agents
@@ -52,6 +52,7 @@ interface FinishNotificationScenario {
resolveChildPermissionWhileIdle(requestId?: string): void;
finishChild(): void;
finishChildAndReadParentPrompt(): Promise<string>;
closeChildAndReadParentPrompt(): Promise<string>;
parentPrompts(): string[];
wasParentPrompted(): boolean;
}
@@ -211,6 +212,25 @@ function createFinishNotificationScenario(
return parentPrompt;
},
async closeChildAndReadParentPrompt() {
const parentPrompt = new Promise<string>((resolve) => {
resolveParentPrompt = resolve;
});
childAgent.lifecycle = "running";
subscriber?.({
type: "agent_state",
agent: childAgent,
});
childAgent.lifecycle = "closed";
subscriber?.({
type: "agent_state",
agent: childAgent,
});
return parentPrompt;
},
parentPrompts() {
return parentPrompts;
},
@@ -279,6 +299,34 @@ test("finish notifications tell the parent the child's last assistant message",
);
});
test("finish notifications truncate oversized child responses", async () => {
const included = "x".repeat(4000);
const omitted = "TAIL-MARKER".repeat(50);
const scenario = createFinishNotificationScenario({
childLastAssistantMessage: included + omitted,
});
scenario.startWatchingChild();
const parentPrompt = await scenario.finishChildAndReadParentPrompt();
expect(parentPrompt).toContain(included);
expect(parentPrompt).toContain(
`[truncated ${omitted.length} chars; use get_agent_activity for the full response]`,
);
expect(parentPrompt).not.toContain("TAIL-MARKER");
});
test("closing a watched child notifies the caller", async () => {
const scenario = createFinishNotificationScenario();
scenario.startWatchingChild();
const parentPrompt = await scenario.closeChildAndReadParentPrompt();
expect(parentPrompt).toEqual(
formatSystemNotificationPrompt("Agent child-agent (Child Agent) was closed."),
);
});
test("finish notifications survive permission responses", async () => {
const scenario = createFinishNotificationScenario();
@@ -253,10 +253,14 @@ export interface SetupFinishNotificationParams {
logger: Logger;
}
type FinishNotificationReason = "finished" | "errored" | "needs permission" | "was closed";
const FINISH_NOTIFICATION_MESSAGE_LIMIT = 4000;
interface FinishNotificationBodyInput {
childAgentId: string;
title: string;
reason: "finished" | "errored" | "needs permission";
reason: FinishNotificationReason;
lastAssistantMessage: string | null;
permissionRequest?: AgentPermissionRequest;
}
@@ -278,8 +282,12 @@ function formatFinishNotificationBody(params: FinishNotificationBodyInput): stri
)}\n</permission-request>`,
);
}
const lastAssistantMessage = params.lastAssistantMessage?.trim();
let lastAssistantMessage = params.lastAssistantMessage?.trim();
if (lastAssistantMessage) {
if (lastAssistantMessage.length > FINISH_NOTIFICATION_MESSAGE_LIMIT) {
const omitted = lastAssistantMessage.length - FINISH_NOTIFICATION_MESSAGE_LIMIT;
lastAssistantMessage = `${lastAssistantMessage.slice(0, FINISH_NOTIFICATION_MESSAGE_LIMIT)}\n[truncated ${omitted} chars; use get_agent_activity for the full response]`;
}
sections.push(`<agent-response>\n${lastAssistantMessage}\n</agent-response>`);
}
return sections.join("\n\n");
@@ -312,7 +320,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
}
async function notify(
reason: "finished" | "errored" | "needs permission",
reason: FinishNotificationReason,
permissionRequest?: AgentPermissionRequest,
): Promise<void> {
const callerRecord = await agentStorage.get(callerAgentId);
@@ -344,10 +352,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
});
}
function notifySafely(
reason: "finished" | "errored" | "needs permission",
options: NotifySafelyOptions = {},
): void {
function notifySafely(reason: FinishNotificationReason, options: NotifySafelyOptions = {}): void {
if (stopped) return;
if (options.terminal ?? true) stop();
notificationQueue = notificationQueue
@@ -387,7 +392,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
return;
}
if (event.agent.lifecycle === "closed") {
stop();
notifySafely("was closed");
return;
}
return;