/** * ToolRunRepository - CRUD + list_by_task, list_by_origin_message for tool_runs table (§11) * * Implements Repository per contracts §6. * * @module packages/runtime/src/storage/repositories/ToolRunRepository */ import type { Repository, TransactionHandle, SessionID, TaskID, AgentID, ToolRunID, MessageID, ISOTimeString, } from '@aircoding/contracts' import { DatabaseHandle } from '../MigrationRunner.js' // ============================================================================= // Types - per db-schema §11 // ============================================================================= export type ToolRunStatus = 'running' | 'ok' | 'error' | 'cancelled' export interface ToolRunRecord { id: ToolRunID session_id: SessionID task_id?: TaskID agent_id?: AgentID origin_message_id?: MessageID tool_name: string status: ToolRunStatus input_json: string output_json?: string error_json?: string started_at: ISOTimeString completed_at?: ISOTimeString duration_ms?: number artifacts_json?: string evidence_refs_json?: string metadata_json?: string } export type ToolRunInsert = Omit & { id?: ToolRunID } export type ToolRunUpdate = Partial> // ============================================================================= // ToolRunRepository // ============================================================================= export class ToolRunRepository implements Repository { private db: DatabaseHandle constructor(db: DatabaseHandle) { this.db = db } /** * Get a tool run by ID. */ async get(id: ToolRunID, tx?: TransactionHandle): Promise { const stmt = (tx?.db ?? this.db).prepare('SELECT * FROM tool_runs WHERE id = ?') const row = stmt.get(id) as ToolRunRecord | undefined return row } /** * Insert a new tool run. Status is set by EventStore projection (INV-1). */ async insert(record: ToolRunInsert, tx?: TransactionHandle): Promise { // Status is set by EventStore.project(), not by caller const status: ToolRunStatus = 'running' const stmt = (tx?.db ?? this.db).prepare(` INSERT INTO tool_runs ( id, session_id, task_id, agent_id, origin_message_id, tool_name, status, input_json, output_json, error_json, started_at, completed_at, duration_ms, artifacts_json, evidence_refs_json, metadata_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) stmt.run( record.id, record.session_id, record.task_id ?? null, record.agent_id ?? null, record.origin_message_id ?? null, record.tool_name, status, record.input_json, record.output_json ?? null, record.error_json ?? null, record.started_at, record.completed_at ?? null, record.duration_ms ?? null, record.artifacts_json ?? null, record.evidence_refs_json ?? null, record.metadata_json ?? null, ) } /** * Update an existing tool run. Status changes only via EventStore projection (INV-1). */ async update(id: ToolRunID, patch: ToolRunUpdate, tx?: TransactionHandle): Promise { const fields: string[] = [] const values: unknown[] = [] // status reaches here only via EventStore.project() (INV-1's authorized writer). if (patch.status !== undefined) { fields.push('status = ?') values.push(patch.status) } if (patch.output_json !== undefined) { fields.push('output_json = ?') values.push(patch.output_json) } if (patch.error_json !== undefined) { fields.push('error_json = ?') values.push(patch.error_json) } if (patch.completed_at !== undefined) { fields.push('completed_at = ?') values.push(patch.completed_at) } if (patch.duration_ms !== undefined) { fields.push('duration_ms = ?') values.push(patch.duration_ms) } if (patch.artifacts_json !== undefined) { fields.push('artifacts_json = ?') values.push(patch.artifacts_json) } if (patch.evidence_refs_json !== undefined) { fields.push('evidence_refs_json = ?') values.push(patch.evidence_refs_json) } if (patch.metadata_json !== undefined) { fields.push('metadata_json = ?') values.push(patch.metadata_json) } if (fields.length === 0) { return // Nothing to update } values.push(id) const stmt = (tx?.db ?? this.db).prepare(`UPDATE tool_runs SET ${fields.join(', ')} WHERE id = ?`) stmt.run(...values) } // ============================================================================= // Extra methods // ============================================================================= /** * List tool runs for a specific task. */ async list_by_task(task_id: TaskID): Promise { const stmt = this.db.prepare( 'SELECT * FROM tool_runs WHERE task_id = ? ORDER BY started_at DESC', ) return stmt.all(task_id) as ToolRunRecord[] } /** * List tool runs for a specific origin message. */ async list_by_origin_message(message_id: MessageID): Promise { const stmt = this.db.prepare( 'SELECT * FROM tool_runs WHERE origin_message_id = ? ORDER BY started_at ASC', ) return stmt.all(message_id) as ToolRunRecord[] } }