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
@@ -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;