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:
co-authored by
Thong Van
parent
f24664b882
commit
cdb1163145
@@ -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.
|
`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.
|
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.
|
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
|
## Provider-managed child agents
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ interface FinishNotificationScenario {
|
|||||||
resolveChildPermissionWhileIdle(requestId?: string): void;
|
resolveChildPermissionWhileIdle(requestId?: string): void;
|
||||||
finishChild(): void;
|
finishChild(): void;
|
||||||
finishChildAndReadParentPrompt(): Promise<string>;
|
finishChildAndReadParentPrompt(): Promise<string>;
|
||||||
|
closeChildAndReadParentPrompt(): Promise<string>;
|
||||||
parentPrompts(): string[];
|
parentPrompts(): string[];
|
||||||
wasParentPrompted(): boolean;
|
wasParentPrompted(): boolean;
|
||||||
}
|
}
|
||||||
@@ -211,6 +212,25 @@ function createFinishNotificationScenario(
|
|||||||
|
|
||||||
return parentPrompt;
|
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() {
|
parentPrompts() {
|
||||||
return 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 () => {
|
test("finish notifications survive permission responses", async () => {
|
||||||
const scenario = createFinishNotificationScenario();
|
const scenario = createFinishNotificationScenario();
|
||||||
|
|
||||||
|
|||||||
@@ -253,10 +253,14 @@ export interface SetupFinishNotificationParams {
|
|||||||
logger: Logger;
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FinishNotificationReason = "finished" | "errored" | "needs permission" | "was closed";
|
||||||
|
|
||||||
|
const FINISH_NOTIFICATION_MESSAGE_LIMIT = 4000;
|
||||||
|
|
||||||
interface FinishNotificationBodyInput {
|
interface FinishNotificationBodyInput {
|
||||||
childAgentId: string;
|
childAgentId: string;
|
||||||
title: string;
|
title: string;
|
||||||
reason: "finished" | "errored" | "needs permission";
|
reason: FinishNotificationReason;
|
||||||
lastAssistantMessage: string | null;
|
lastAssistantMessage: string | null;
|
||||||
permissionRequest?: AgentPermissionRequest;
|
permissionRequest?: AgentPermissionRequest;
|
||||||
}
|
}
|
||||||
@@ -278,8 +282,12 @@ function formatFinishNotificationBody(params: FinishNotificationBodyInput): stri
|
|||||||
)}\n</permission-request>`,
|
)}\n</permission-request>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const lastAssistantMessage = params.lastAssistantMessage?.trim();
|
let lastAssistantMessage = params.lastAssistantMessage?.trim();
|
||||||
if (lastAssistantMessage) {
|
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>`);
|
sections.push(`<agent-response>\n${lastAssistantMessage}\n</agent-response>`);
|
||||||
}
|
}
|
||||||
return sections.join("\n\n");
|
return sections.join("\n\n");
|
||||||
@@ -312,7 +320,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function notify(
|
async function notify(
|
||||||
reason: "finished" | "errored" | "needs permission",
|
reason: FinishNotificationReason,
|
||||||
permissionRequest?: AgentPermissionRequest,
|
permissionRequest?: AgentPermissionRequest,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const callerRecord = await agentStorage.get(callerAgentId);
|
const callerRecord = await agentStorage.get(callerAgentId);
|
||||||
@@ -344,10 +352,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifySafely(
|
function notifySafely(reason: FinishNotificationReason, options: NotifySafelyOptions = {}): void {
|
||||||
reason: "finished" | "errored" | "needs permission",
|
|
||||||
options: NotifySafelyOptions = {},
|
|
||||||
): void {
|
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
if (options.terminal ?? true) stop();
|
if (options.terminal ?? true) stop();
|
||||||
notificationQueue = notificationQueue
|
notificationQueue = notificationQueue
|
||||||
@@ -387,7 +392,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.agent.lifecycle === "closed") {
|
if (event.agent.lifecycle === "closed") {
|
||||||
stop();
|
notifySafely("was closed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user