Files
pikasTech-HWLAB/internal/cloud/code-agent-trace-store.ts
T
2026-06-01 17:06:11 +08:00

555 lines
21 KiB
TypeScript

import { randomUUID } from "node:crypto";
const DEFAULT_MAX_TRACES = 256;
const DEFAULT_MAX_EVENTS = 6000;
const TEXT_LIMIT = 1200;
const SECRET_PATTERN = /\b(?:sk-[A-Za-z0-9_-]+|OPENAI_API_KEY|DATABASE_URL|secretRef:[^\s,;]+|secret|token|password|passwd|credential|private[_ -]?key|kubeconfig|BEGIN [A-Z ]*PRIVATE KEY)\b/giu;
export function createCodeAgentTraceStore(options = {}) {
const maxTraces = positiveInteger(options.maxTraces, DEFAULT_MAX_TRACES);
const maxEvents = positiveInteger(options.maxEvents, DEFAULT_MAX_EVENTS);
const traces = new Map();
function ensure(traceId, meta = {}) {
const id = cleanTraceId(traceId) || `trc_${randomUUID()}`;
let trace = traces.get(id);
if (!trace) {
const timestamp = timestampFor(meta.now);
trace = {
traceId: id,
status: "running",
createdAt: timestamp,
updatedAt: timestamp,
startedAt: timestamp,
finishedAt: null,
nextSeq: 1,
events: [],
assistantStreams: new Map(),
listeners: new Set(),
meta: {
runnerKind: meta.runnerKind ?? null,
workspace: meta.workspace ?? null,
sandbox: meta.sandbox ?? null,
sessionMode: meta.sessionMode ?? null,
implementationType: meta.implementationType ?? null
}
};
traces.set(id, trace);
prune();
} else {
trace.meta = {
...trace.meta,
...dropEmpty({
runnerKind: meta.runnerKind,
workspace: meta.workspace,
sandbox: meta.sandbox,
sessionMode: meta.sessionMode,
implementationType: meta.implementationType
})
};
}
return trace;
}
function append(traceId, event = {}, meta = {}) {
const trace = ensure(traceId, meta);
const normalized = normalizeTraceEvent(event, {
traceId: trace.traceId,
seq: trace.nextSeq,
now: meta.now,
fallbackRunnerKind: trace.meta.runnerKind
});
trace.nextSeq += 1;
trace.events.push(normalized);
if (trace.events.length > maxEvents) {
trace.events.splice(0, trace.events.length - maxEvents);
}
trace.updatedAt = normalized.createdAt;
if (normalized.terminal === true) {
trace.status = normalized.status === "completed"
? "completed"
: normalized.status === "canceled"
? "canceled"
: normalized.type;
trace.finishedAt = normalized.createdAt;
} else if (normalized.status === "failed" || normalized.type === "error" || normalized.type === "timeout") {
trace.status = normalized.type;
trace.finishedAt = normalized.createdAt;
}
notify(trace, normalized);
return normalized;
}
function appendAssistantDelta(traceId, delta = {}, meta = {}) {
const trace = ensure(traceId, meta);
const normalized = normalizeAssistantDelta(delta, {
traceId: trace.traceId,
now: meta.now,
fallbackRunnerKind: trace.meta.runnerKind
});
if (!normalized) return null;
const key = normalized.itemId || "assistant-message";
const previous = trace.assistantStreams.get(key) ?? {
traceId: trace.traceId,
itemId: key,
type: "assistant_message_stream",
status: "streaming",
label: "assistant:stream",
createdAt: normalized.createdAt,
updatedAt: normalized.createdAt,
runnerKind: normalized.runnerKind,
sessionId: normalized.sessionId,
sessionStatus: normalized.sessionStatus,
turn: normalized.turn,
threadId: normalized.threadId,
turnId: normalized.turnId,
waitingFor: normalized.waitingFor,
chunkCount: 0,
text: "",
lastChunk: "",
valuesPrinted: false
};
const text = `${previous.text ?? ""}${normalized.chunk}`;
const stream = dropUndefined({
...previous,
updatedAt: normalized.createdAt,
runnerKind: normalized.runnerKind ?? previous.runnerKind,
sessionId: normalized.sessionId ?? previous.sessionId,
sessionStatus: normalized.sessionStatus ?? previous.sessionStatus,
turn: normalized.turn ?? previous.turn,
threadId: normalized.threadId ?? previous.threadId,
turnId: normalized.turnId ?? previous.turnId,
waitingFor: normalized.waitingFor ?? previous.waitingFor,
chunkCount: previous.chunkCount + 1,
text,
lastChunk: normalized.chunk,
valuesPrinted: false
});
trace.assistantStreams.set(key, stream);
trace.updatedAt = normalized.createdAt;
notify(trace, null);
return stream;
}
function snapshot(traceId, extra = {}) {
const trace = traces.get(cleanTraceId(traceId));
if (!trace) return emptySnapshot(traceId, extra);
const events = trace.events.map((event) => ({ ...event }));
const assistantStreams = assistantStreamsSnapshot(trace);
const lastEvent = events.at(-1) ?? null;
return {
traceId: trace.traceId,
status: trace.status,
createdAt: trace.createdAt,
updatedAt: trace.updatedAt,
startedAt: trace.startedAt,
finishedAt: trace.finishedAt,
eventCount: events.length,
events,
assistantStreams,
eventLabels: events.map((event) => event.label).filter(Boolean),
lastEvent,
elapsedMs: elapsedMs(trace.startedAt, trace.finishedAt ?? trace.updatedAt),
waitingFor: snapshotWaitingFor(trace, events, assistantStreams, lastEvent),
runnerKind: extra.runnerKind ?? trace.meta.runnerKind ?? null,
workspace: extra.workspace ?? trace.meta.workspace ?? null,
sandbox: extra.sandbox ?? trace.meta.sandbox ?? null,
sessionMode: extra.sessionMode ?? trace.meta.sessionMode ?? null,
implementationType: extra.implementationType ?? trace.meta.implementationType ?? null,
sessionId: extra.sessionId ?? lastEvent?.sessionId ?? null,
sessionStatus: extra.sessionStatus ?? lastEvent?.sessionStatus ?? null,
sessionLifecycleStatus: extra.sessionLifecycleStatus ?? lastEvent?.sessionLifecycleStatus ?? null,
turn: extra.turn ?? lastEvent?.turn ?? null,
outputTruncated: events.some((event) => event.outputTruncated === true),
valuesPrinted: false
};
}
function subscribe(traceId, listener, meta = {}) {
const trace = ensure(traceId, meta);
trace.listeners.add(listener);
return () => {
trace.listeners.delete(listener);
};
}
function clear() {
traces.clear();
}
function prune() {
if (traces.size <= maxTraces) return;
const stale = [...traces.values()]
.sort((left, right) => String(left.updatedAt).localeCompare(String(right.updatedAt)))
.slice(0, traces.size - maxTraces);
for (const trace of stale) {
traces.delete(trace.traceId);
}
}
return {
ensure,
append,
appendAssistantDelta,
snapshot,
subscribe,
clear
};
}
export const defaultCodeAgentTraceStore = createCodeAgentTraceStore();
export function createCodeAgentTraceRecorder({
traceStore = defaultCodeAgentTraceStore,
traceId,
now,
runnerKind = null,
workspace = null,
sandbox = null,
sessionMode = null,
implementationType = null
} = {}) {
const startedAt = timestampFor(now);
const startedEpochMs = Date.now();
const baseMeta = { now, runnerKind, workspace, sandbox, sessionMode, implementationType };
traceStore.ensure(traceId, baseMeta);
function append(event = {}) {
return traceStore.append(traceId, {
elapsedMs: Date.now() - startedEpochMs,
...event
}, baseMeta);
}
function appendAssistantDelta(delta = {}) {
return traceStore.appendAssistantDelta(traceId, delta, baseMeta);
}
function snapshot(extra = {}) {
return traceStore.snapshot(traceId, { runnerKind, workspace, sandbox, sessionMode, implementationType, ...extra });
}
function runnerTrace(extra = {}) {
const current = snapshot(extra);
return {
traceId: current.traceId,
runnerKind: extra.runnerKind ?? current.runnerKind,
workspace: extra.workspace ?? current.workspace,
sandbox: extra.sandbox ?? current.sandbox,
sessionMode: extra.sessionMode ?? current.sessionMode,
sessionId: extra.sessionId ?? current.sessionId,
sessionStatus: extra.sessionStatus ?? current.sessionStatus,
sessionLifecycleStatus: extra.sessionLifecycleStatus ?? current.sessionLifecycleStatus,
idleTimeoutMs: extra.idleTimeoutMs ?? null,
lastTraceId: extra.lastTraceId ?? current.traceId,
turn: extra.turn ?? current.turn,
sessionReused: extra.sessionReused ?? false,
implementationType: extra.implementationType ?? current.implementationType,
limitations: Array.isArray(extra.limitations) ? extra.limitations : [],
startedAt: extra.startedAt ?? startedAt,
finishedAt: extra.finishedAt ?? current.finishedAt ?? timestampFor(now),
updatedAt: current.updatedAt,
events: current.events,
assistantStreams: current.assistantStreams,
eventLabels: current.eventLabels,
lastEvent: current.lastEvent,
elapsedMs: current.elapsedMs,
waitingFor: current.waitingFor,
outputTruncated: current.outputTruncated || extra.outputTruncated === true,
valuesPrinted: false,
note: extra.note ?? "Real-time runnerTrace is appended as Codex stdio/session events are captured."
};
}
return {
traceId,
append,
appendAssistantDelta,
snapshot,
runnerTrace
};
}
export function runnerTraceFromSnapshot(snapshot = {}, extra = {}) {
return {
traceId: snapshot.traceId ?? extra.traceId ?? null,
runnerKind: extra.runnerKind ?? snapshot.runnerKind ?? null,
workspace: extra.workspace ?? snapshot.workspace ?? null,
sandbox: extra.sandbox ?? snapshot.sandbox ?? null,
sessionMode: extra.sessionMode ?? snapshot.sessionMode ?? null,
sessionId: extra.sessionId ?? snapshot.sessionId ?? null,
sessionStatus: extra.sessionStatus ?? snapshot.sessionStatus ?? null,
sessionLifecycleStatus: extra.sessionLifecycleStatus ?? snapshot.sessionLifecycleStatus ?? null,
idleTimeoutMs: extra.idleTimeoutMs ?? null,
lastTraceId: extra.lastTraceId ?? snapshot.traceId ?? null,
turn: extra.turn ?? snapshot.turn ?? null,
sessionReused: extra.sessionReused ?? false,
implementationType: extra.implementationType ?? snapshot.implementationType ?? null,
limitations: Array.isArray(extra.limitations) ? extra.limitations : [],
startedAt: extra.startedAt ?? snapshot.startedAt ?? null,
finishedAt: extra.finishedAt ?? snapshot.finishedAt ?? snapshot.updatedAt ?? null,
updatedAt: snapshot.updatedAt ?? null,
events: Array.isArray(snapshot.events) ? snapshot.events : [],
assistantStreams: Array.isArray(snapshot.assistantStreams) ? snapshot.assistantStreams : [],
eventLabels: Array.isArray(snapshot.eventLabels) ? snapshot.eventLabels : [],
lastEvent: snapshot.lastEvent ?? null,
elapsedMs: snapshot.elapsedMs ?? null,
waitingFor: snapshot.waitingFor ?? null,
outputTruncated: snapshot.outputTruncated === true || extra.outputTruncated === true,
valuesPrinted: false,
note: extra.note ?? "runnerTrace snapshot"
};
}
function normalizeTraceEvent(event, { traceId, seq, now, fallbackRunnerKind } = {}) {
const type = safeToken(event.type ?? event.kind ?? "event");
const status = safeToken(event.status ?? "observed");
const toolName = safeText(event.toolName ?? event.name, 120);
const label = safeText(event.label, 180) || labelFor({ type, status, toolName });
return dropUndefined({
seq,
traceId,
type,
stage: safeToken(event.stage ?? type),
status,
label,
createdAt: timestampFor(now),
elapsedMs: typeof event.elapsedMs === "number" ? Math.max(0, Math.trunc(event.elapsedMs)) : null,
runnerKind: safeText(event.runnerKind ?? fallbackRunnerKind, 140),
source: safeText(event.source, 120),
sourceSeq: Number.isInteger(event.sourceSeq) ? event.sourceSeq : undefined,
runId: safeText(event.runId, 180),
commandId: safeText(event.commandId, 180),
attemptId: safeText(event.attemptId, 180),
runnerId: safeText(event.runnerId, 180),
jobName: safeText(event.jobName, 180),
namespace: safeText(event.namespace, 180),
sessionId: safeText(event.sessionId, 180),
sessionStatus: safeText(event.sessionStatus, 80),
sessionLifecycleStatus: safeText(event.sessionLifecycleStatus, 80),
sessionReused: typeof event.sessionReused === "boolean" ? event.sessionReused : undefined,
turn: typeof event.turn === "number" ? event.turn : undefined,
toolName,
itemId: safeText(event.itemId, 180),
command: safeText(event.command, 900),
exitCode: Number.isInteger(event.exitCode) ? event.exitCode : undefined,
durationMs: typeof event.durationMs === "number" ? Math.max(0, Math.trunc(event.durationMs)) : undefined,
outputBytes: typeof event.outputBytes === "number" ? Math.max(0, Math.trunc(event.outputBytes)) : undefined,
promptSummary: safeText(event.promptSummary, 240),
outputSummary: safeText(event.outputSummary, 400),
stdoutSummary: safeText(event.stdoutSummary, 600),
stderrSummary: safeText(event.stderrSummary, 600),
chunk: safeText(event.chunk, 400),
message: safeText(event.message, 1200),
errorCode: safeText(event.errorCode, 120),
diagnosisCode: safeText(event.diagnosisCode ?? event.diagnosis?.code, 160),
diagnosis: normalizeDiagnosis(event.diagnosis),
waitingFor: safeText(event.waitingFor, 220),
timeoutMs: typeof event.timeoutMs === "number" ? Math.trunc(event.timeoutMs) : undefined,
hardTimeoutMs: typeof event.hardTimeoutMs === "number" ? Math.trunc(event.hardTimeoutMs) : undefined,
lastActivityAt: safeText(event.lastActivityAt, 80),
idleMs: typeof event.idleMs === "number" ? Math.max(0, Math.trunc(event.idleMs)) : undefined,
outputTruncated: event.outputTruncated === true,
terminal: event.terminal === true,
valuesPrinted: false
});
}
function normalizeAssistantDelta(delta, { traceId, now, fallbackRunnerKind } = {}) {
const chunk = safeText(delta.chunk ?? delta.delta, 400);
if (!chunk) return null;
return dropUndefined({
traceId,
itemId: safeText(delta.itemId, 180),
chunk,
createdAt: timestampFor(now),
runnerKind: safeText(delta.runnerKind ?? fallbackRunnerKind, 140),
sessionId: safeText(delta.sessionId, 180),
sessionStatus: safeText(delta.sessionStatus, 80),
turn: typeof delta.turn === "number" ? delta.turn : undefined,
threadId: safeText(delta.threadId, 180),
turnId: safeText(delta.turnId, 180),
waitingFor: safeText(delta.waitingFor, 220),
valuesPrinted: false
});
}
function assistantStreamsSnapshot(trace) {
if (!(trace?.assistantStreams instanceof Map)) return [];
return [...trace.assistantStreams.values()].map((stream) => ({ ...stream }));
}
function labelFor({ type, status, toolName }) {
if (type === "session") return `session:${status}`;
if (type === "prompt") return `prompt:${status}`;
if (type === "tool_call") return `tool:${toolName || "codex"}:${status}`;
if (type === "assistant_message") return `assistant:${status}`;
if (["timeout", "cancel", "error"].includes(type)) return type;
if (type === "request") return `request:${status}`;
if (type === "stdio") return `stdio:${status}`;
return `${type}:${status}`;
}
function notify(trace, event) {
const assistantStreams = assistantStreamsSnapshot(trace);
const snapshot = {
...emptySnapshot(trace.traceId),
...{
traceId: trace.traceId,
status: trace.status,
createdAt: trace.createdAt,
updatedAt: trace.updatedAt,
startedAt: trace.startedAt,
finishedAt: trace.finishedAt,
eventCount: trace.events.length,
events: trace.events.map((item) => ({ ...item })),
assistantStreams,
eventLabels: trace.events.map((item) => item.label).filter(Boolean),
lastEvent: event ?? trace.events.at(-1) ?? null,
elapsedMs: elapsedMs(trace.startedAt, trace.finishedAt ?? trace.updatedAt),
waitingFor: snapshotWaitingFor(trace, trace.events, assistantStreams, event ?? trace.events.at(-1) ?? null),
runnerKind: trace.meta.runnerKind,
workspace: trace.meta.workspace,
sandbox: trace.meta.sandbox,
sessionMode: trace.meta.sessionMode,
implementationType: trace.meta.implementationType,
sessionId: event?.sessionId ?? assistantStreams.at(-1)?.sessionId ?? null,
sessionStatus: event?.sessionStatus ?? assistantStreams.at(-1)?.sessionStatus ?? null,
sessionLifecycleStatus: event?.sessionLifecycleStatus ?? null,
outputTruncated: trace.events.some((item) => item.outputTruncated === true),
valuesPrinted: false
}
};
for (const listener of trace.listeners) {
try {
listener(event, snapshot);
} catch {
trace.listeners.delete(listener);
}
}
}
function emptySnapshot(traceId, extra = {}) {
const id = cleanTraceId(traceId);
return {
traceId: id,
status: "missing",
createdAt: null,
updatedAt: null,
startedAt: null,
finishedAt: null,
eventCount: 0,
events: [],
assistantStreams: [],
eventLabels: [],
lastEvent: null,
elapsedMs: null,
waitingFor: null,
runnerKind: extra.runnerKind ?? null,
workspace: extra.workspace ?? null,
sandbox: extra.sandbox ?? null,
sessionMode: extra.sessionMode ?? null,
implementationType: extra.implementationType ?? null,
sessionId: null,
sessionStatus: null,
sessionLifecycleStatus: null,
turn: null,
outputTruncated: false,
valuesPrinted: false
};
}
function lastWaitingFor(events) {
for (const event of [...events].reverse()) {
if (event.waitingFor) return event.waitingFor;
if (event.type === "tool_call" && ["started", "output_chunk"].includes(event.status)) {
return `tool:${event.toolName ?? "codex"}`;
}
if (event.type === "prompt" && event.status === "sent") return "assistant-message";
}
return null;
}
function snapshotWaitingFor(trace, events, assistantStreams, lastEvent) {
if (lastEvent?.terminal === true) {
return lastEvent.waitingFor ?? null;
}
if (trace?.finishedAt) return null;
return lastWaitingFor(events) ?? assistantStreams.at(-1)?.waitingFor ?? null;
}
function elapsedMs(start, end) {
const startMs = Date.parse(start ?? "");
const endMs = Date.parse(end ?? "");
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return null;
return Math.max(0, endMs - startMs);
}
function normalizeDiagnosis(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const lastRoutedEvent = value.lastRoutedEvent && typeof value.lastRoutedEvent === "object" && !Array.isArray(value.lastRoutedEvent)
? dropUndefined({
type: safeText(value.lastRoutedEvent.type, 80),
status: safeText(value.lastRoutedEvent.status, 80),
label: safeText(value.lastRoutedEvent.label, 180),
stage: safeText(value.lastRoutedEvent.stage, 80),
toolName: safeText(value.lastRoutedEvent.toolName, 120),
itemId: safeText(value.lastRoutedEvent.itemId, 180),
waitingFor: safeText(value.lastRoutedEvent.waitingFor, 220),
exitCode: Number.isInteger(value.lastRoutedEvent.exitCode) ? value.lastRoutedEvent.exitCode : undefined,
durationMs: typeof value.lastRoutedEvent.durationMs === "number" ? Math.max(0, Math.trunc(value.lastRoutedEvent.durationMs)) : undefined
})
: undefined;
return dropUndefined({
code: safeText(value.code, 160),
layer: safeText(value.layer, 120),
kind: safeText(value.kind, 120),
summary: safeText(value.summary, 500),
waitingFor: safeText(value.waitingFor, 220),
lastActivityAt: safeText(value.lastActivityAt, 80),
idleMs: typeof value.idleMs === "number" ? Math.max(0, Math.trunc(value.idleMs)) : undefined,
lastActivityLabel: safeText(value.lastActivityLabel, 180),
threadId: safeText(value.threadId, 180),
turnId: safeText(value.turnId, 180),
lastRoutedEvent
});
}
function safeText(value, limit = TEXT_LIMIT) {
if (value === undefined || value === null || value === "") return undefined;
const redacted = String(value).replace(SECRET_PATTERN, "[redacted]").replace(/\s+/gu, " ").trim();
if (!redacted) return undefined;
return redacted.length > limit ? `${redacted.slice(0, limit - 3)}...` : redacted;
}
function safeToken(value) {
return String(value ?? "")
.trim()
.replace(/[^A-Za-z0-9_.:-]/gu, "_")
.slice(0, 80) || "event";
}
function cleanTraceId(value) {
const id = String(value ?? "").trim();
return /^trc_[A-Za-z0-9_.:-]+$/u.test(id) ? id : null;
}
function timestampFor(now) {
const value = typeof now === "function" ? now() : now;
const date = value ? new Date(value) : new Date();
return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString();
}
function positiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function dropEmpty(value) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ""));
}
function dropUndefined(value) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}