239 lines
8.9 KiB
TypeScript
239 lines
8.9 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. Reasoning, raw payloads,
|
|
* provider metadata, tool args/results, system/skill source text, birth data
|
|
* and internal errors are dropped at this boundary.
|
|
*/
|
|
import type { AgentChunkType } from "@mastra/core/stream";
|
|
import {
|
|
isPublicRectificationMethod,
|
|
isPublicRectificationTool,
|
|
isRectificationActivityStatus,
|
|
safeActivityEvent,
|
|
type RectificationActivityEvent,
|
|
type PublicRectificationMethod,
|
|
type PublicRectificationPhase,
|
|
type PublicRectificationTool,
|
|
} from "./public-receipt";
|
|
|
|
export type PublicPhaseStreamEvent = Readonly<{
|
|
type: PublicRectificationPhase;
|
|
text?: string;
|
|
tool?: PublicRectificationTool;
|
|
methods?: readonly PublicRectificationMethod[];
|
|
turnId?: string;
|
|
}>;
|
|
|
|
export type PublicErrorCode =
|
|
| "billing_denied"
|
|
| "skill_identity_unverifiable"
|
|
| "skill_identity_missing"
|
|
| "skill_identity_mismatch"
|
|
| "run_failed";
|
|
|
|
export type PublicErrorStreamEvent = Readonly<{
|
|
type: "error";
|
|
code: PublicErrorCode;
|
|
message: string;
|
|
}>;
|
|
|
|
export type PublicStreamEvent = PublicPhaseStreamEvent | RectificationActivityEvent | PublicErrorStreamEvent;
|
|
|
|
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",
|
|
// 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 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) {
|
|
if (!candidate || typeof candidate !== "object") continue;
|
|
const methods = (candidate as Record<string, unknown>).executed_methods;
|
|
if (Array.isArray(methods)) return [...new Set(methods.filter(isPublicRectificationMethod))];
|
|
}
|
|
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": {
|
|
const text = typeof chunk.payload?.text === "string" ? chunk.payload.text : "";
|
|
if (!text) return null;
|
|
return { type: "answer.delta", text };
|
|
}
|
|
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:
|
|
// reasoning-*, raw, step-*, source, file, response-metadata and any
|
|
// future chunk type are never forwarded.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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") {
|
|
return { type: "tool.activity", tool: toolName, status: "failed" };
|
|
}
|
|
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;
|
|
tool?: unknown;
|
|
methods?: unknown;
|
|
turnId?: unknown;
|
|
code?: unknown;
|
|
message?: unknown;
|
|
};
|
|
if (event.type === "error") {
|
|
const codes = new Set<PublicErrorCode>([
|
|
"billing_denied",
|
|
"skill_identity_unverifiable",
|
|
"skill_identity_missing",
|
|
"skill_identity_mismatch",
|
|
"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))]
|
|
: [];
|
|
return {
|
|
type: "tool.activity",
|
|
tool: event.tool,
|
|
status: event.status,
|
|
...(methods.length > 0 ? { methods } : {}),
|
|
};
|
|
}
|
|
const type = safeActivityEvent(event.type);
|
|
if (!type) return null;
|
|
const text = type === "answer.delta" && typeof event.text === "string"
|
|
? event.text.slice(0, 4_000)
|
|
: 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 } : {}),
|
|
...(tool ? { tool } : {}),
|
|
...(methods.length > 0 ? { methods } : {}),
|
|
...(turnId ? { turnId } : {}),
|
|
};
|
|
}
|