fix(subagent): capture latest non-empty findings from child runs
This commit is contained in:
parent
4d6017e782
commit
12075b96f2
2 changed files with 95 additions and 7 deletions
67
src/agent/subagent/announce-findings.test.ts
Normal file
67
src/agent/subagent/announce-findings.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const readEntriesMock = vi.fn();
|
||||
|
||||
vi.mock("../session/storage.js", () => ({
|
||||
readEntries: (sessionId: string) => readEntriesMock(sessionId),
|
||||
}));
|
||||
|
||||
import { readLatestAssistantReply } from "./announce.js";
|
||||
|
||||
describe("readLatestAssistantReply", () => {
|
||||
beforeEach(() => {
|
||||
readEntriesMock.mockReset();
|
||||
});
|
||||
|
||||
it("returns the latest non-empty assistant text when the last assistant message is tool-only", () => {
|
||||
readEntriesMock.mockReturnValue([
|
||||
{
|
||||
type: "message",
|
||||
timestamp: 1,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "南京天气:晴,12°C。" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
timestamp: 2,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "tool-1", name: "weather", arguments: { city: "Nanjing" } }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = readLatestAssistantReply("child-session");
|
||||
expect(result).toBe("南京天气:晴,12°C。");
|
||||
});
|
||||
|
||||
it("falls back to latest toolResult text when no assistant text exists", () => {
|
||||
readEntriesMock.mockReturnValue([
|
||||
{
|
||||
type: "message",
|
||||
timestamp: 1,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "tool-2", name: "weather", arguments: { city: "Nanjing" } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
timestamp: 2,
|
||||
message: {
|
||||
role: "toolResult",
|
||||
toolCallId: "tool-2",
|
||||
toolName: "weather",
|
||||
content: [{ type: "text", text: "{\"city\":\"Nanjing\",\"tempC\":12,\"condition\":\"Sunny\"}" }],
|
||||
isError: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = readLatestAssistantReply("child-session");
|
||||
expect(result).toContain("\"city\":\"Nanjing\"");
|
||||
expect(result).toContain("\"condition\":\"Sunny\"");
|
||||
});
|
||||
});
|
||||
|
|
@ -39,19 +39,29 @@ export function buildSubagentSystemPrompt(params: SubagentSystemPromptParams): s
|
|||
*/
|
||||
export function readLatestAssistantReply(sessionId: string): string | undefined {
|
||||
const entries = readEntries(sessionId);
|
||||
let latestToolResultText: string | undefined;
|
||||
|
||||
// Walk backwards to find last assistant message
|
||||
// Walk backwards to find the last non-empty assistant reply.
|
||||
// If no assistant text exists (e.g. run ended after tool execution),
|
||||
// fall back to the latest non-empty toolResult content.
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const entry = entries[i]!;
|
||||
if (entry.type !== "message") continue;
|
||||
|
||||
const message = entry.message;
|
||||
if (message.role !== "assistant") continue;
|
||||
if (message.role === "assistant") {
|
||||
const text = extractAssistantText(message);
|
||||
if (text) return text;
|
||||
continue;
|
||||
}
|
||||
|
||||
return extractAssistantText(message);
|
||||
if (message.role === "toolResult" && !latestToolResultText) {
|
||||
const text = extractToolResultText(message);
|
||||
if (text) latestToolResultText = text;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return latestToolResultText;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,7 +69,17 @@ export function readLatestAssistantReply(sessionId: string): string | undefined
|
|||
* AgentMessage.content for assistant is (TextContent | ThinkingContent | ToolCall)[].
|
||||
*/
|
||||
function extractAssistantText(message: { role: string; content: unknown }): string {
|
||||
const content = message.content;
|
||||
return extractTextLikeContent(message.content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from a toolResult message.
|
||||
*/
|
||||
function extractToolResultText(message: { role: string; content: unknown }): string {
|
||||
return extractTextLikeContent(message.content);
|
||||
}
|
||||
|
||||
function extractTextLikeContent(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return sanitizeText(content);
|
||||
}
|
||||
|
|
@ -68,8 +88,9 @@ function extractAssistantText(message: { role: string; content: unknown }): stri
|
|||
|
||||
const textParts: string[] = [];
|
||||
for (const block of content) {
|
||||
if (block && typeof block === "object" && "type" in block && block.type === "text" && "text" in block) {
|
||||
textParts.push(String(block.text));
|
||||
if (!block || typeof block !== "object") continue;
|
||||
if ("text" in block) {
|
||||
textParts.push(String((block as { text: unknown }).text));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue