Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/stream-mapping.ts
T
Jesse_Chen 47b4b06bfb
Independent Staging Quality Gate / validate (push) Successful in 12m42s
Independent Staging Quality Gate / publish (push) Successful in 22m34s
fix(rectification): surface pricing failures and persist opening question
2026-08-31 15:16:23 +08:00

366 lines
13 KiB
TypeScript

/**
* V9 fullStream → public NDJSON mapping.
*
* Consumes the Mastra agent's fullStream (AgentChunkType) and emits only the
* allowlisted public phases from public-receipt.ts. Raw provider reasoning
* payloads, tool args/results, system/skill source text, birth data and
* internal errors are dropped at this boundary. Chain-of-thought is never
* forwarded to the browser.
*/
import type { AgentChunkType } from "@mastra/core/stream";
import {
activityForRectificationTool,
isPublicRectificationActivity,
isPublicRectificationMethod,
isPublicRectificationTool,
isRectificationActivityStatus,
safeActivityEvent,
type RectificationActivityChangedEvent,
type RectificationActivityEvent,
type RectificationChoiceAppliedEvent,
type PublicRectificationMethod,
type PublicRectificationPhase,
type PublicRectificationTool,
} from "./public-receipt";
import { sanitizePublicThinkingText } from "../../public-thinking";
export type PublicPhaseStreamEvent = Readonly<{
type: PublicRectificationPhase;
text?: string;
replace?: true;
tool?: PublicRectificationTool;
methods?: readonly PublicRectificationMethod[];
turnId?: string;
}>;
export type PublicErrorCode =
| "billing_denied"
| "billing_unavailable"
| "skill_identity_unverifiable"
| "skill_identity_missing"
| "skill_identity_mismatch"
| "answer_truncated"
| "run_timeout"
| "max_steps"
| "provider_error"
| "run_failed";
export type PublicErrorStreamEvent = Readonly<{
type: "error";
code: PublicErrorCode;
message: string;
}>;
export type PublicFailedStreamEvent = Readonly<{
type: "run.failed";
code?: string;
recoverable?: boolean;
message?: string;
}>;
export type PublicStreamEvent =
| PublicPhaseStreamEvent
| RectificationActivityEvent
| RectificationActivityChangedEvent
| RectificationChoiceAppliedEvent
| PublicErrorStreamEvent
| PublicFailedStreamEvent;
export type StreamObservation = Readonly<{
phases: readonly PublicRectificationPhase[];
toolsUsed: readonly PublicRectificationTool[];
skillLoaded: boolean;
answerText: string;
errored: boolean;
aborted: boolean;
finished: boolean;
}>;
const TOOL_PHASE_ON_CALL: Readonly<Partial<Record<PublicRectificationTool, PublicRectificationPhase>>> = {
"rectification-compare-candidates": "candidates.comparing",
};
const TOOL_PHASE_ON_RESULT: Readonly<Record<PublicRectificationTool, PublicRectificationPhase | null>> = {
"rectification-read-case": "case.loaded",
"rectification-set-focus": "intent.classified",
"rectification-resolve-focus": "intent.classified",
"rectification-record-evidence-batch": "evidence.proposed",
"rectification-propose-evidence": "evidence.proposed",
"rectification-confirm-evidence": "evidence.confirmed",
"rectification-revise-evidence": "evidence.proposed",
"rectification-compare-candidates": "candidates.updated",
"rectification-read-diagnostics": "diagnostics.completed",
"rectification-offer-candidates": "candidates.updated",
"rectification-accept-candidate": "candidate.accepted",
"rectification-confirm-birth-time": "birth_time.confirmed",
"rectification-stop-and-review": "intent.classified",
// Closing a Case has no truthful existing semantic phase. The tool.activity
// event remains visible, while completion is still owned by the runner.
"rectification-close-case": null,
};
const METHOD_TOOLS = new Set<PublicRectificationTool>([
"rectification-compare-candidates",
"rectification-read-diagnostics",
]);
export function isPublicRectificationToolName(value: unknown): value is PublicRectificationTool {
return isPublicRectificationTool(value);
}
function executedMethodsFromRecord(value: unknown): PublicRectificationMethod[] {
if (!value || typeof value !== "object") return [];
const record = value as Record<string, unknown>;
const found: PublicRectificationMethod[] = [];
const add = (methods: unknown) => {
if (!Array.isArray(methods)) return;
for (const method of methods) {
if (isPublicRectificationMethod(method) && !found.includes(method)) found.push(method);
}
};
add(record.executed_methods);
if (record.rescore && typeof record.rescore === "object") {
add((record.rescore as Record<string, unknown>).executed_methods);
}
return found;
}
function resultMethods(chunk: AgentChunkType): PublicRectificationMethod[] {
if (chunk.type !== "tool-result") return [];
const payload = chunk.payload && typeof chunk.payload === "object"
? chunk.payload as unknown as Record<string, unknown>
: {};
const candidates = [payload.result, payload.output, (chunk as unknown as { object?: unknown }).object];
for (const candidate of candidates) {
const methods = executedMethodsFromRecord(candidate);
if (methods.length > 0) return methods;
}
return [];
}
/**
* Map a single fullStream chunk to a public phase (or null to drop). The
* skill tool is the framework's auditable skill loader. Only its successful
* tool-result proves that the Case-bound package was bound for this attempt.
*/
export function mapStreamChunkToPhase(chunk: AgentChunkType): PublicPhaseStreamEvent | null {
switch (chunk.type) {
case "start":
return null;
case "tool-call": {
const toolName = typeof chunk.payload?.toolName === "string" ? chunk.payload.toolName : "";
if (toolName === "skill") return null;
if (!isPublicRectificationTool(toolName)) return null;
const phase = TOOL_PHASE_ON_CALL[toolName];
return phase ? { type: phase, tool: toolName } : null;
}
case "tool-result": {
const toolName = typeof chunk.payload?.toolName === "string" ? chunk.payload.toolName : "";
if (toolName === "skill") return { type: "skill.bound" };
if (!isPublicRectificationTool(toolName)) return null;
const phase = TOOL_PHASE_ON_RESULT[toolName];
const methods = METHOD_TOOLS.has(toolName) ? resultMethods(chunk) : [];
return phase ? { type: phase, tool: toolName, ...(methods.length > 0 ? { methods } : {}) } : null;
}
case "text-delta":
return null;
case "finish":
// Completion is decided by the runner after the skill/first-turn gates;
// a finish chunk alone never proves a settled answer.
return null;
case "error":
case "abort":
// Failure is decided by the runner so receipts stay accurate.
return null;
default:
// Raw reasoning, payloads, step internals and provider metadata stay off
// the answer channel. Chinese thinking is mapped separately.
return null;
}
}
export type InternalThinkingDeltaEvent = Readonly<{
type: "thinking.delta";
text: string;
}>;
export function toPublicThinkingDelta(text: string): InternalThinkingDeltaEvent | null {
const cleaned = sanitizePublicThinkingText(text);
if (!cleaned) return null;
return { type: "thinking.delta", text: cleaned };
}
export function mapStreamChunkToThinking(chunk: AgentChunkType): InternalThinkingDeltaEvent | null {
if (chunk.type !== "reasoning-delta") return null;
const text = typeof chunk.payload?.text === "string" ? chunk.payload.text : "";
return toPublicThinkingDelta(text);
}
/**
* Project real public tool lifecycle events for the live UI. This stream is
* deliberately separate from the durable phase receipt: it never exposes
* args, results, provider errors, scores, birth data or permission flags.
*/
const SAFE_TOOL_ACTIVITY_CODES = new Set([
"duplicate_focus",
"probe_already_answered",
"stale_revision",
"quote_mismatch",
"quote_not_grounded",
"zero_information_gain",
"invalid_tool_input",
"invalid_choice_copy",
"already_exists",
"focus_idempotency_conflict",
"invalid_focus",
]);
function safeToolActivityCode(error: unknown): string | undefined {
const message = error instanceof Error ? error.message : String(error ?? "");
for (const code of SAFE_TOOL_ACTIVITY_CODES) {
if (message.includes(code)) {
return code === "focus_idempotency_conflict" || code === "invalid_focus"
? "duplicate_focus"
: code === "quote_not_grounded"
? "quote_mismatch"
: code;
}
}
return undefined;
}
export function mapStreamChunkToActivity(chunk: AgentChunkType): RectificationActivityEvent | null {
if (chunk.type !== "tool-call" && chunk.type !== "tool-result" && chunk.type !== "tool-error") {
return null;
}
const toolName = typeof chunk.payload?.toolName === "string" ? chunk.payload.toolName : "";
if (!isPublicRectificationTool(toolName)) return null;
if (chunk.type === "tool-call") {
return { type: "tool.activity", tool: toolName, status: "started" };
}
if (chunk.type === "tool-error") {
const code = safeToolActivityCode(chunk.payload?.error);
return {
type: "tool.activity",
tool: toolName,
status: "failed",
...(code ? { code } : {}),
};
}
const methods = resultMethods(chunk);
return {
type: "tool.activity",
tool: toolName,
status: "completed",
...(methods.length > 0 ? { methods } : {}),
};
}
export function streamToolNames(chunk: AgentChunkType): PublicRectificationTool[] {
if (chunk.type !== "tool-call" && chunk.type !== "tool-result") return [];
const toolName = typeof chunk.payload?.toolName === "string" ? chunk.payload.toolName : "";
return isPublicRectificationToolName(toolName) ? [toolName] : [];
}
/** Safe activity event for the web client; drops anything not allowlisted. */
export function safePublicEvent(value: unknown): PublicStreamEvent | null {
if (!value || typeof value !== "object") return null;
const event = value as {
type?: unknown;
status?: unknown;
text?: unknown;
replace?: unknown;
tool?: unknown;
methods?: unknown;
turnId?: unknown;
code?: unknown;
message?: unknown;
activity?: unknown;
questionId?: unknown;
recoverable?: unknown;
origin?: unknown;
};
if (event.type === "thinking.delta") return null;
if (event.type === "error") {
const codes = new Set<PublicErrorCode>([
"billing_denied",
"billing_unavailable",
"skill_identity_unverifiable",
"skill_identity_missing",
"skill_identity_mismatch",
"answer_truncated",
"run_timeout",
"max_steps",
"provider_error",
"run_failed",
]);
if (!codes.has(event.code as PublicErrorCode) || typeof event.message !== "string") return null;
return {
type: "error",
code: event.code as PublicErrorCode,
message: event.message.slice(0, 500),
};
}
if (event.type === "tool.activity") {
if (!isPublicRectificationTool(event.tool) || !isRectificationActivityStatus(event.status)) return null;
const methods = event.status === "completed" && Array.isArray(event.methods)
? [...new Set(event.methods.filter(isPublicRectificationMethod))]
: [];
const code = typeof event.code === "string" && SAFE_TOOL_ACTIVITY_CODES.has(event.code)
? event.code
: undefined;
return {
type: "tool.activity",
tool: event.tool,
status: event.status,
...(methods.length > 0 ? { methods } : {}),
...(code ? { code } : {}),
};
}
if (event.type === "activity.changed") {
if (!isPublicRectificationActivity(event.activity)) return null;
return { type: "activity.changed", activity: event.activity };
}
if (event.type === "choice.applied") {
if (typeof event.questionId !== "string" || !event.questionId.trim()) return null;
return { type: "choice.applied", questionId: event.questionId.slice(0, 200) };
}
if (event.type === "run.failed") {
return {
type: "run.failed",
...(typeof event.code === "string" ? { code: event.code.slice(0, 80) } : {}),
...(typeof event.message === "string" ? { message: event.message.slice(0, 500) } : {}),
recoverable: event.recoverable === true,
};
}
const type = safeActivityEvent(event.type);
if (!type || type === "activity.changed" || type === "choice.applied") return null;
const text = type === "answer.delta" && typeof event.text === "string"
? event.text.slice(0, 4_000)
: undefined;
const replace = type === "answer.delta" && event.replace === true ? true as const : undefined;
const tool = type !== "answer.delta" && isPublicRectificationTool(event.tool)
? event.tool
: undefined;
const methods = tool && METHOD_TOOLS.has(tool) && Array.isArray(event.methods)
? [...new Set(event.methods.filter(isPublicRectificationMethod))]
: [];
const turnId = type === "run.completed"
&& typeof event.turnId === "string"
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(event.turnId)
? event.turnId
: undefined;
return {
type,
...(text !== undefined ? { text } : {}),
...(replace ? { replace } : {}),
...(tool ? { tool } : {}),
...(methods.length > 0 ? { methods } : {}),
...(turnId ? { turnId } : {}),
};
}
export function activityChangedFromTool(tool: PublicRectificationTool): RectificationActivityChangedEvent {
return { type: "activity.changed", activity: activityForRectificationTool(tool) };
}