| 1 | /** |
| 2 | * A session's transcript (`agent_session_events`): one statement per entry, |
| 3 | * appended in order. Here on its own so that whatever writes an entry |
| 4 | * (sessions, cards, the computer) needs nothing else from sessions.ts. |
| 5 | */ |
| 6 | import type { AgentComputerCommand, SessionEvent } from "@g1t/contracts"; |
| 7 | |
| 8 | const iso = () => new Date().toISOString(); |
| 9 | |
| 10 | /** Appends to a session's transcript. */ |
| 11 | export function eventStatement(db: D1Database, id: string, kind: SessionEvent["kind"], by: string | null, body: string, tool: string | null = null, outcome: string | null = null): D1PreparedStatement { |
| 12 | return db |
| 13 | .prepare( |
| 14 | `INSERT INTO agent_session_events (session_id, seq, kind, by_name, body, tool, outcome, created_at) |
| 15 | SELECT ?1, COALESCE(MAX(seq), 0) + 1, ?2, ?3, ?4, ?5, ?6, ?7 FROM agent_session_events WHERE session_id = ?1`, |
| 16 | ) |
| 17 | .bind(id, kind, by, body.slice(0, 20_000), tool, outcome, iso()); |
| 18 | } |
| 19 | |
| 20 | /** How much of a command's output a session's transcript keeps: the page shows it folded. */ |
| 21 | const COMMAND_OUTPUT_CHARS = 8_000; |
| 22 | |
| 23 | /** `outcome` for a command: `exit 0 · 1.2 s`, with what went wrong. */ |
| 24 | export function commandOutcome(command: Pick<AgentComputerCommand, "exit_code" | "duration_ms" | "timed_out" | "truncated">): string { |
| 25 | const seconds = command.duration_ms / 1000; |
| 26 | const took = seconds < 10 ? `${seconds.toFixed(1)} s` : seconds < 90 ? `${Math.round(seconds)} s` : `${Math.round(seconds / 60)} min`; |
| 27 | return [`exit ${command.exit_code}`, took, command.timed_out ? "timed out" : null, command.truncated ? "output cut" : null].filter(Boolean).join(" · "); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * A command the agent ran on its computer, as the transcript keeps it: the |
| 32 | * command on the first line, then its output (cut), the directory as |
| 33 | * `tool`, and how it ended as `outcome`. |
| 34 | */ |
| 35 | export function commandEvent(db: D1Database, sessionId: string, by: string, command: AgentComputerCommand): D1PreparedStatement { |
| 36 | const output = command.output.length > COMMAND_OUTPUT_CHARS ? `${command.output.slice(0, COMMAND_OUTPUT_CHARS)}\n[cut: ${command.output.length - COMMAND_OUTPUT_CHARS} more characters]` : command.output; |
| 37 | return eventStatement(db, sessionId, "command", by, `${command.cmd.split("\n")[0].slice(0, 500)}\n${output}`, command.cwd, commandOutcome(command)); |
| 38 | } |