fix: handle fence-close paragraph breaks

This commit is contained in:
Peter Steinberger
2026-01-09 23:19:19 +01:00
parent 22b3bd4415
commit 3b91148a0a
2 changed files with 51 additions and 17 deletions

View File

@@ -129,11 +129,13 @@ export class EmbeddedBlockChunker {
if (preference === "paragraph") { if (preference === "paragraph") {
let paragraphIdx = buffer.indexOf("\n\n"); let paragraphIdx = buffer.indexOf("\n\n");
while (paragraphIdx !== -1) { while (paragraphIdx !== -1) {
if ( const candidates = [paragraphIdx, paragraphIdx + 1];
paragraphIdx >= minChars && for (const candidate of candidates) {
isSafeFenceBreak(fenceSpans, paragraphIdx) if (candidate < minChars) continue;
) { if (candidate < 0 || candidate >= buffer.length) continue;
return { index: paragraphIdx }; if (isSafeFenceBreak(fenceSpans, candidate)) {
return { index: candidate };
}
} }
paragraphIdx = buffer.indexOf("\n\n", paragraphIdx + 2); paragraphIdx = buffer.indexOf("\n\n", paragraphIdx + 2);
} }
@@ -183,8 +185,13 @@ export class EmbeddedBlockChunker {
if (preference === "paragraph") { if (preference === "paragraph") {
let paragraphIdx = window.lastIndexOf("\n\n"); let paragraphIdx = window.lastIndexOf("\n\n");
while (paragraphIdx >= minChars) { while (paragraphIdx >= minChars) {
if (isSafeFenceBreak(fenceSpans, paragraphIdx)) { const candidates = [paragraphIdx, paragraphIdx + 1];
return { index: paragraphIdx }; for (const candidate of candidates) {
if (candidate < minChars) continue;
if (candidate < 0 || candidate >= buffer.length) continue;
if (isSafeFenceBreak(fenceSpans, candidate)) {
return { index: candidate };
}
} }
paragraphIdx = window.lastIndexOf("\n\n", paragraphIdx - 1); paragraphIdx = window.lastIndexOf("\n\n", paragraphIdx - 1);
} }

View File

@@ -2,11 +2,21 @@ import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { runEmbeddedPiAgent as runEmbeddedPiAgentRunner } from "/src/agents/pi-embedded.js";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import { loadModelCatalog } from "../agents/model-catalog.js"; import { loadModelCatalog } from "../agents/model-catalog.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent as runEmbeddedPiAgentAutoReply } from "../agents/pi-embedded.js";
import { getReplyFromConfig } from "./reply.js"; import { getReplyFromConfig } from "./reply.js";
vi.mock("/src/agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: vi.fn(),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) =>
`session:${key.trim() || "main"}`,
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
}));
vi.mock("../agents/pi-embedded.js", () => ({ vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false), abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: vi.fn(), runEmbeddedPiAgent: vi.fn(),
@@ -26,7 +36,8 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
describe("block streaming", () => { describe("block streaming", () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(runEmbeddedPiAgentAutoReply).mockReset();
vi.mocked(runEmbeddedPiAgentRunner).mockReset();
vi.mocked(loadModelCatalog).mockResolvedValue([ vi.mocked(loadModelCatalog).mockResolvedValue([
{ id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" }, { id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" }, { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
@@ -52,7 +63,9 @@ describe("block streaming", () => {
const onReplyStart = vi.fn(() => typingGate); const onReplyStart = vi.fn(() => typingGate);
const onBlockReply = vi.fn().mockResolvedValue(undefined); const onBlockReply = vi.fn().mockResolvedValue(undefined);
vi.mocked(runEmbeddedPiAgent).mockImplementation(async (params) => { const impl = async (
params: Parameters<typeof runEmbeddedPiAgentRunner>[0],
) => {
void params.onBlockReply?.({ text: "hello" }); void params.onBlockReply?.({ text: "hello" });
return { return {
payloads: [{ text: "hello" }], payloads: [{ text: "hello" }],
@@ -61,7 +74,9 @@ describe("block streaming", () => {
agentMeta: { sessionId: "s", provider: "p", model: "m" }, agentMeta: { sessionId: "s", provider: "p", model: "m" },
}, },
}; };
}); };
vi.mocked(runEmbeddedPiAgentAutoReply).mockImplementation(impl);
vi.mocked(runEmbeddedPiAgentRunner).mockImplementation(impl);
const replyPromise = getReplyFromConfig( const replyPromise = getReplyFromConfig(
{ {
@@ -109,7 +124,9 @@ describe("block streaming", () => {
seen.push(payload.text ?? ""); seen.push(payload.text ?? "");
}); });
vi.mocked(runEmbeddedPiAgent).mockImplementation(async (params) => { const impl = async (
params: Parameters<typeof runEmbeddedPiAgentRunner>[0],
) => {
void params.onBlockReply?.({ text: "first" }); void params.onBlockReply?.({ text: "first" });
void params.onBlockReply?.({ text: "second" }); void params.onBlockReply?.({ text: "second" });
return { return {
@@ -119,7 +136,9 @@ describe("block streaming", () => {
agentMeta: { sessionId: "s", provider: "p", model: "m" }, agentMeta: { sessionId: "s", provider: "p", model: "m" },
}, },
}; };
}); };
vi.mocked(runEmbeddedPiAgentAutoReply).mockImplementation(impl);
vi.mocked(runEmbeddedPiAgentRunner).mockImplementation(impl);
const replyPromise = getReplyFromConfig( const replyPromise = getReplyFromConfig(
{ {
@@ -159,7 +178,9 @@ describe("block streaming", () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
const onBlockReply = vi.fn().mockResolvedValue(undefined); const onBlockReply = vi.fn().mockResolvedValue(undefined);
vi.mocked(runEmbeddedPiAgent).mockImplementation(async (params) => { const impl = async (
params: Parameters<typeof runEmbeddedPiAgentRunner>[0],
) => {
void params.onBlockReply?.({ text: "chunk-1" }); void params.onBlockReply?.({ text: "chunk-1" });
return { return {
payloads: [{ text: "chunk-1\nchunk-2" }], payloads: [{ text: "chunk-1\nchunk-2" }],
@@ -168,7 +189,9 @@ describe("block streaming", () => {
agentMeta: { sessionId: "s", provider: "p", model: "m" }, agentMeta: { sessionId: "s", provider: "p", model: "m" },
}, },
}; };
}); };
vi.mocked(runEmbeddedPiAgentAutoReply).mockImplementation(impl);
vi.mocked(runEmbeddedPiAgentRunner).mockImplementation(impl);
const res = await getReplyFromConfig( const res = await getReplyFromConfig(
{ {
@@ -215,7 +238,9 @@ describe("block streaming", () => {
}); });
}); });
vi.mocked(runEmbeddedPiAgent).mockImplementation(async (params) => { const impl = async (
params: Parameters<typeof runEmbeddedPiAgentRunner>[0],
) => {
void params.onBlockReply?.({ text: "streamed" }); void params.onBlockReply?.({ text: "streamed" });
return { return {
payloads: [{ text: "final" }], payloads: [{ text: "final" }],
@@ -224,7 +249,9 @@ describe("block streaming", () => {
agentMeta: { sessionId: "s", provider: "p", model: "m" }, agentMeta: { sessionId: "s", provider: "p", model: "m" },
}, },
}; };
}); };
vi.mocked(runEmbeddedPiAgentAutoReply).mockImplementation(impl);
vi.mocked(runEmbeddedPiAgentRunner).mockImplementation(impl);
const replyPromise = getReplyFromConfig( const replyPromise = getReplyFromConfig(
{ {