Files
pikasTech-HWLAB/internal/workbench/native-agentrun-application.ts
T

171 lines
8.0 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path";
import {
cancelAgentRunChatTurn,
describeAgentRunAdapterAvailability,
submitAgentRunChatTurn,
} from "../cloud/code-agent-agentrun-adapter.ts";
import { createCodeAgentTraceStore } from "../cloud/code-agent-trace-store.ts";
import type { WorkbenchApplication, WorkbenchTurnInput } from "./contracts.ts";
type NativeState = {
sessions: Record<string, Record<string, any>>;
turns: Record<string, Record<string, any>>;
};
export function createNativeAgentRunWorkbenchApplication(options: {
stateFile: string;
env: Record<string, string | undefined>;
fetchImpl?: typeof fetch;
now?: () => string;
}): WorkbenchApplication {
const env = options.env;
const fetchImpl = options.fetchImpl ?? fetch;
const now = options.now ?? (() => new Date().toISOString());
const traceStore = createCodeAgentTraceStore();
const mutate = async <T>(fn: (state: NativeState) => T | Promise<T>) => {
const state = await load(options.stateFile);
const result = await fn(state);
await mkdir(path.dirname(options.stateFile), { recursive: true });
const temporary = `${options.stateFile}.${process.pid}.tmp`;
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, "utf8");
await rename(temporary, options.stateFile);
return result;
};
return {
async snapshot() { return load(options.stateFile); },
async health() {
const state = await load(options.stateFile);
const availability = describeAgentRunAdapterAvailability(env, { params: { providerProfile: env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE } });
return {
serviceId: "hwlab-workbench-native-agentrun",
status: availability.ready ? "ready" : "blocked",
mode: "agentrun-native",
providerProfile: text(env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE),
agentRun: availability.agentRun,
sessionCount: Object.keys(state.sessions).length,
turnCount: Object.keys(state.turns).length,
valuesPrinted: false,
};
},
async createSession({ actor, params }) {
return mutate((state) => {
const sessionId = validOptionalId(params.sessionId, "ses_") ?? `ses_${randomUUID()}`;
const conversationId = validOptionalId(params.conversationId, "cnv_") ?? `cnv_${randomUUID()}`;
const timestamp = now();
const session = {
sessionId,
conversationId,
projectId: text(params.projectId) || "pikasTech/HWLAB",
ownerUserId: actor.id,
ownerRole: actor.role ?? "user",
status: "idle",
providerProfile: text(params.providerProfile) || text(env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE) || "gpt.pika",
createdAt: timestamp,
updatedAt: timestamp,
mode: "agentrun-native",
messages: [] as Record<string, unknown>[],
};
state.sessions[sessionId] = session;
return { status: "created", session, valuesRedacted: true, secretMaterialStored: false };
});
},
async admitTurn(input) {
return mutate((state) => {
const session = ownedSession(state, input.sessionId, input.actor.id);
const timestamp = validTimestamp(input.params.submittedAt) ?? now();
const message = requiredText(input.params.message, "message");
state.turns[input.traceId] = {
traceId: input.traceId,
sessionId: input.sessionId,
conversationId: session.conversationId,
ownerUserId: input.actor.id,
status: "admitted",
message,
createdAt: timestamp,
updatedAt: timestamp,
terminal: false,
};
session.messages.push({ messageId: `msg_user_${input.traceId.slice(4)}`, role: "user", content: message, traceId: input.traceId, status: "completed", createdAt: timestamp });
session.status = "running";
session.lastTraceId = input.traceId;
session.updatedAt = timestamp;
return { ...input, params: { ...input.params, message, submittedAt: timestamp, providerProfile: text(input.params.providerProfile) || session.providerProfile } };
});
},
async dispatchTurn(input: WorkbenchTurnInput) {
const state = await load(options.stateFile);
const session = ownedSession(state, input.sessionId, input.actor.id);
const params = {
...input.params,
sessionId: input.sessionId,
conversationId: session.conversationId,
projectId: session.projectId,
ownerUserId: input.actor.id,
ownerRole: input.actor.role ?? "user",
providerProfile: text(input.params.providerProfile) || session.providerProfile,
shortConnection: true,
};
const admitted = await submitAgentRunChatTurn({ traceId: input.traceId, params, options: { env, fetchImpl }, traceStore });
await projectRunning(admitted, input.traceId);
return {
status: "running",
terminal: false,
traceId: input.traceId,
sessionId: input.sessionId,
agentRun: admitted.agentRun,
terminalAuthority: "hwlab.event.v1",
resultSynthesized: false,
valuesPrinted: false,
};
},
async cancelTurn(input) {
const state = await load(options.stateFile);
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");
const currentResult = turn.agentRun ? { status: "running", agentRun: turn.agentRun } : null;
const canceled = await cancelAgentRunChatTurn({ traceId: input.traceId, currentResult, options: { env, fetchImpl }, traceStore });
if (!canceled) throw codedError("turn_not_running", "turn has no active AgentRun command");
return {
status: "cancel-requested",
terminal: false,
traceId: input.traceId,
agentRun: canceled.agentRun,
terminalAuthority: "hwlab.event.v1",
resultSynthesized: false,
valuesPrinted: false,
};
},
};
async function projectRunning(result: any, traceId: string) {
return mutate((state) => {
const turn = state.turns[traceId];
if (!turn) throw codedError("turn_not_admitted", "turn must be admitted before dispatch");
Object.assign(turn, {
status: "running",
updatedAt: now(),
runId: result.agentRun?.runId ?? null,
commandId: result.agentRun?.commandId ?? null,
runnerJobId: result.agentRun?.runnerJobId ?? null,
agentRun: result.agentRun ?? null,
});
});
}
}
async function load(stateFile: string): Promise<NativeState> {
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<string, Record<string, any>> : {}; }
function text(value: unknown) { return String(value ?? "").trim(); }
function validTimestamp(value: unknown) { const timestamp = text(value); return timestamp && Number.isFinite(Date.parse(timestamp)) ? new Date(Date.parse(timestamp)).toISOString() : null; }
function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw codedError("invalid_input", `${field} is required`); return result; }
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 ownedSession(state: NativeState, sessionId: string, ownerUserId: string) { const session = state.sessions[sessionId]; if (!session || session.ownerUserId !== ownerUserId) throw codedError("session_not_found", "session is not visible to the actor"); return session; }
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }