fix(agent): finalize block chunking
This commit is contained in:
@@ -284,6 +284,50 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
const isSafeBreak = (spans: FenceSpan[], index: number): boolean =>
|
const isSafeBreak = (spans: FenceSpan[], index: number): boolean =>
|
||||||
!findFenceSpanAt(spans, index);
|
!findFenceSpanAt(spans, index);
|
||||||
|
|
||||||
|
const pickSoftBreakIndex = (buffer: string): BreakResult => {
|
||||||
|
if (!blockChunking) return { index: -1 };
|
||||||
|
const minChars = Math.max(1, Math.floor(blockChunking.minChars));
|
||||||
|
if (buffer.length < minChars) return { index: -1 };
|
||||||
|
const fenceSpans = parseFenceSpans(buffer);
|
||||||
|
const preference = blockChunking.breakPreference ?? "paragraph";
|
||||||
|
|
||||||
|
if (preference === "paragraph") {
|
||||||
|
let paragraphIdx = buffer.lastIndexOf("\n\n");
|
||||||
|
while (paragraphIdx >= minChars) {
|
||||||
|
if (isSafeBreak(fenceSpans, paragraphIdx)) {
|
||||||
|
return { index: paragraphIdx };
|
||||||
|
}
|
||||||
|
paragraphIdx = buffer.lastIndexOf("\n\n", paragraphIdx - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference === "paragraph" || preference === "newline") {
|
||||||
|
let newlineIdx = buffer.lastIndexOf("\n");
|
||||||
|
while (newlineIdx >= minChars) {
|
||||||
|
if (isSafeBreak(fenceSpans, newlineIdx)) {
|
||||||
|
return { index: newlineIdx };
|
||||||
|
}
|
||||||
|
newlineIdx = buffer.lastIndexOf("\n", newlineIdx - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference !== "newline") {
|
||||||
|
const matches = buffer.matchAll(/[.!?](?=\s|$)/g);
|
||||||
|
let sentenceIdx = -1;
|
||||||
|
for (const match of matches) {
|
||||||
|
const at = match.index ?? -1;
|
||||||
|
if (at < minChars) continue;
|
||||||
|
const candidate = at + 1;
|
||||||
|
if (isSafeBreak(fenceSpans, candidate)) {
|
||||||
|
sentenceIdx = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sentenceIdx >= minChars) return { index: sentenceIdx };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { index: -1 };
|
||||||
|
};
|
||||||
|
|
||||||
const pickBreakIndex = (buffer: string): BreakResult => {
|
const pickBreakIndex = (buffer: string): BreakResult => {
|
||||||
if (!blockChunking) return { index: -1 };
|
if (!blockChunking) return { index: -1 };
|
||||||
const minChars = Math.max(1, Math.floor(blockChunking.minChars));
|
const minChars = Math.max(1, Math.floor(blockChunking.minChars));
|
||||||
@@ -371,18 +415,15 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
if (!blockChunking) return;
|
if (!blockChunking) return;
|
||||||
const minChars = Math.max(1, Math.floor(blockChunking.minChars));
|
const minChars = Math.max(1, Math.floor(blockChunking.minChars));
|
||||||
const maxChars = Math.max(minChars, Math.floor(blockChunking.maxChars));
|
const maxChars = Math.max(minChars, Math.floor(blockChunking.maxChars));
|
||||||
// Force flush small remainders as a single chunk to avoid re-splitting.
|
|
||||||
if (force && blockBuffer.length > 0 && blockBuffer.length <= maxChars) {
|
|
||||||
emitBlockChunk(blockBuffer);
|
|
||||||
blockBuffer = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (blockBuffer.length < minChars && !force) return;
|
if (blockBuffer.length < minChars && !force) return;
|
||||||
while (
|
while (
|
||||||
blockBuffer.length >= minChars ||
|
blockBuffer.length >= minChars ||
|
||||||
(force && blockBuffer.length > 0)
|
(force && blockBuffer.length > 0)
|
||||||
) {
|
) {
|
||||||
const breakResult = pickBreakIndex(blockBuffer);
|
const breakResult =
|
||||||
|
force && blockBuffer.length <= maxChars
|
||||||
|
? pickSoftBreakIndex(blockBuffer)
|
||||||
|
: pickBreakIndex(blockBuffer);
|
||||||
if (breakResult.index <= 0) {
|
if (breakResult.index <= 0) {
|
||||||
if (force) {
|
if (force) {
|
||||||
emitBlockChunk(blockBuffer);
|
emitBlockChunk(blockBuffer);
|
||||||
@@ -643,7 +684,11 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.onBlockReply && blockChunking) {
|
if (
|
||||||
|
params.onBlockReply &&
|
||||||
|
blockChunking &&
|
||||||
|
blockReplyBreak === "text_end"
|
||||||
|
) {
|
||||||
drainBlockBuffer(false);
|
drainBlockBuffer(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,7 +723,8 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
|
|
||||||
const addedDuringMessage =
|
const addedDuringMessage =
|
||||||
assistantTexts.length > assistantTextBaseline;
|
assistantTexts.length > assistantTextBaseline;
|
||||||
if (!addedDuringMessage && text) {
|
const chunkingEnabled = Boolean(blockChunking);
|
||||||
|
if (!chunkingEnabled && !addedDuringMessage && text) {
|
||||||
const last = assistantTexts.at(-1);
|
const last = assistantTexts.at(-1);
|
||||||
if (!last || last !== text) assistantTexts.push(text);
|
if (!last || last !== text) assistantTexts.push(text);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user