Include child results in finish notifications (#1558)
* Include child results in finish notifications * Nest child results in system notifications
This commit is contained in:
@@ -11,6 +11,97 @@ import {
|
|||||||
} from "./agent-prompt.js";
|
} from "./agent-prompt.js";
|
||||||
import type { AgentManagerEvent, ManagedAgent } from "./agent-manager.js";
|
import type { AgentManagerEvent, ManagedAgent } from "./agent-manager.js";
|
||||||
|
|
||||||
|
interface FinishNotificationScenarioOptions {
|
||||||
|
childLastAssistantMessage?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FinishNotificationScenario {
|
||||||
|
startWatchingChild(): void;
|
||||||
|
finishChildAndReadParentPrompt(): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFinishNotificationScenario(
|
||||||
|
options?: FinishNotificationScenarioOptions,
|
||||||
|
): FinishNotificationScenario {
|
||||||
|
let subscriber: ((event: AgentManagerEvent) => void) | null = null;
|
||||||
|
let resolveParentPrompt: ((prompt: string) => void) | null = null;
|
||||||
|
|
||||||
|
const childAgent: ManagedAgent = Object.create(null);
|
||||||
|
Reflect.set(childAgent, "id", "child-agent");
|
||||||
|
Reflect.set(childAgent, "lifecycle", "idle");
|
||||||
|
Reflect.set(childAgent, "config", { title: "Child Agent" });
|
||||||
|
|
||||||
|
const callerAgent: ManagedAgent = Object.create(null);
|
||||||
|
Reflect.set(callerAgent, "id", "caller-agent");
|
||||||
|
Reflect.set(callerAgent, "lifecycle", "idle");
|
||||||
|
Reflect.set(callerAgent, "config", { title: "Caller Agent" });
|
||||||
|
|
||||||
|
const agentManager: AgentManager = Object.create(AgentManager.prototype);
|
||||||
|
Reflect.set(agentManager, "getAgent", (agentId: string) => {
|
||||||
|
if (agentId === "child-agent") {
|
||||||
|
return childAgent;
|
||||||
|
}
|
||||||
|
if (agentId === "caller-agent") {
|
||||||
|
return callerAgent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
Reflect.set(agentManager, "subscribe", (callback: (event: AgentManagerEvent) => void) => {
|
||||||
|
subscriber = callback;
|
||||||
|
return () => {
|
||||||
|
subscriber = null;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
Reflect.set(agentManager, "getLastAssistantMessage", async () => {
|
||||||
|
return options?.childLastAssistantMessage ?? null;
|
||||||
|
});
|
||||||
|
Reflect.set(agentManager, "tryRunOutOfBand", () => false);
|
||||||
|
Reflect.set(agentManager, "hasInFlightRun", () => false);
|
||||||
|
Reflect.set(agentManager, "streamAgent", (_agentId: string, prompt: string) => {
|
||||||
|
resolveParentPrompt?.(prompt);
|
||||||
|
return (async function* noop() {})();
|
||||||
|
});
|
||||||
|
|
||||||
|
const agentStorage: AgentStorage = Object.create(AgentStorage.prototype);
|
||||||
|
Reflect.set(agentStorage, "get", async (agentId: string) => {
|
||||||
|
if (agentId === "child-agent") {
|
||||||
|
return { title: "Child Agent" };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startWatchingChild() {
|
||||||
|
setupFinishNotification({
|
||||||
|
agentManager,
|
||||||
|
agentStorage,
|
||||||
|
childAgentId: "child-agent",
|
||||||
|
callerAgentId: "caller-agent",
|
||||||
|
logger: createTestLogger(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async finishChildAndReadParentPrompt() {
|
||||||
|
const parentPrompt = new Promise<string>((resolve) => {
|
||||||
|
resolveParentPrompt = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
childAgent.lifecycle = "running";
|
||||||
|
subscriber?.({
|
||||||
|
type: "agent_state",
|
||||||
|
agent: childAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
childAgent.lifecycle = "idle";
|
||||||
|
subscriber?.({
|
||||||
|
type: "agent_state",
|
||||||
|
agent: childAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return parentPrompt;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
test("isSystemInjectedEnvelope matches the envelope formatSystemNotificationPrompt produces", () => {
|
test("isSystemInjectedEnvelope matches the envelope formatSystemNotificationPrompt produces", () => {
|
||||||
expect(isSystemInjectedEnvelope(formatSystemNotificationPrompt("child finished"))).toBe(true);
|
expect(isSystemInjectedEnvelope(formatSystemNotificationPrompt("child finished"))).toBe(true);
|
||||||
expect(isSystemInjectedEnvelope("hello world")).toBe(false);
|
expect(isSystemInjectedEnvelope("hello world")).toBe(false);
|
||||||
@@ -55,6 +146,21 @@ test("sendPromptToAgent forwards the client message id as run options", async ()
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("finish notifications tell the parent the child's last assistant message", async () => {
|
||||||
|
const scenario = createFinishNotificationScenario({
|
||||||
|
childLastAssistantMessage: "Implemented the cleanup and all checks pass.",
|
||||||
|
});
|
||||||
|
|
||||||
|
scenario.startWatchingChild();
|
||||||
|
const parentPrompt = await scenario.finishChildAndReadParentPrompt();
|
||||||
|
|
||||||
|
expect(parentPrompt).toEqual(
|
||||||
|
formatSystemNotificationPrompt(
|
||||||
|
"Agent child-agent (Child Agent) finished.\n\n<agent-response>\nImplemented the cleanup and all checks pass.\n</agent-response>",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not notify archived callers", async () => {
|
it("does not notify archived callers", async () => {
|
||||||
let subscriber: ((event: AgentManagerEvent) => void) | null = null;
|
let subscriber: ((event: AgentManagerEvent) => void) | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,22 @@ export interface SetupFinishNotificationParams {
|
|||||||
logger: Logger;
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FinishNotificationBodyInput {
|
||||||
|
childAgentId: string;
|
||||||
|
title: string;
|
||||||
|
reason: "finished" | "errored" | "needs permission";
|
||||||
|
lastAssistantMessage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFinishNotificationBody(params: FinishNotificationBodyInput): string {
|
||||||
|
const statusLine = `Agent ${params.childAgentId} (${params.title}) ${params.reason}.`;
|
||||||
|
const lastAssistantMessage = params.lastAssistantMessage?.trim();
|
||||||
|
if (!lastAssistantMessage) {
|
||||||
|
return statusLine;
|
||||||
|
}
|
||||||
|
return `${statusLine}\n\n<agent-response>\n${lastAssistantMessage}\n</agent-response>`;
|
||||||
|
}
|
||||||
|
|
||||||
export function setupFinishNotification(params: SetupFinishNotificationParams): void {
|
export function setupFinishNotification(params: SetupFinishNotificationParams): void {
|
||||||
const { agentManager, agentStorage, childAgentId, callerAgentId, logger } = params;
|
const { agentManager, agentStorage, childAgentId, callerAgentId, logger } = params;
|
||||||
let hasSeenRunning = false;
|
let hasSeenRunning = false;
|
||||||
@@ -265,9 +281,20 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
|
|||||||
fired = true;
|
fired = true;
|
||||||
unsubscribe?.();
|
unsubscribe?.();
|
||||||
|
|
||||||
|
const callerRecord = await agentStorage.get(callerAgentId);
|
||||||
|
if (callerRecord?.archivedAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const record = await agentStorage.get(childAgentId);
|
const record = await agentStorage.get(childAgentId);
|
||||||
const title = record?.title ?? childAgentId;
|
const title = record?.title ?? childAgentId;
|
||||||
const body = `Agent ${childAgentId} (${title}) ${reason}.`;
|
const lastAssistantMessage = await agentManager.getLastAssistantMessage(childAgentId);
|
||||||
|
const body = formatFinishNotificationBody({
|
||||||
|
childAgentId,
|
||||||
|
title,
|
||||||
|
reason,
|
||||||
|
lastAssistantMessage,
|
||||||
|
});
|
||||||
|
|
||||||
await sendPromptToAgent({
|
await sendPromptToAgent({
|
||||||
agentManager,
|
agentManager,
|
||||||
|
|||||||
Reference in New Issue
Block a user