Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
import type { MessageBlock } from "@rakazo/contracts";
|
|
import { isToolActivityBlock } from "@rakazo/core";
|
|
|
|
/** Keep mid-turn progress beats short; prefer a few high-signal updates. */
|
|
export const USER_PROGRESS_MESSAGE_MAX_LENGTH = 500;
|
|
|
|
export function clampUserProgressMessage(text: string): string {
|
|
const trimmed = text.trim();
|
|
if (!trimmed) return "";
|
|
if (trimmed.length <= USER_PROGRESS_MESSAGE_MAX_LENGTH) return trimmed;
|
|
return `${trimmed.slice(0, USER_PROGRESS_MESSAGE_MAX_LENGTH - 1).trimEnd()}…`;
|
|
}
|
|
|
|
/**
|
|
* True when the raw text a caller tried to post as a mid-turn progress update
|
|
* would not fit and got cut off by clampUserProgressMessage. Callers should
|
|
* surface this back to the model (it gets no other signal that its message
|
|
* was mangled) rather than reporting a silent success.
|
|
*/
|
|
export function isProgressMessageTruncated(text: string): boolean {
|
|
return text.trim().length > USER_PROGRESS_MESSAGE_MAX_LENGTH;
|
|
}
|
|
|
|
/**
|
|
* Pull narration text out of the in-progress turn segments so it can be posted
|
|
* as a durable mid-turn message. Tool/step blocks stay for the final publish.
|
|
*/
|
|
|
|
/** clientNonce prefix for mid-turn progress messages (reconciler uses this). */
|
|
export const USER_PROGRESS_CLIENT_NONCE_PREFIX = "user-progress:";
|
|
|
|
export function userProgressClientNonce(runId: string, index = 0): string {
|
|
// Include entropy so a resumed executor turn cannot reuse an earlier nonce
|
|
// (threadId + clientNonce is unique).
|
|
return `${USER_PROGRESS_CLIENT_NONCE_PREFIX}${runId}:${index}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
|
|
}
|
|
|
|
export function isUserProgressClientNonce(clientNonce: string | null | undefined): boolean {
|
|
return Boolean(clientNonce?.startsWith(USER_PROGRESS_CLIENT_NONCE_PREFIX));
|
|
}
|
|
|
|
export function extractNarrationText(
|
|
segments: readonly MessageBlock[],
|
|
currentText: string,
|
|
): { text: string; remaining: MessageBlock[] } {
|
|
const parts: string[] = [];
|
|
const remaining: MessageBlock[] = [];
|
|
for (const block of segments) {
|
|
if (block.kind === "text") {
|
|
if (block.text) parts.push(block.text);
|
|
continue;
|
|
}
|
|
remaining.push(block);
|
|
}
|
|
if (currentText) parts.push(currentText);
|
|
return { text: parts.join(""), remaining };
|
|
}
|
|
|
|
/**
|
|
* After mid-turn progress messages were already posted, skip a hollow final
|
|
* message that would only carry hidden tool-activity blocks (or nothing).
|
|
*/
|
|
export function finalBlocksAfterMidTurnProgress(
|
|
blocks: MessageBlock[],
|
|
publishedMidTurn: boolean,
|
|
): MessageBlock[] {
|
|
if (!publishedMidTurn || blocks.length === 0) return blocks;
|
|
if (blocks.every((block) => isToolActivityBlock(block))) return [];
|
|
return blocks;
|
|
}
|
|
|
|
/** Outcome to return for a bot_message run after mid-turn progress posts. */
|
|
export function botMessageOutcomeFromMidTurn(
|
|
finalText: string,
|
|
midTurnTexts: readonly string[],
|
|
): { text: string; intent: "result" | "status" } | null {
|
|
const trimmed = finalText.trim();
|
|
if (trimmed) return { text: trimmed, intent: "result" };
|
|
const midTurn = midTurnTexts.map((part) => part.trim()).filter(Boolean);
|
|
if (midTurn.length === 0) return null;
|
|
return { text: midTurn.join("\n\n"), intent: "status" };
|
|
}
|