import { randomUUID } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import type { WorkbenchApplication, WorkbenchTurnInput } from "./contracts.ts"; type NativeState = { sessions: Record>; turns: Record>; }; export function createNativeTestWorkbenchApplication(options: { stateFile: string; now?: () => string }): WorkbenchApplication { const now = options.now ?? (() => new Date().toISOString()); const update = async (fn: (state: NativeState) => T | Promise) => { const state = await load(options.stateFile); const result = await fn(state); await mkdir(path.dirname(options.stateFile), { recursive: true }); await writeFile(options.stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8"); return result; }; return { async snapshot() { return load(options.stateFile); }, async health() { const state = await load(options.stateFile); return { serviceId: "hwlab-workbench-native-test", status: "ready", sessionCount: Object.keys(state.sessions).length, turnCount: Object.keys(state.turns).length }; }, async createSession({ actor, params }) { return update((state) => { const sessionId = validOptionalId(params.sessionId, "ses_") ?? `ses_${randomUUID()}`; const conversationId = validOptionalId(params.conversationId, "cnv_") ?? `cnv_${randomUUID()}`; const session = { sessionId, conversationId, projectId: text(params.projectId) || "prj_hwpod_workbench", ownerUserId: actor.id, status: "idle", providerProfile: text(params.providerProfile) || "native-test", createdAt: now(), updatedAt: now(), mode: "native-test", messages: [] as Record[] }; state.sessions[sessionId] = session; return { status: "created", session, valuesRedacted: true, secretMaterialStored: false }; }); }, async admitTurn(input) { return update((state) => { const session = state.sessions[input.sessionId]; if (!session || session.ownerUserId !== input.actor.id) throw codedError("session_not_found", "session is not visible to the actor"); const createdAt = now(); const message = text(input.params.message); state.turns[input.traceId] = { traceId: input.traceId, sessionId: input.sessionId, ownerUserId: input.actor.id, status: "admitted", message, createdAt, updatedAt: createdAt, terminal: false }; const messages = Array.isArray(session.messages) ? session.messages as Record[] : []; messages.push({ messageId: `msg_user_${input.traceId.slice(4)}`, role: "user", content: message, traceId: input.traceId, status: "completed", createdAt }); session.messages = messages; session.status = "running"; session.lastTraceId = input.traceId; session.updatedAt = createdAt; return input; }); }, async dispatchTurn(input: WorkbenchTurnInput) { return update((state) => { const turn = state.turns[input.traceId]; if (!turn) throw codedError("turn_not_admitted", "turn must be admitted before dispatch"); Object.assign(turn, { status: "running", updatedAt: now(), runId: `run_native_${input.traceId.slice(4)}`, commandId: `cmd_native_${input.traceId.slice(4)}` }); return { accepted: true, status: "running", traceId: input.traceId, sessionId: input.sessionId, runId: turn.runId, commandId: turn.commandId, mode: "native-test", terminal: false }; }); }, async cancelTurn(input) { return update((state) => { const turn = state.turns[input.traceId]; if (!turn || turn.ownerUserId !== input.actor.id) throw codedError("turn_not_found", "turn is not visible to the actor"); Object.assign(turn, { status: "cancel_requested", updatedAt: now() }); return { accepted: true, status: "cancel_requested", traceId: input.traceId, terminal: false, mode: "native-test" }; }); } }; } async function load(stateFile: string): Promise { const parsed = JSON.parse(await readFile(stateFile, "utf8").catch(() => '{"sessions":{},"turns":{}}')); return { sessions: object(parsed.sessions), turns: object(parsed.turns) }; } function object(value: unknown) { return value && typeof value === "object" && !Array.isArray(value) ? value as Record> : {}; } function text(value: unknown) { return String(value ?? "").trim(); } function validOptionalId(value: unknown, prefix: string) { const id = text(value); if (!id) return null; if (!id.startsWith(prefix)) throw codedError("invalid_input", `${prefix.slice(0, -1)} id is invalid`); return id; } function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }