agents
Version:
A home for your AI agents
1,082 lines (1,081 loc) • 37.9 kB
JavaScript
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