Auto-reply: smarter chunking breaks

This commit is contained in:
Peter Steinberger
2025-12-03 00:25:01 +00:00
parent ec46932259
commit b6c45485bc
11 changed files with 239 additions and 50 deletions

View File

@@ -62,7 +62,7 @@ describe("agent buildArgs + parseOutput helpers", () => {
'{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"hello world"}],"usage":{"input":10,"output":5},"model":"pi-1","provider":"inflection","stopReason":"end"}}',
].join("\n");
const parsed = piSpec.parseOutput(stdout);
expect(parsed.text).toBe("hello world");
expect(parsed.texts?.[0]).toBe("hello world");
expect(parsed.meta?.provider).toBe("inflection");
expect((parsed.meta?.usage as { output?: number })?.output).toBe(5);
});
@@ -73,7 +73,7 @@ describe("agent buildArgs + parseOutput helpers", () => {
'{"type":"turn.completed","usage":{"input_tokens":50,"output_tokens":10,"cached_input_tokens":5}}',
].join("\n");
const parsed = codexSpec.parseOutput(stdout);
expect(parsed.text).toBe("hi there");
expect(parsed.texts?.[0]).toBe("hi there");
const usage = parsed.meta?.usage as {
input?: number;
output?: number;
@@ -93,7 +93,7 @@ describe("agent buildArgs + parseOutput helpers", () => {
'{"type":"step_finish","timestamp":1200,"part":{"cost":0.002,"tokens":{"input":100,"output":20}}}',
].join("\n");
const parsed = opencodeSpec.parseOutput(stdout);
expect(parsed.text).toBe("hi");
expect(parsed.texts?.[0]).toBe("hi");
expect(parsed.meta?.extra?.summary).toContain("duration=1200ms");
expect(parsed.meta?.extra?.summary).toContain("cost=$0.0020");
expect(parsed.meta?.extra?.summary).toContain("tokens=100+20");

View File

@@ -14,11 +14,10 @@ type PiAssistantMessage = {
function parsePiJson(raw: string): AgentParseResult {
const lines = raw.split(/\n+/).filter((l) => l.trim().startsWith("{"));
// Collect every assistant message we see; Tau in RPC mode can emit multiple
// assistant payloads in one run (e.g., queued turns, heartbeats). We concatenate
// all text blocks so users see everything instead of only the last message_end.
// Collect only completed assistant messages (skip streaming updates/toolcalls).
const texts: string[] = [];
let lastAssistant: PiAssistantMessage | undefined;
let lastPushed: string | undefined;
for (const line of lines) {
try {
@@ -26,15 +25,24 @@ function parsePiJson(raw: string): AgentParseResult {
type?: string;
message?: PiAssistantMessage;
};
const msg = ev.message;
if (msg?.role === "assistant" && Array.isArray(msg.content)) {
const msgText = msg.content
.filter((c) => c?.type === "text" && typeof c.text === "string")
.map((c) => c.text)
.join("\n")
.trim();
if (msgText) texts.push(msgText);
// keep meta from the most recent assistant message
const isAssistantMessage =
(ev.type === "message" || ev.type === "message_end") &&
ev.message?.role === "assistant" &&
Array.isArray(ev.message.content);
if (!isAssistantMessage) continue;
const msg = ev.message as PiAssistantMessage;
const msgText = msg.content
?.filter((c) => c?.type === "text" && typeof c.text === "string")
.map((c) => c.text)
.join("\n")
.trim();
if (msgText && msgText !== lastPushed) {
texts.push(msgText);
lastPushed = msgText;
lastAssistant = msg;
}
} catch {
@@ -42,12 +50,8 @@ function parsePiJson(raw: string): AgentParseResult {
}
}
// Combine all assistant text messages (ignore tool calls/partials). This keeps
// multi-message replies intact while dropping non-text events.
const text = texts.length ? texts.join("\n\n").trim() : undefined;
const meta: AgentMeta | undefined =
text && lastAssistant
lastAssistant && texts.length
? {
model: lastAssistant.model,
provider: lastAssistant.provider,

View File

@@ -16,6 +16,7 @@ export type AgentMeta = {
};
export type AgentParseResult = {
// Plural to support agents that emit multiple assistant turns per prompt.
texts?: string[];
mediaUrls?: string[];
meta?: AgentMeta;