feat: upgrade investigation and presentation runtime
Generic Container CI/CD / test-build-publish (push) Successful in 1m6s
Agent CI/CD / build-test-publish-and-deploy (push) Successful in 1m7s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent 3b52a0ddab
commit 6336157ff4
104 changed files with 6541 additions and 961 deletions
+63 -62
View File
@@ -1,17 +1,12 @@
import { smoothStream, type TextStreamPart } from "ai";
type TokenSmootherOptions = {
enabled: boolean;
delayMs: number;
locale: string;
maxWaitMs?: number;
sessionId: string;
write: (event: string, data: Record<string, unknown>) => void;
};
type SmoothTextStreamPart = TextStreamPart<Record<string, never>>;
type SmoothTextDeltaPart = Extract<SmoothTextStreamPart, { type: "text-delta" }>;
type SmoothTextEndPart = Extract<SmoothTextStreamPart, { type: "text-end" }>;
const segmenters = new Map<string, Intl.Segmenter | null>();
const CJK_SCRIPT_PATTERN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
const CJK_PUNCTUATION_PATTERN = /[。!?!?,、;:,;:]/u;
@@ -94,6 +89,7 @@ export function createTokenSmoother({
enabled,
delayMs,
locale,
maxWaitMs = 100,
sessionId,
write,
}: TokenSmootherOptions) {
@@ -104,69 +100,74 @@ export function createTokenSmoother({
});
};
if (!enabled) {
return {
writeToken(content: string) {
if (content) {
emitChunk(content);
}
},
async flush() {
return;
},
};
let buffer = "";
let bufferedAt = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
let scheduledAt = 0;
let closed = false;
let releasedChars = 0;
const barriers: Array<{ target: number; resolve: () => void }> = [];
function clearTimer() {
if (timer !== undefined) clearTimeout(timer);
timer = undefined;
}
const textPartId = `smooth-text-${sessionId}`;
const stream = smoothStream<Record<string, never>>({
delayInMs: delayMs,
chunking: (buffer) => detectTokenSmoothingChunk(buffer, locale),
})({ tools: {} });
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
let closed = false;
let writeChain = Promise.resolve();
const readPromise = (async () => {
while (true) {
const result = await reader.read();
if (result.done) {
return;
}
if (result.value.type === "text-delta") {
emitChunk(result.value.text);
}
}
})();
function release(content: string) {
buffer = buffer.slice(content.length);
// The short-tail deadline starts at the last visible output. It does not
// force a whole paragraph out while regular chunks are still available.
bufferedAt = buffer ? Date.now() : 0;
emitChunk(content);
releasedChars += content.length;
while (barriers.length && barriers[0].target <= releasedChars) barriers.shift()!.resolve();
}
function schedule() {
if (!buffer) return;
const now = Date.now();
const remaining = Math.max(0, maxWaitMs - (now - bufferedAt));
const wait = detectTokenSmoothingChunk(buffer, locale) || barriers.length ? delayMs : remaining;
// More input may make a chunk ready sooner, but must never postpone an
// already scheduled release. A stage boundary uses the same pacing.
if (timer !== undefined && scheduledAt <= now + wait) return;
clearTimer();
scheduledAt = now + wait;
timer = setTimeout(() => {
timer = undefined;
const chunk = detectTokenSmoothingChunk(buffer, locale) ?? buffer;
const boundary = barriers[0]?.target;
// Later narration must not be merged across a pending UI/tool boundary.
release(boundary === undefined ? chunk : chunk.slice(0, boundary - releasedChars));
schedule();
}, wait);
}
async function flush() {
if (!buffer) return;
const target = releasedChars + buffer.length;
await new Promise<void>((resolve) => {
barriers.push({ target, resolve });
schedule();
});
}
return {
writeToken(content: string) {
if (!content) {
return;
}
if (closed) {
return;
}
writeChain = writeChain.then(() =>
writer.write({
type: "text-delta",
id: textPartId,
text: content,
} satisfies SmoothTextDeltaPart),
);
if (!content || closed) return;
if (!enabled) { emitChunk(content); return; }
if (!buffer) bufferedAt = Date.now();
buffer += content;
schedule();
},
async flush() {
if (closed) {
await readPromise;
return;
}
// Stage boundaries drain narration without ending future text output.
flush,
async close({ immediate = false }: { immediate?: boolean } = {}) {
closed = true;
await writeChain;
await writer.write({
type: "text-end",
id: textPartId,
} satisfies SmoothTextEndPart);
await writer.close();
await readPromise;
if (immediate) {
clearTimer();
if (buffer) release(buffer);
} else await flush();
},
};
}