diff --git a/src/agent/async-agent.test.ts b/src/agent/async-agent.test.ts index 3d359d3d..3ebe01fd 100644 --- a/src/agent/async-agent.test.ts +++ b/src/agent/async-agent.test.ts @@ -7,6 +7,7 @@ const internalRunState = { value: false }; const runMock = vi.fn(async () => ({ text: "", thinking: undefined, error: undefined })); const runInternalMock = vi.fn(async () => ({ text: "", thinking: undefined, error: undefined })); const flushSessionMock = vi.fn(async () => {}); +const persistAssistantSummaryMock = vi.fn(); const subscribeAllMock = vi.fn((fn: (event: any) => void) => { subscribeCallbacks.push(fn); return () => {}; @@ -19,6 +20,7 @@ vi.mock("./runner.js", () => ({ run = runMock; runInternal = runInternalMock; flushSession = flushSessionMock; + persistAssistantSummary = persistAssistantSummaryMock; get isInternalRun() { return internalRunState.value; } @@ -84,6 +86,7 @@ describe("AsyncAgent internal flow", () => { runMock.mockReset(); runInternalMock.mockReset(); flushSessionMock.mockReset(); + persistAssistantSummaryMock.mockReset(); subscribeAllMock.mockClear(); runMock.mockResolvedValue({ text: "", thinking: undefined, error: undefined }); runInternalMock.mockResolvedValue({ text: "", thinking: undefined, error: undefined }); @@ -207,4 +210,55 @@ describe("AsyncAgent internal flow", () => { internalRunState.value = false; agent.close(); }); + + it("persists assistant summary when persistResponse is true and result has text", async () => { + runInternalMock.mockResolvedValueOnce({ text: "Summary of findings", thinking: undefined, error: undefined }); + const agent = new AsyncAgent(); + + agent.writeInternal("announce findings", { forwardAssistant: true, persistResponse: true }); + await agent.waitForIdle(); + + expect(persistAssistantSummaryMock).toHaveBeenCalledOnce(); + expect(persistAssistantSummaryMock).toHaveBeenCalledWith("Summary of findings"); + // flushSession called twice: once after runInternal, once after persistAssistantSummary + expect(flushSessionMock).toHaveBeenCalledTimes(2); + + agent.close(); + }); + + it("does not persist assistant summary when result text is NO_REPLY", async () => { + runInternalMock.mockResolvedValueOnce({ text: "NO_REPLY", thinking: undefined, error: undefined }); + const agent = new AsyncAgent(); + + agent.writeInternal("announce findings", { forwardAssistant: true, persistResponse: true }); + await agent.waitForIdle(); + + expect(persistAssistantSummaryMock).not.toHaveBeenCalled(); + + agent.close(); + }); + + it("does not persist assistant summary when result text is empty", async () => { + runInternalMock.mockResolvedValueOnce({ text: " ", thinking: undefined, error: undefined }); + const agent = new AsyncAgent(); + + agent.writeInternal("announce findings", { forwardAssistant: true, persistResponse: true }); + await agent.waitForIdle(); + + expect(persistAssistantSummaryMock).not.toHaveBeenCalled(); + + agent.close(); + }); + + it("does not persist assistant summary when persistResponse is not set", async () => { + runInternalMock.mockResolvedValueOnce({ text: "Summary of findings", thinking: undefined, error: undefined }); + const agent = new AsyncAgent(); + + agent.writeInternal("announce findings", { forwardAssistant: true }); + await agent.waitForIdle(); + + expect(persistAssistantSummaryMock).not.toHaveBeenCalled(); + + agent.close(); + }); }); diff --git a/src/agent/async-agent.ts b/src/agent/async-agent.ts index a284871b..b3b76ec1 100644 --- a/src/agent/async-agent.ts +++ b/src/agent/async-agent.ts @@ -13,6 +13,8 @@ export type ChannelItem = Message | AgentEvent | MulticaEvent; export interface WriteInternalOptions { /** Forward assistant message_end events to realtime stream during internal runs */ forwardAssistant?: boolean | undefined; + /** After internal run completes, persist the LLM's summary as a non-internal assistant message */ + persistResponse?: boolean | undefined; } export class AsyncAgent { @@ -74,6 +76,7 @@ export class AsyncAgent { writeInternal(content: string, options?: WriteInternalOptions): void { if (this._closed) throw new Error("Agent is closed"); const forwardAssistant = options?.forwardAssistant === true; + const persistResponse = options?.persistResponse === true; this.queue = this.queue .then(async () => { @@ -87,6 +90,11 @@ export class AsyncAgent { // Internal run errors are for diagnostics only; do not leak to user stream. console.error(`[AsyncAgent] Internal run error: ${result.error}`); } + // Persist the LLM summary so it remains in parent context for future turns + if (persistResponse && result.text?.trim() && result.text.trim() !== "NO_REPLY") { + this.agent.persistAssistantSummary(result.text.trim()); + await this.agent.flushSession(); + } } finally { this.forwardInternalAssistant = prevForward; } diff --git a/src/agent/runner.ts b/src/agent/runner.ts index 8955b04c..41c0f7a2 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -563,6 +563,35 @@ export class Agent { return this._internalRun; } + /** + * Persist a synthetic assistant message into both in-memory state and session JSONL. + * Used after an internal run to keep the LLM summary visible in future turns + * while the internal prompt stays hidden. + */ + persistAssistantSummary(text: string): void { + const model = this.agent.state.model; + const message = { + role: "assistant" as const, + content: [{ type: "text" as const, text }], + api: model?.api ?? "openai-completions", + provider: model?.provider ?? "internal", + model: model?.id ?? "unknown", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop" as const, + timestamp: Date.now(), + }; + + this.agent.appendMessage(message); + this.session.saveMessage(message); + } + /** Ensure session messages are loaded from disk (idempotent) */ async ensureInitialized(): Promise { if (this.initialized) return; diff --git a/src/agent/subagent/announce.ts b/src/agent/subagent/announce.ts index 5f9b5438..54c92e75 100644 --- a/src/agent/subagent/announce.ts +++ b/src/agent/subagent/announce.ts @@ -293,7 +293,7 @@ export function runCoalescedAnnounceFlow( return false; } - parentAgent.writeInternal(message, { forwardAssistant: true }); + parentAgent.writeInternal(message, { forwardAssistant: true, persistResponse: true }); return true; } catch (err) { console.error(`[SubagentAnnounce] Failed to coalesced-announce to parent:`, err); @@ -341,7 +341,7 @@ export function runSubagentAnnounceFlow(params: SubagentAnnounceParams): boolean return false; } - parentAgent.writeInternal(message, { forwardAssistant: true }); + parentAgent.writeInternal(message, { forwardAssistant: true, persistResponse: true }); return true; } catch (err) { console.error(`[SubagentAnnounce] Failed to announce to parent:`, err);