fix: handle fence-close paragraph breaks
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user