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
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
import type { Sql } from '../db.js';
|
|
|
|
export interface ToolTrace {
|
|
id: string;
|
|
session_id: string;
|
|
chat_id: string;
|
|
message_id: string | null;
|
|
turn_number: number;
|
|
tool_name: string;
|
|
tool_input: unknown;
|
|
tool_output: string | null;
|
|
started_at: string;
|
|
finished_at: string | null;
|
|
latency_ms: number | null;
|
|
tokens_used: number | null;
|
|
cache_tokens: number | null;
|
|
reasoning_tokens: number | null;
|
|
error: string | null;
|
|
outcome: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ToolTraceInsert {
|
|
session_id: string;
|
|
chat_id: string;
|
|
message_id: string | null;
|
|
turn_number: number;
|
|
tool_name: string;
|
|
tool_input: unknown;
|
|
outcome?: string;
|
|
}
|
|
|
|
export interface ToolTraceUpdate {
|
|
finished_at?: string;
|
|
latency_ms?: number;
|
|
tool_output?: string;
|
|
tokens_used?: number;
|
|
cache_tokens?: number;
|
|
reasoning_tokens?: number;
|
|
error?: string;
|
|
outcome?: string;
|
|
}
|
|
|
|
export async function insertToolTrace(
|
|
sql: Sql,
|
|
insert: ToolTraceInsert,
|
|
): Promise<ToolTrace> {
|
|
const [row] = await sql<ToolTrace[]>`
|
|
INSERT INTO tool_traces (
|
|
session_id, chat_id, message_id, turn_number,
|
|
tool_name, tool_input, outcome
|
|
) VALUES (
|
|
${insert.session_id}, ${insert.chat_id}, ${insert.message_id},
|
|
${insert.turn_number}, ${insert.tool_name},
|
|
${sql.json(insert.tool_input as never)},
|
|
${insert.outcome ?? null}
|
|
)
|
|
RETURNING *
|
|
`;
|
|
if (!row) throw new Error('insertToolTrace returned no row');
|
|
return row;
|
|
}
|
|
|
|
export async function updateToolTrace(
|
|
sql: Sql,
|
|
id: string,
|
|
updates: ToolTraceUpdate,
|
|
): Promise<ToolTrace | null> {
|
|
const cols: string[] = [];
|
|
const vals: any[] = [];
|
|
|
|
if (updates.finished_at !== undefined) { cols.push('finished_at'); vals.push(updates.finished_at); }
|
|
if (updates.latency_ms !== undefined) { cols.push('latency_ms'); vals.push(updates.latency_ms); }
|
|
if (updates.tool_output !== undefined) { cols.push('tool_output'); vals.push(updates.tool_output); }
|
|
if (updates.tokens_used !== undefined) { cols.push('tokens_used'); vals.push(updates.tokens_used); }
|
|
if (updates.cache_tokens !== undefined) { cols.push('cache_tokens'); vals.push(updates.cache_tokens); }
|
|
if (updates.reasoning_tokens !== undefined) { cols.push('reasoning_tokens'); vals.push(updates.reasoning_tokens); }
|
|
if (updates.error !== undefined) { cols.push('error'); vals.push(updates.error); }
|
|
if (updates.outcome !== undefined) { cols.push('outcome'); vals.push(updates.outcome); }
|
|
|
|
if (cols.length === 0) {
|
|
const [row] = await sql<ToolTrace[]>`SELECT * FROM tool_traces WHERE id = ${id}`;
|
|
return row ?? null;
|
|
}
|
|
|
|
const setClause = cols.map((c, i) => `${c} = $${i + 1}`).join(', ');
|
|
const [row] = await sql.unsafe<ToolTrace[]>(
|
|
`UPDATE tool_traces SET ${setClause} WHERE id = $${cols.length + 1} RETURNING *`,
|
|
[...vals, id],
|
|
);
|
|
return row ?? null;
|
|
}
|