|
|
|
|
@@ -8,8 +8,6 @@
|
|
|
|
|
|
|
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
|
|
|
|
// ---- shared primitives -----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const Uuid = z.string().uuid();
|
|
|
|
|
// Tool call IDs are model-emitted (e.g. "call_abc123") — not UUIDs.
|
|
|
|
|
const ToolCallId = z.string().min(1);
|
|
|
|
|
@@ -64,8 +62,6 @@ const ToolCallShape = z.object({
|
|
|
|
|
// publishFrame boundary.
|
|
|
|
|
const OpaqueObject = z.unknown();
|
|
|
|
|
|
|
|
|
|
// ---- per-session channel frames --------------------------------------------
|
|
|
|
|
|
|
|
|
|
export const SnapshotFrame = z.object({
|
|
|
|
|
type: z.literal('snapshot'),
|
|
|
|
|
messages: z.array(OpaqueObject),
|
|
|
|
|
@@ -78,6 +74,7 @@ export const MessageStartedFrame = z.object({
|
|
|
|
|
role: MessageRoleValue,
|
|
|
|
|
// v2.8-compare: groups messages belonging to the same compare operation.
|
|
|
|
|
compare_group_id: z.string().uuid().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const DeltaFrame = z.object({
|
|
|
|
|
@@ -86,6 +83,7 @@ export const DeltaFrame = z.object({
|
|
|
|
|
chat_id: Uuid.optional(),
|
|
|
|
|
content: z.string(),
|
|
|
|
|
compare_group_id: z.string().uuid().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const ReasoningDeltaFrame = z.object({
|
|
|
|
|
@@ -93,6 +91,7 @@ export const ReasoningDeltaFrame = z.object({
|
|
|
|
|
message_id: Uuid,
|
|
|
|
|
chat_id: Uuid.optional(),
|
|
|
|
|
content: z.string(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const ToolCallFrame = z.object({
|
|
|
|
|
@@ -100,6 +99,7 @@ export const ToolCallFrame = z.object({
|
|
|
|
|
message_id: Uuid,
|
|
|
|
|
chat_id: Uuid.optional(),
|
|
|
|
|
tool_call: ToolCallShape,
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const ToolResultFrame = z.object({
|
|
|
|
|
@@ -114,6 +114,7 @@ export const ToolResultFrame = z.object({
|
|
|
|
|
// Published alongside successful tool results so the frontend can render
|
|
|
|
|
// a compact diff snippet inline. Absent for read-only tools or failures.
|
|
|
|
|
diff: z.string().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const MessageCompleteFrame = z.object({
|
|
|
|
|
@@ -140,6 +141,7 @@ export const MessageCompleteFrame = z.object({
|
|
|
|
|
// type. Optional → fail-closed publishFrame must keep, not strip, it.
|
|
|
|
|
status: z.enum(['complete', 'cancelled', 'failed']).optional(),
|
|
|
|
|
compare_group_id: z.string().uuid().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const UsageFrame = z.object({
|
|
|
|
|
@@ -149,12 +151,14 @@ export const UsageFrame = z.object({
|
|
|
|
|
completion_tokens: z.number().int().nonnegative().nullable(),
|
|
|
|
|
ctx_used: z.number().int().nonnegative().nullable(),
|
|
|
|
|
ctx_max: z.number().int().positive().nullable(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const MessagesDeletedFrame = z.object({
|
|
|
|
|
type: z.literal('messages_deleted'),
|
|
|
|
|
message_ids: z.array(Uuid),
|
|
|
|
|
chat_id: Uuid.optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const ChatRenamedFrame = z.object({
|
|
|
|
|
@@ -177,10 +181,9 @@ export const ErrorFrame = z.object({
|
|
|
|
|
error: z.string(),
|
|
|
|
|
reason: ErrorReasonValue.optional(),
|
|
|
|
|
compare_group_id: z.string().uuid().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- per-user channel frames (sidebar refresh) -----------------------------
|
|
|
|
|
|
|
|
|
|
export const ChatStatusFrame = z.object({
|
|
|
|
|
type: z.literal('chat_status'),
|
|
|
|
|
chat_id: Uuid,
|
|
|
|
|
@@ -336,8 +339,6 @@ export const AgentStatusUpdatedFrame = z.object({
|
|
|
|
|
at: IsoTimestamp,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- orchestrator frames ([D-6]) -------------------------------------------
|
|
|
|
|
|
|
|
|
|
const FlowStepManifestEntry = z.object({
|
|
|
|
|
step_id: z.string().min(1),
|
|
|
|
|
agent: z.string().min(1),
|
|
|
|
|
@@ -354,6 +355,7 @@ export const FlowRunStartedFrame = z.object({
|
|
|
|
|
flow_name: z.string().min(1),
|
|
|
|
|
band: z.enum(['small', 'medium', 'large']),
|
|
|
|
|
steps: z.array(FlowStepManifestEntry),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Published on every step status change and on run completion. `report` is
|
|
|
|
|
@@ -367,23 +369,18 @@ export const FlowRunStepUpdatedFrame = z.object({
|
|
|
|
|
status: z.enum(['pending', 'running', 'completed', 'failed', 'skipped', 'cancelled', 'timed_out']),
|
|
|
|
|
run_status: z.enum(['running', 'completed', 'failed', 'cancelled']).optional(),
|
|
|
|
|
report: z.string().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- inter-agent message frame ---------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Published when one agent step sends a live message to another step in the
|
|
|
|
|
// same flow run. Broadcast on the user WS channel and delivered to in-process
|
|
|
|
|
// subscribers via the broker's internal topic.
|
|
|
|
|
export const AgentMessageFrame = z.object({
|
|
|
|
|
type: z.literal('agent_message'),
|
|
|
|
|
run_id: Uuid,
|
|
|
|
|
sender_step_id: z.string().min(1),
|
|
|
|
|
content: z.string(),
|
|
|
|
|
channel: z.string().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- arena frames ----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const ContestantManifestEntry = z.object({
|
|
|
|
|
id: Uuid,
|
|
|
|
|
identity: z.string().min(1),
|
|
|
|
|
@@ -399,6 +396,7 @@ export const BattleStartedFrame = z.object({
|
|
|
|
|
battle_type: z.enum(['coding', 'qa']),
|
|
|
|
|
prompt: z.string(),
|
|
|
|
|
contestants: z.array(ContestantManifestEntry),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Published on every contestant status change or streaming update.
|
|
|
|
|
@@ -414,6 +412,7 @@ export const ContestantUpdatedFrame = z.object({
|
|
|
|
|
battle_status: z.enum(['pending', 'running', 'completed', 'failed', 'cancelled']).optional(),
|
|
|
|
|
delta: z.string().optional(),
|
|
|
|
|
error: z.string().optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Published when battle-level state changes that don't ride on a contestant
|
|
|
|
|
@@ -427,10 +426,9 @@ export const BattleUpdatedFrame = z.object({
|
|
|
|
|
winner_contestant_id: Uuid.nullable().optional(),
|
|
|
|
|
analysis_ready: z.boolean().optional(),
|
|
|
|
|
cross_exam_id: Uuid.optional(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- agent snapshot restore frame ------------------------------------------
|
|
|
|
|
|
|
|
|
|
export const AgentSnapshotFrame = z.object({
|
|
|
|
|
type: z.literal('agent_snapshot'),
|
|
|
|
|
chat_id: z.string().uuid(),
|
|
|
|
|
@@ -440,8 +438,6 @@ export const AgentSnapshotFrame = z.object({
|
|
|
|
|
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(),
|
|
|
|
|
@@ -450,6 +446,7 @@ export const ToolTraceStartFrame = z.object({
|
|
|
|
|
tool_name: z.string().min(1),
|
|
|
|
|
tool_input: z.record(z.unknown()),
|
|
|
|
|
started_at: z.string().datetime(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const ToolTraceFinishFrame = z.object({
|
|
|
|
|
@@ -466,27 +463,13 @@ export const ToolTraceFinishFrame = z.object({
|
|
|
|
|
error: z.string().optional(),
|
|
|
|
|
outcome: z.string().optional(),
|
|
|
|
|
finished_at: z.string().datetime(),
|
|
|
|
|
stream_seq: z.number().int().min(0).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- collision warning frame (v2.8) ----------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Published when the BooCoder detects that multiple worktrees/agents are editing
|
|
|
|
|
// the same file concurrently. Advisory only — writes are not blocked.
|
|
|
|
|
|
|
|
|
|
// ---- BooControl fleet frames -----------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Published by the BooControl host service on the /api/ws/control WS endpoint.
|
|
|
|
|
// These frames use a 2-location sync pattern: contracts (WsFrameSchema +
|
|
|
|
|
// KNOWN_FRAME_TYPES) + web strict union only. They skip the server's broker
|
|
|
|
|
// entirely — control frames relay raw bytes through the proxy, so they never
|
|
|
|
|
// flow through the server's InferenceFrame union.
|
|
|
|
|
//
|
|
|
|
|
// The web strict union is the wire-format gate; missing it silently drops
|
|
|
|
|
// frames at JSON parse. The server loose union is NOT updated — adding it
|
|
|
|
|
// would be dead code.
|
|
|
|
|
|
|
|
|
|
// Host liveness state.
|
|
|
|
|
const HostLivenessValue = z.enum(['connected', 'reconnecting', 'down']);
|
|
|
|
|
// Host liveness state. Hosts are only ever 'connected' or 'down' — the WS
|
|
|
|
|
// connection pill carries a separate 'reconnecting' state (see ControlConnection
|
|
|
|
|
// in apps/web), which is unrelated to per-host liveness.
|
|
|
|
|
const HostLivenessValue = z.enum(['connected', 'down']);
|
|
|
|
|
|
|
|
|
|
// Control fleet snapshot/delta: full snapshot on join + seq-stamped state deltas.
|
|
|
|
|
export const ControlFleetFrame = z.object({
|
|
|
|
|
@@ -539,6 +522,9 @@ export const ControlLogFrame = z.object({
|
|
|
|
|
providerId: z.string(),
|
|
|
|
|
source: z.enum(['proxy', 'upstream', 'model']),
|
|
|
|
|
line: z.string(),
|
|
|
|
|
// Server-emit timestamp (ISO). Optional for backward compat; the web stamps
|
|
|
|
|
// ingest time when absent. Added so log rows show event time, not render time.
|
|
|
|
|
ts: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Control job: bench/eval run progress events.
|
|
|
|
|
@@ -551,11 +537,6 @@ export const ControlJobFrame = z.object({
|
|
|
|
|
detail: z.record(z.unknown()).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- collision warning frame (v2.8) ----------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Published when the BooCoder detects that multiple worktrees/agents are editing
|
|
|
|
|
// the same file concurrently. Advisory only — writes are not blocked.
|
|
|
|
|
|
|
|
|
|
const ConflictSeverityValue = z.enum(['same_line', 'adjacent_line', 'different_area']);
|
|
|
|
|
|
|
|
|
|
export const CollisionWarningFrame = z.object({
|
|
|
|
|
@@ -566,11 +547,6 @@ export const CollisionWarningFrame = z.object({
|
|
|
|
|
severity: ConflictSeverityValue,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- pty_exited frame (booterm) ---------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Published by booterm when a PTY process exits. Carries exit code, last output
|
|
|
|
|
// lines from the ring buffer, session metadata, and timeout status.
|
|
|
|
|
|
|
|
|
|
export const PtyExitedFrame = z.object({
|
|
|
|
|
type: z.literal('pty_exited'),
|
|
|
|
|
session_id: z.string().min(1).max(64),
|
|
|
|
|
@@ -583,13 +559,6 @@ export const PtyExitedFrame = z.object({
|
|
|
|
|
timed_out: z.boolean(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- 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(),
|
|
|
|
|
@@ -661,8 +630,6 @@ export const ChannelDeltaFrame = z.object({
|
|
|
|
|
diff: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- discriminated union ---------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export const WsFrameSchema = z.discriminatedUnion('type', [
|
|
|
|
|
// per-session
|
|
|
|
|
SnapshotFrame,
|
|
|
|
|
@@ -784,3 +751,11 @@ export const KNOWN_FRAME_TYPES: readonly WsFrame['type'][] = [
|
|
|
|
|
'control_log',
|
|
|
|
|
'control_job',
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
// Named type aliases for BooControl frame types, derived from the Zod schema.
|
|
|
|
|
// Consumers import these instead of hand-writing the shapes locally.
|
|
|
|
|
export type ControlFleetFrameType = Extract<WsFrame, { type: 'control_fleet' }>;
|
|
|
|
|
export type ControlActivityFrameType = Extract<WsFrame, { type: 'control_activity' }>;
|
|
|
|
|
export type ControlPerfFrameType = Extract<WsFrame, { type: 'control_perf' }>;
|
|
|
|
|
export type ControlLogFrameType = Extract<WsFrame, { type: 'control_log' }>;
|
|
|
|
|
export type ControlJobFrameType = Extract<WsFrame, { type: 'control_job' }>;
|
|
|
|
|
|