UNPKG

agents

Version:

A home for your AI agents

1,082 lines (1,081 loc) • 37.9 kB
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../../../classPrivateFieldGet2-DZBYAB34.js"; import { t as _classPrivateMethodInitSpec } from "../../../classPrivateMethodInitSpec-qMjJ6sHQ.js"; import { n as CLOSE_UNAUTHORIZED, o as CONTAINER_SESSION_PATH, r as CONTAINER_ENV, s as CONTAINER_TOKEN_HEADER, u as inputText } from "../../../protocol-cOklVBBD.js"; import { t as ContainerDaemon } from "../../../daemon-core-Buubj-y1.js"; import { createHash, timingSafeEqual } from "node:crypto"; import { createServer } from "node:http"; import { spawn } from "node:child_process"; import { constants } from "node:fs"; import { chown, copyFile, mkdir, open, readdir, readlink, rm, stat, symlink } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join, relative, resolve, sep } from "node:path"; import { createInterface } from "node:readline"; //#region src/harness/container/runtime/websocket.ts /** * The server half of RFC 6455, for one kind of peer: the Durable Object, * which sends text messages and closes cleanly. Enough of the protocol for * that (handshake, masked client frames, fragmentation, ping, close) and * nothing else, so the daemon needs no dependencies and runs on the plain * Node in `cloudflare/debian-trixie`. */ const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; /** Largest message accepted. Restore messages are chunked well below this. */ const MAX_MESSAGE_BYTES = 64 * 1024 * 1024; const OP_CONTINUATION = 0; const OP_TEXT = 1; const OP_BINARY = 2; const OP_CLOSE = 8; const OP_PING = 9; const OP_PONG = 10; function frame(opcode, payload) { const length = payload.length; const header = length < 126 ? Buffer.from([128 | opcode, length]) : length < 65536 ? Buffer.from([ 128 | opcode, 126, length >> 8, length & 255 ]) : Buffer.concat([Buffer.from([128 | opcode, 127]), (() => { const size = Buffer.alloc(8); size.writeBigUInt64BE(BigInt(length)); return size; })()]); return Buffer.concat([header, payload]); } /** * Complete the upgrade handshake and serve the connection. * * @param request - The upgrade request. * @param socket - The raw socket. * @param head - Bytes read past the request head. * @param handlers - Called for each message and on close. * @returns The connection, or undefined when the request was not a valid * upgrade (the socket is destroyed). */ function acceptWebSocket(request, socket, head, handlers) { const key = request.headers["sec-websocket-key"]; if (typeof key !== "string" || request.headers.upgrade?.toLowerCase() !== "websocket") { socket.destroy(); return; } const accept = createHash("sha1").update(key + GUID).digest("base64"); socket.write(`HTTP/1.1 101 Switching Protocols\r Upgrade: websocket\r Connection: Upgrade\r Sec-WebSocket-Accept: ${accept}\r\n\r\n`); let open = true; let ended = false; const end = () => { if (ended) return; ended = true; open = false; handlers.closed(); }; const write = (opcode, payload) => { if (!open) return; socket.write(frame(opcode, payload)); }; const connection = { send: (text) => write(OP_TEXT, Buffer.from(text, "utf8")), close: (code, reason) => { if (!open) return; const payload = Buffer.alloc(2 + Buffer.byteLength(reason)); payload.writeUInt16BE(code); payload.write(reason, 2); write(OP_CLOSE, payload); open = false; socket.end(); } }; let buffered = head.length > 0 ? Buffer.from(head) : Buffer.alloc(0); let fragments = []; let fragmentBytes = 0; const drain = () => { for (;;) { if (buffered.length < 2) return; const first = buffered[0] ?? 0; const second = buffered[1] ?? 0; const fin = (first & 128) !== 0; const opcode = first & 15; const masked = (second & 128) !== 0; let length = second & 127; let offset = 2; if (length === 126) { if (buffered.length < 4) return; length = buffered.readUInt16BE(2); offset = 4; } else if (length === 127) { if (buffered.length < 10) return; const big = buffered.readBigUInt64BE(2); if (big > BigInt(MAX_MESSAGE_BYTES)) { connection.close(1009, "message too big"); return; } length = Number(big); offset = 10; } const maskOffset = offset; if (masked) offset += 4; if (buffered.length < offset + length) return; const payload = Buffer.from(buffered.subarray(offset, offset + length)); if (masked) for (let i = 0; i < payload.length; i++) payload[i] = (payload[i] ?? 0) ^ (buffered[maskOffset + i % 4] ?? 0); buffered = buffered.subarray(offset + length); switch (opcode) { case OP_TEXT: case OP_BINARY: case OP_CONTINUATION: fragments.push(payload); fragmentBytes += payload.length; if (fragmentBytes > MAX_MESSAGE_BYTES) { connection.close(1009, "message too big"); return; } if (fin) { const message = Buffer.concat(fragments).toString("utf8"); fragments = []; fragmentBytes = 0; handlers.message(message); } break; case OP_PING: write(OP_PONG, payload); break; case OP_PONG: break; case OP_CLOSE: if (open) { write(OP_CLOSE, payload.subarray(0, 2)); open = false; } socket.end(); end(); return; default: connection.close(1002, "unsupported opcode"); return; } } }; socket.on("data", (chunk) => { buffered = buffered.length === 0 ? chunk : Buffer.concat([buffered, chunk]); drain(); }); socket.on("close", end); socket.on("error", end); if (buffered.length > 0) queueMicrotask(drain); return connection; } //#endregion //#region src/harness/container/runtime/server.ts /** * The daemon's transport: an HTTP server that answers the health check and * upgrades `/sessions/<id>` to a WebSocket for the daemon core. The Durable * Object reaches it through `ctx.container.getTcpPort(port)`. * * Every request except the health check must carry the token the object * started the container with. The health check stays open so that a wrong * token looks different from a dead container. */ function tokenMatches(expected, given) { if (given === void 0) return false; const a = Buffer.from(expected); const b = Buffer.from(given); return a.length === b.length && timingSafeEqual(a, b); } /** * Start the daemon. * * @param options - The adapter, identity, secret, and port. * @returns The running server, once it listens. * * @experimental The API may change before it stabilizes. */ async function serve(options) { const daemon = new ContainerDaemon({ adapter: options.adapter, runtimeId: options.runtimeId }); const sockets = /* @__PURE__ */ new Set(); const server = createServer((request, response) => { if (request.url === "/healthz") { response.writeHead(200, { "content-type": "text/plain" }); response.end("ok"); return; } response.writeHead(404); response.end(); }); server.on("upgrade", (request, socket, head) => { const url = new URL(request.url ?? "/", "http://container"); if (!url.pathname.startsWith("/sessions/")) { socket.destroy(); return; } const session = decodeURIComponent(url.pathname.slice(CONTAINER_SESSION_PATH.length)); const header = request.headers[CONTAINER_TOKEN_HEADER]; if (!tokenMatches(options.token, Array.isArray(header) ? header[0] : header) || session.length === 0) { acceptWebSocket(request, socket, head, { message: () => {}, closed: () => {} })?.close(CLOSE_UNAUTHORIZED, "unauthorized"); return; } let connection; const ws = acceptWebSocket(request, socket, head, { message: (text) => connection?.receive(text), closed: () => { if (ws) sockets.delete(ws); connection?.closed(); } }); if (!ws) return; sockets.add(ws); connection = daemon.connect(session, ws); }); await new Promise((resolve) => { server.listen(options.port, options.host ?? "0.0.0.0", resolve); }); const address = server.address(); return { port: typeof address === "object" && address ? address.port : 0, async close() { for (const client of sockets) client.close(1001, "shutting down"); await new Promise((resolve) => server.close(() => resolve())); await daemon.close(); } }; } /** * Start the daemon with the identity, secret, and port the Durable Object * started the container with, and stop it on SIGTERM. The entrypoint of an * image: `serveFromEnv(cliAdapter({ ... }))`. * * @param adapter - The agent to run. * @param env - The environment. Default `process.env`. * @returns The running server. * * @experimental The API may change before it stabilizes. */ async function serveFromEnv(adapter, env = process.env) { const token = env[CONTAINER_ENV.token]; const runtimeId = env[CONTAINER_ENV.runtimeId]; const port = Number(env[CONTAINER_ENV.port] ?? "8080"); if (!token || !runtimeId || !Number.isInteger(port) || port <= 0) throw new Error(`${CONTAINER_ENV.token}, ${CONTAINER_ENV.runtimeId} and ${CONTAINER_ENV.port} must be set; ContainerHarness sets them`); delete env[CONTAINER_ENV.token]; const server = await serve({ adapter, runtimeId, token, port }); console.log(`harness-daemon: ${adapter.id} on ${server.port} (${runtimeId})`); const stop = () => { server.close().finally(() => process.exit(0)); }; process.once("SIGTERM", stop); process.once("SIGINT", stop); return server; } //#endregion //#region src/harness/container/runtime/json.ts /** * Whether a JSON value is an object (not an array or a primitive). * * @param value - The value, or undefined for a missing field. * @returns True for an object. */ function isJsonObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } /** * The strings of a JSON array field. * * @param value - The field. * @returns Its string elements, or undefined when it is not an array. */ function stringsOf(value) { if (!Array.isArray(value)) return void 0; return value.filter((each) => typeof each === "string"); } //#endregion //#region src/harness/container/runtime/file-mirror.ts /** * Mirrors a CLI's state directories into the Durable Object and back. * * Agent CLIs keep their sessions as files: Claude Code under * `~/.claude/projects`, Codex under `~/.codex/sessions`. A container's disk * does not outlive the container, so the mirror ships every change to * those files out as persisted entries, and writes them back, in order, * when the session opens in a new container. The CLI then resumes its own * session from its own files and never knows it moved. * * Session files are append-only logs, so a file that grew ships only the * bytes it gained. A file that shrank, or changed without growing, ships * whole. Data travels base64-encoded in pieces small enough for one * WebSocket message. */ /** Most bytes of one file in one entry. */ const PIECE_BYTES = 128 * 1024; function isFileEntry(value) { return isJsonObject(value) && value.kind === "file" && typeof value.path === "string" && typeof value.offset === "number" && typeof value.data === "string"; } var _root = /* @__PURE__ */ new WeakMap(); var _dirs = /* @__PURE__ */ new WeakMap(); var _seen$1 = /* @__PURE__ */ new WeakMap(); var _FileMirror_brand = /* @__PURE__ */ new WeakSet(); /** One session's mirrored directories, under the session's home. */ var FileMirror = class { /** * @param root - The session's home directory. * @param dirs - Directories to mirror, relative to `root`. */ constructor(root, dirs) { _classPrivateMethodInitSpec(this, _FileMirror_brand); _classPrivateFieldInitSpec(this, _root, void 0); _classPrivateFieldInitSpec(this, _dirs, void 0); _classPrivateFieldInitSpec(this, _seen$1, /* @__PURE__ */ new Map()); _classPrivateFieldSet2(_root, this, resolve(root)); _classPrivateFieldSet2(_dirs, this, dirs); } /** * Replace the mirrored directories with the persisted entries, applied in * order. Entries that are not file entries are skipped. Whatever the * directories held before (a reset session's old files, say) is removed. * * @param entries - Everything persisted for the session, oldest first. */ async restore(entries) { for (const dir of _classPrivateFieldGet2(_dirs, this)) { const path = _assertClassBrand(_FileMirror_brand, this, _inside).call(this, dir); if (path !== void 0 && path !== _classPrivateFieldGet2(_root, this)) await rm(path, { recursive: true, force: true }); } for (const entry of entries) { if (!isFileEntry(entry)) continue; const path = _assertClassBrand(_FileMirror_brand, this, _inside).call(this, entry.path); if (path === void 0) continue; await mkdir(dirname(path), { recursive: true }); const handle = entry.offset === 0 ? await open(path, "w") : await open(path, "r+").catch(() => open(path, "w")); try { if (entry.offset > 0) await handle.truncate(entry.offset); await handle.write(Buffer.from(entry.data, "base64"), 0, void 0, entry.offset); } finally { await handle.close(); } } for (const file of await _assertClassBrand(_FileMirror_brand, this, _files).call(this)) { const info = await stat(file); _classPrivateFieldGet2(_seen$1, this).set(file, { size: info.size, mtimeMs: info.mtimeMs }); } } /** * The changes since the last call (or the restore), as entries to persist. * * @returns File entries, in the order they must be applied. */ async collect() { const entries = []; for (const file of await _assertClassBrand(_FileMirror_brand, this, _files).call(this)) { const info = await stat(file).catch(() => void 0); if (!info) continue; const seen = _classPrivateFieldGet2(_seen$1, this).get(file); if (seen && seen.size === info.size && seen.mtimeMs === info.mtimeMs) continue; const from = seen && info.size > seen.size ? seen.size : 0; const bytes = await _assertClassBrand(_FileMirror_brand, this, _read).call(this, file, from, info.size); const pieces = []; for (let at = 0; at < bytes.length; at += PIECE_BYTES) pieces.push(bytes.subarray(at, at + PIECE_BYTES)); if (pieces.length === 0) pieces.push(bytes); pieces.forEach((piece, index) => { entries.push(_assertClassBrand(_FileMirror_brand, this, _entry).call(this, file, from + index * PIECE_BYTES, piece)); }); _classPrivateFieldGet2(_seen$1, this).set(file, { size: from + bytes.length, mtimeMs: info.mtimeMs }); } return entries; } }; function _entry(file, offset, data) { return { kind: "file", path: relative(_classPrivateFieldGet2(_root, this), file).split(sep).join("/"), offset, data: data.toString("base64") }; } async function _read(file, from, to) { const handle = await open(file, "r"); try { const buffer = Buffer.alloc(Math.max(0, to - from)); const { bytesRead } = await handle.read(buffer, 0, buffer.length, from); return buffer.subarray(0, bytesRead); } finally { await handle.close(); } } async function _files() { const files = []; const walk = async (dir) => { const entries = await readdir(dir, { withFileTypes: true }).catch(() => []); for (const entry of entries) { const path = join(dir, entry.name); if (entry.isDirectory()) await walk(path); else if (entry.isFile()) files.push(path); } }; for (const dir of _classPrivateFieldGet2(_dirs, this)) { const path = _assertClassBrand(_FileMirror_brand, this, _inside).call(this, dir); if (path !== void 0) await walk(path); } return files.sort(); } /** `path` under the root, or undefined if it would escape it. */ function _inside(path) { const full = resolve(_classPrivateFieldGet2(_root, this), path); return full === _classPrivateFieldGet2(_root, this) || full.startsWith(_classPrivateFieldGet2(_root, this) + sep) ? full : void 0; } //#endregion //#region src/harness/container/runtime/cli-adapter.ts /** * An adapter for any agent CLI that runs one turn per process and prints * JSON lines: Claude Code (`claude -p --output-format stream-json`), Codex * (`codex exec --json`), or your own. * * A CLI is described by three things: * * - `command`: the argv (and stdin) for one turn, given the prompt, the * session's settings, and the state earlier turns reported, such as the * CLI's own session id to resume. * - `parser`: turns each stdout line into `ContainerEvent`s, state updates, * and the turn's outcome. * - `stateDirs`: where the CLI keeps its sessions, relative to `$HOME`. * * Each harness session gets its own `$HOME`, so sessions sharing a * container never see each other's CLI state. The working directory is * shared. Everything the CLI writes under `stateDirs`, and every state * update, is persisted to the Durable Object while the turn runs; in a new * container both are restored before the first turn, so the CLI resumes * its own session. Forks need nothing special: a fork's home starts as a * copy of its parent's. */ /** How long a CLI has to exit after an interrupt before it is killed. */ const KILL_AFTER_MS = 5e3; /** The session state folded from persisted `{ kind: "state" }` entries. */ function foldState(entries) { const state = {}; for (const entry of entries) { if (!isJsonObject(entry) || entry.kind !== "state") continue; const values = entry.values; if (!isJsonObject(values)) continue; for (const [key, value] of Object.entries(values)) if (typeof value === "string") state[key] = value; } return state; } /** Give a directory tree to a user. */ async function chownTree(path, user) { await chown(path, user.uid, user.gid); const entries = await readdir(path, { withFileTypes: true }).catch(() => []); for (const entry of entries) { const child = join(path, entry.name); if (entry.isDirectory()) await chownTree(child, user); else await chown(child, user.uid, user.gid).catch(() => void 0); } } /** * Copy a skeleton home into a session home, keeping what is already there * and leaving out the session homes themselves when they live inside it. * (`fs.cp` refuses to copy a directory into its own subdirectory, filter * or not, so this walks the tree itself.) */ async function seed(skeleton, home, homes) { const excluded = resolve(homes); const walk = async (from, to) => { const entries = await readdir(from, { withFileTypes: true }).catch(() => []); for (const entry of entries) { const source = join(from, entry.name); const path = resolve(source); if (path === excluded || path.startsWith(excluded + sep)) continue; const target = join(to, entry.name); if (entry.isDirectory()) { await mkdir(target, { recursive: true }); await walk(source, target); } else if (entry.isSymbolicLink()) await symlink(await readlink(source), target).catch(() => void 0); else if (entry.isFile()) await copyFile(source, target, constants.COPYFILE_EXCL).catch(() => void 0); } }; await walk(resolve(skeleton), home); } function homeFor(root, session) { return join(root, createHash("sha256").update(session).digest("hex").slice(0, 32)); } var _options = /* @__PURE__ */ new WeakMap(); var _context = /* @__PURE__ */ new WeakMap(); var _home = /* @__PURE__ */ new WeakMap(); var _mirror = /* @__PURE__ */ new WeakMap(); var _state = /* @__PURE__ */ new WeakMap(); var _settings = /* @__PURE__ */ new WeakMap(); var _syncing = /* @__PURE__ */ new WeakMap(); var _CliSession_brand = /* @__PURE__ */ new WeakSet(); var CliSession = class { constructor(options, context, home, mirror) { _classPrivateMethodInitSpec(this, _CliSession_brand); _classPrivateFieldInitSpec(this, _options, void 0); _classPrivateFieldInitSpec(this, _context, void 0); _classPrivateFieldInitSpec(this, _home, void 0); _classPrivateFieldInitSpec(this, _mirror, void 0); _classPrivateFieldInitSpec(this, _state, void 0); _classPrivateFieldInitSpec(this, _settings, void 0); _classPrivateFieldInitSpec(this, _syncing, Promise.resolve()); _classPrivateFieldSet2(_options, this, options); _classPrivateFieldSet2(_context, this, context); _classPrivateFieldSet2(_home, this, home); _classPrivateFieldSet2(_mirror, this, mirror); _classPrivateFieldSet2(_state, this, foldState(context.restore)); _classPrivateFieldSet2(_settings, this, context.settings); } async run(turn) { const cliTurn = { operationId: turn.operationId, prompt: inputText(turn.input), settings: _classPrivateFieldGet2(_settings, this), state: _classPrivateFieldGet2(_state, this) }; const command = _classPrivateFieldGet2(_options, this).command(cliTurn); const parser = _classPrivateFieldGet2(_options, this).parser(cliTurn); const [file, ...args] = command.argv; if (file === void 0) throw new Error("The CLI command is empty"); const user = _classPrivateFieldGet2(_options, this).user; const child = spawn(file, args, { cwd: _classPrivateFieldGet2(_options, this).cwd ?? "/workspace", env: { ...process.env, ...command.env, HOME: _classPrivateFieldGet2(_home, this) }, stdio: [ "pipe", "pipe", "pipe" ], ...user ? { uid: user.uid, gid: user.gid } : {} }); child.stdin.on("error", () => void 0); child.stdin.end(command.stdin ?? ""); let stderr = ""; child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk) => { stderr = (stderr + chunk).slice(-4096); }); let outcome; createInterface({ input: child.stdout }).on("line", (text) => { if (text.trim() === "") return; let update; try { update = parser.line(text); } catch (error) { _classPrivateFieldGet2(_context, this).log("warn", `unreadable CLI output: ${String(error)}`); return; } for (const event of update.events ?? []) _classPrivateFieldGet2(_context, this).emit(event); if (update.state) _assertClassBrand(_CliSession_brand, this, _remember).call(this, update.state); if (update.outcome) outcome = update.outcome; }); const onAbort = () => { child.kill("SIGINT"); setTimeout(() => child.kill("SIGKILL"), KILL_AFTER_MS).unref(); }; turn.signal.addEventListener("abort", onAbort, { once: true }); const sync = setInterval(() => void _assertClassBrand(_CliSession_brand, this, _sync).call(this), _classPrivateFieldGet2(_options, this).syncIntervalMs ?? 5e3); const code = await new Promise((resolve) => { child.on("error", (error) => { stderr += `\n${error.message}`; resolve(null); }); child.on("close", (exitCode) => resolve(exitCode)); }); clearInterval(sync); turn.signal.removeEventListener("abort", onAbort); await _assertClassBrand(_CliSession_brand, this, _sync).call(this); if (turn.signal.aborted) return { status: "unanswered", reason: "aborted" }; return outcome ?? parser.end({ code, stderr }); } configure(settings) { _classPrivateFieldSet2(_settings, this, settings); } async close() { await _classPrivateFieldGet2(_syncing, this); } }; function _remember(update) { if (!Object.entries(update).some(([key, value]) => _classPrivateFieldGet2(_state, this)[key] !== value)) return; _classPrivateFieldSet2(_state, this, { ..._classPrivateFieldGet2(_state, this), ...update }); _classPrivateFieldGet2(_context, this).persist([{ kind: "state", values: { ...update } }]); } function _sync() { _classPrivateFieldSet2(_syncing, this, _classPrivateFieldGet2(_syncing, this).then(async () => { try { const entries = await _classPrivateFieldGet2(_mirror, this).collect(); if (entries.length > 0) _classPrivateFieldGet2(_context, this).persist(entries); } catch (error) { _classPrivateFieldGet2(_context, this).log("warn", `state sync failed: ${String(error)}`); } })); return _classPrivateFieldGet2(_syncing, this); } /** * An adapter for an agent CLI. * * @param options - The CLI's command, parser, and state directories. * @returns The adapter. * * @experimental The API may change before it stabilizes. */ function cliAdapter(options) { const homes = options.homes ?? join(homedir(), ".harness", "sessions"); return { id: options.id, version: options.version ?? "1", capabilities: [ "resume", "fork", "model" ], async open(context) { const home = homeFor(homes, context.session); await mkdir(home, { recursive: true }); if (options.skeleton) await seed(options.skeleton, home, homes); const mirror = new FileMirror(home, options.stateDirs ?? []); await mirror.restore(context.restore); if (options.user) await chownTree(home, options.user); return new CliSession(options, context, home, mirror); } }; } //#endregion //#region src/harness/container/runtime/presets/claude-code-events.ts function json(value) { if (value === void 0) return null; try { return JSON.parse(JSON.stringify(value)); } catch { return String(value); } } function toolOutput(content) { if (typeof content === "string") return content; if (Array.isArray(content)) { const texts = content.map((block) => typeof block === "object" && block !== null && "type" in block && block.type === "text" && "text" in block && typeof block.text === "string" ? block.text : void 0).filter((text) => text !== void 0); if (texts.length === content.length) return texts.join("\n"); } return json(content); } var _drafts = /* @__PURE__ */ new WeakMap(); var _toolOwners = /* @__PURE__ */ new WeakMap(); var _seen = /* @__PURE__ */ new WeakMap(); var _streaming = /* @__PURE__ */ new WeakMap(); var _now = /* @__PURE__ */ new WeakMap(); var _ClaudeProjection_brand = /* @__PURE__ */ new WeakSet(); /** The projection state of one adapter session. */ var ClaudeProjection = class { /** * @param now - The clock, for message timestamps. */ constructor(now = Date.now) { _classPrivateMethodInitSpec(this, _ClaudeProjection_brand); _classPrivateFieldInitSpec(this, _drafts, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _toolOwners, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _seen, /* @__PURE__ */ new Set()); _classPrivateFieldInitSpec(this, _streaming, void 0); _classPrivateFieldInitSpec(this, _now, void 0); _classPrivateFieldSet2(_now, this, now); } /** * The events one SDK message produces. * * @param message - The SDK message. * @param operationId - The operation it belongs to. * @returns The events, in order. */ project(message, operationId) { if (message.parent_tool_use_id) return []; switch (message.type) { case "stream_event": return _assertClassBrand(_ClaudeProjection_brand, this, _stream).call(this, message); case "assistant": return _assertClassBrand(_ClaudeProjection_brand, this, _assistant).call(this, message, operationId); case "user": return _assertClassBrand(_ClaudeProjection_brand, this, _toolResults).call(this, message); case "result": return _assertClassBrand(_ClaudeProjection_brand, this, _usage).call(this, message); default: return []; } } /** * How a turn ended, from its `result` message. * * @param message - The result message. * @param aborted - Whether the turn was interrupted. * @returns The outcome. */ outcome(message, aborted) { if (aborted) return { status: "unanswered", reason: "aborted" }; if (message.subtype === "success" && message.is_error !== true) return { status: "done", text: message.result ?? "" }; const detail = message.errors?.join("; ") || message.result; return { status: "unanswered", reason: [message.subtype && message.subtype !== "success" ? message.subtype : "", detail].filter((part) => part).join(": ") || "error" }; } }; function _stream(message) { const event = message.event; if (!event) return []; if (event.type === "message_start") { _classPrivateFieldSet2(_streaming, this, event.message?.id); return []; } const messageId = _classPrivateFieldGet2(_streaming, this); if (event.type !== "content_block_delta" || !messageId || !event.delta) return []; if (event.delta.type === "text_delta" && event.delta.text) return [{ type: "text-delta", messageId, delta: event.delta.text }]; if (event.delta.type === "thinking_delta" && event.delta.thinking) return [{ type: "reasoning-delta", messageId, delta: event.delta.thinking }]; return []; } function _assistant(message, operationId) { const id = message.message?.id; const content = message.message?.content; if (!id || !Array.isArray(content)) return []; if (message.uuid !== void 0) { if (_classPrivateFieldGet2(_seen, this).has(message.uuid)) return []; _classPrivateFieldGet2(_seen, this).add(message.uuid); } let draft = _classPrivateFieldGet2(_drafts, this).get(id); if (!draft) { draft = { id, operationId, createdAt: _classPrivateFieldGet2(_now, this).call(this), parts: [] }; _classPrivateFieldGet2(_drafts, this).set(id, draft); } for (const block of content) if (block.type === "text" && block.text) draft.parts.push({ type: "text", text: block.text }); else if (block.type === "thinking" && block.thinking) draft.parts.push({ type: "reasoning", text: block.thinking }); else if (block.type === "tool_use" && block.id) { draft.parts.push({ type: "tool", toolCallId: block.id, toolName: block.name ?? "tool", input: json(block.input), state: "running" }); _classPrivateFieldGet2(_toolOwners, this).set(block.id, id); } return [{ type: "message", message: _assertClassBrand(_ClaudeProjection_brand, this, _message).call(this, draft) }]; } function _toolResults(message) { const content = message.message?.content; if (!Array.isArray(content)) return []; const changed = /* @__PURE__ */ new Set(); for (const block of content) { if (block.type !== "tool_result" || !block.tool_use_id) continue; const owner = _classPrivateFieldGet2(_toolOwners, this).get(block.tool_use_id); const draft = owner === void 0 ? void 0 : _classPrivateFieldGet2(_drafts, this).get(owner); if (!draft) continue; draft.parts = draft.parts.map((part) => part.type === "tool" && part.toolCallId === block.tool_use_id ? { ...part, state: block.is_error ? "error" : "done", output: toolOutput(block.content) } : part); changed.add(draft); } return [...changed].map((draft) => ({ type: "message", message: _assertClassBrand(_ClaudeProjection_brand, this, _message).call(this, draft) })); } function _usage(message) { return [{ type: "usage", ...message.usage?.input_tokens === void 0 ? {} : { inputTokens: message.usage.input_tokens }, ...message.usage?.output_tokens === void 0 ? {} : { outputTokens: message.usage.output_tokens }, ...message.total_cost_usd === void 0 ? {} : { costUsd: message.total_cost_usd } }]; } function _message(draft) { return { id: draft.id, role: "assistant", parts: [...draft.parts], ...draft.operationId === void 0 ? {} : { operationId: draft.operationId }, createdAt: draft.createdAt }; } //#endregion //#region src/harness/container/runtime/presets/claude-code.ts function parseOptions$1(value) { if (!isJsonObject(value)) return {}; const allowed = stringsOf(value.allowedTools); const disallowed = stringsOf(value.disallowedTools); return { ...typeof value.appendSystemPrompt === "string" ? { appendSystemPrompt: value.appendSystemPrompt } : {}, ...allowed ? { allowedTools: allowed } : {}, ...disallowed ? { disallowedTools: disallowed } : {}, ...typeof value.maxTurns === "number" ? { maxTurns: value.maxTurns } : {} }; } function parseLine$1(text) { const value = JSON.parse(text); if (typeof value !== "object" || value === null || !("type" in value)) return; return value; } /** The Claude Code preset's description, for `cliAdapter`. */ const claudeCodeCli = { id: "claude-code", stateDirs: [".claude/projects"], command(turn) { const options = parseOptions$1(turn.settings.options); const argv = [ "claude", "-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--dangerously-skip-permissions" ]; if (turn.settings.model) argv.push("--model", turn.settings.model); if (turn.state.sessionId) argv.push("--resume", turn.state.sessionId); if (options.appendSystemPrompt) argv.push("--append-system-prompt", options.appendSystemPrompt); if (options.allowedTools?.length) argv.push("--allowedTools", options.allowedTools.join(",")); if (options.disallowedTools?.length) argv.push("--disallowedTools", options.disallowedTools.join(",")); if (options.maxTurns !== void 0) argv.push("--max-turns", String(options.maxTurns)); return { argv, stdin: turn.prompt, env: { DISABLE_AUTOUPDATER: "1" } }; }, parser(turn) { const projection = new ClaudeProjection(); return { line(text) { const message = parseLine$1(text); if (!message) return {}; return { events: projection.project(message, turn.operationId), ...message.session_id ? { state: { sessionId: message.session_id } } : {}, ...message.type === "result" ? { outcome: projection.outcome(message, false) } : {} }; }, end({ code, stderr }) { return { status: "unanswered", reason: `claude exited with ${code ?? "a signal"}: ${stderr.trim() || "no output"}` }; } }; } }; //#endregion //#region src/harness/container/runtime/presets/codex.ts function parseLine(text) { const value = JSON.parse(text); if (typeof value !== "object" || value === null) return void 0; return value; } function toolState(status) { if (status === "completed") return "done"; if (status === "failed" || status === "declined") return "error"; return "running"; } /** One item as a transcript part, or undefined for items that are not. */ function partOf(item) { const id = item.id ?? "item"; const state = toolState(item.status); switch (item.type) { case "agent_message": return { type: "text", text: item.text ?? "" }; case "reasoning": return { type: "reasoning", text: item.text ?? "" }; case "command_execution": return { type: "tool", toolCallId: id, toolName: "shell", input: { command: item.command ?? "" }, state: state === "done" && item.exit_code !== 0 && item.exit_code != null ? "error" : state, ...item.aggregated_output ? { output: item.aggregated_output } : {} }; case "file_change": return { type: "tool", toolCallId: id, toolName: "file_change", input: item.changes ?? null, state }; case "mcp_tool_call": return { type: "tool", toolCallId: id, toolName: `${item.server ?? "mcp"}.${item.tool ?? "tool"}`, input: item.arguments ?? null, state, ...item.result !== void 0 || item.error !== void 0 ? { output: item.result ?? item.error ?? null } : {} }; case "web_search": return { type: "tool", toolCallId: id, toolName: "web_search", input: { query: item.query ?? "" }, state }; case "todo_list": return { type: "tool", toolCallId: id, toolName: "todo_list", input: item.items ?? null, state }; default: return; } } function parseOptions(value) { if (!isJsonObject(value)) return {}; const config = stringsOf(value.config); return config ? { config } : {}; } /** The Codex preset's description, for `cliAdapter`. */ const codexCli = { id: "codex", stateDirs: [".codex/sessions"], command(turn) { const argv = ["codex", "exec"]; if (turn.state.threadId) argv.push("resume", turn.state.threadId); argv.push("--json", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"); const baseUrl = process.env.OPENAI_BASE_URL; if (baseUrl) argv.push("-c", "model_provider=gateway", "-c", `model_providers.gateway={name="AI Gateway",base_url=${JSON.stringify(baseUrl)},env_key="OPENAI_API_KEY",wire_api="responses"}`); for (const config of parseOptions(turn.settings.options).config ?? []) argv.push("-c", config); if (turn.settings.model) argv.push("-m", turn.settings.model); argv.push("-"); return { argv, stdin: turn.prompt }; }, parser(turn) { const messageId = `${turn.operationId}:assistant`; const createdAt = Date.now(); const parts = /* @__PURE__ */ new Map(); let lastText = ""; let failure; const message = () => { return { type: "message", message: { id: messageId, role: "assistant", parts: [...parts.values()], operationId: turn.operationId, createdAt } }; }; return { line(text) { const event = parseLine(text); if (!event) return {}; switch (event.type) { case "thread.started": return event.thread_id ? { state: { threadId: event.thread_id } } : {}; case "item.started": case "item.updated": case "item.completed": { const item = event.item; if (!item) return {}; if (item.type === "error") return { events: [{ type: "log", level: "warn", message: item.message ?? "error" }] }; const part = partOf(item); if (!part) return {}; parts.set(item.id ?? String(parts.size), part); if (part.type === "text" && event.type === "item.completed") lastText = part.text; return { events: [message()] }; } case "turn.completed": return { events: [{ type: "usage", ...event.usage?.input_tokens === void 0 ? {} : { inputTokens: event.usage.input_tokens }, ...event.usage?.output_tokens === void 0 ? {} : { outputTokens: event.usage.output_tokens } }], outcome: { status: "done", text: lastText } }; case "turn.failed": return { outcome: { status: "unanswered", reason: event.error?.message ?? "turn failed" } }; case "error": failure = event.message; return { events: [{ type: "log", level: "warn", message: event.message ?? "error" }] }; default: return {}; } }, end({ code, stderr }) { return { status: "unanswered", reason: `codex exited with ${code ?? "a signal"}: ${failure ?? (stderr.trim() || "no output")}` }; } }; } }; //#endregion export { claudeCodeCli, cliAdapter, codexCli, serve, serveFromEnv }; //# sourceMappingURL=index.js.map