feat: Paseo-like orchestrator Phase 1-2 — trace system, session persistence, timeline, run_command, auto-fix loop
Phase 1: Trace System + Observability - tool_traces DB table + insert/update service - tool_trace_start/tool_trace_finish WS frames (contracts + FE types) - Instrumented tool-phase.ts with timing around every tool call - GET /api/chats/:id/traces paginated endpoint - Trace viewer frontend (collapsible panel with timing bars + token breakdown) Phase 2: Session Persistence + Resume - agent_snapshots table (UPSERT per chat, persisted on turn boundaries) - save/load/delete service functions - Agent snapshot sent on WS reconnect - Session timeline view (vertical timeline with scroll-to + restore) Tooling: - run_command tool (execFile, 30s timeout, 32KB cap, path-guarded) - Auto-fix loop: after write tools, runs pnpm build, injects errors into next turn
This commit is contained in:
@@ -407,11 +407,115 @@ export const BattleUpdatedFrame = z.object({
|
||||
cross_exam_id: Uuid.optional(),
|
||||
});
|
||||
|
||||
// ---- agent snapshot restore frame ------------------------------------------
|
||||
|
||||
export const AgentSnapshotFrame = z.object({
|
||||
type: z.literal('agent_snapshot'),
|
||||
chat_id: z.string().uuid(),
|
||||
agent: z.string().nullable().optional(),
|
||||
model: z.string(),
|
||||
mode: z.string().nullable().optional(),
|
||||
turn_number: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
// ---- tool trace frames -----------------------------------------------------
|
||||
|
||||
export const ToolTraceStartFrame = z.object({
|
||||
type: z.literal('tool_trace_start'),
|
||||
trace_id: z.string().uuid(),
|
||||
message_id: z.string().uuid(),
|
||||
chat_id: z.string().uuid(),
|
||||
tool_name: z.string().min(1),
|
||||
tool_input: z.record(z.unknown()),
|
||||
started_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const ToolTraceFinishFrame = z.object({
|
||||
type: z.literal('tool_trace_finish'),
|
||||
trace_id: z.string().uuid(),
|
||||
message_id: z.string().uuid(),
|
||||
chat_id: z.string().uuid(),
|
||||
tool_name: z.string().min(1),
|
||||
tool_output: z.union([z.string(), z.null()]).optional(),
|
||||
latency_ms: z.number().int().nonnegative().optional(),
|
||||
tokens_used: z.number().int().nonnegative().nullable().optional(),
|
||||
cache_tokens: z.number().int().nonnegative().nullable().optional(),
|
||||
reasoning_tokens: z.number().int().nonnegative().nullable().optional(),
|
||||
error: z.string().optional(),
|
||||
outcome: z.string().optional(),
|
||||
finished_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
// ---- channel-delta frames (streaming v2) ----------------------------------
|
||||
//
|
||||
// Each channel frame carries a monotonic `seq` counter so the client can
|
||||
// reorder out-of-order deltas per-channel, detect gaps, and request replay on
|
||||
// reconnect. The `channel` discriminator tells the reducer which substate to
|
||||
// update.
|
||||
|
||||
const TextChannelPayload = z.object({
|
||||
message_id: Uuid,
|
||||
chat_id: Uuid.optional(),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
const ToolCallChannelPayload = z.object({
|
||||
message_id: Uuid,
|
||||
chat_id: Uuid.optional(),
|
||||
tool_call: ToolCallShape,
|
||||
});
|
||||
|
||||
const ToolResultChannelPayload = z.object({
|
||||
tool_message_id: Uuid,
|
||||
chat_id: Uuid.optional(),
|
||||
tool_call_id: ToolCallId,
|
||||
output: z.unknown(),
|
||||
truncated: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
const StatusChannelPayload = z.object({
|
||||
message_id: Uuid,
|
||||
chat_id: Uuid.optional(),
|
||||
status: z.enum(['running', 'complete', 'cancelled', 'failed']).optional(),
|
||||
tokens_used: z.number().int().nonnegative().nullable().optional(),
|
||||
ctx_used: z.number().int().nonnegative().nullable().optional(),
|
||||
ctx_max: z.number().int().positive().nullable().optional(),
|
||||
cache_tokens: z.number().int().nonnegative().nullable().optional(),
|
||||
reasoning_tokens: z.number().int().nonnegative().nullable().optional(),
|
||||
started_at: IsoTimestamp.nullable().optional(),
|
||||
finished_at: IsoTimestamp.nullable().optional(),
|
||||
model: z.string().nullable().optional(),
|
||||
metadata: OpaqueObject.nullable().optional(),
|
||||
});
|
||||
|
||||
const ErrorChannelPayload = z.object({
|
||||
message_id: Uuid.optional(),
|
||||
chat_id: Uuid.optional(),
|
||||
error: z.string(),
|
||||
reason: ErrorReasonValue.optional(),
|
||||
});
|
||||
|
||||
const ChannelDeltaPayload = z.discriminatedUnion('channel', [
|
||||
z.object({ channel: z.literal('text'), ...TextChannelPayload.shape }),
|
||||
z.object({ channel: z.literal('tool_call'), ...ToolCallChannelPayload.shape }),
|
||||
z.object({ channel: z.literal('tool_result'), ...ToolResultChannelPayload.shape }),
|
||||
z.object({ channel: z.literal('status'), ...StatusChannelPayload.shape }),
|
||||
z.object({ channel: z.literal('error'), ...ErrorChannelPayload.shape }),
|
||||
]);
|
||||
|
||||
export const ChannelDeltaFrame = z.object({
|
||||
type: z.literal('channel_delta'),
|
||||
seq: z.number().int().nonnegative(),
|
||||
...ChannelDeltaPayload.shape,
|
||||
});
|
||||
|
||||
// ---- discriminated union ---------------------------------------------------
|
||||
|
||||
export const WsFrameSchema = z.discriminatedUnion('type', [
|
||||
// per-session
|
||||
SnapshotFrame,
|
||||
AgentSnapshotFrame,
|
||||
MessageStartedFrame,
|
||||
DeltaFrame,
|
||||
ReasoningDeltaFrame,
|
||||
@@ -434,6 +538,11 @@ export const WsFrameSchema = z.discriminatedUnion('type', [
|
||||
BattleStartedFrame,
|
||||
ContestantUpdatedFrame,
|
||||
BattleUpdatedFrame,
|
||||
// tool trace
|
||||
ToolTraceStartFrame,
|
||||
ToolTraceFinishFrame,
|
||||
// channel-delta (streaming v2)
|
||||
ChannelDeltaFrame,
|
||||
// per-user
|
||||
ChatStatusFrame,
|
||||
SessionUpdatedFrame,
|
||||
@@ -461,6 +570,7 @@ export type WsFrame = z.infer<typeof WsFrameSchema>;
|
||||
// by the drift test in src/__tests__/ws-frames.test.ts.
|
||||
export const KNOWN_FRAME_TYPES: readonly WsFrame['type'][] = [
|
||||
'snapshot',
|
||||
'agent_snapshot',
|
||||
'message_started',
|
||||
'delta',
|
||||
'reasoning_delta',
|
||||
@@ -481,6 +591,9 @@ export const KNOWN_FRAME_TYPES: readonly WsFrame['type'][] = [
|
||||
'battle_started',
|
||||
'contestant_updated',
|
||||
'battle_updated',
|
||||
'tool_trace_start',
|
||||
'tool_trace_finish',
|
||||
'channel_delta',
|
||||
'chat_status',
|
||||
'session_updated',
|
||||
'session_renamed',
|
||||
|
||||
Reference in New Issue
Block a user