UNPKG

@tanstack/ai-sandbox

Version:

Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.

131 lines (130 loc) • 4.78 kB
import { withSandbox } from "./middleware.js"; import { sandboxWorkspaceBackend } from "./workspace-backend.js"; import { EventType, defineAgent } from "@tanstack/ai"; import { defineCommand, definePlugin } from "@tanstack/ai-harness"; //#region src/harness.ts var PLAN_DEFAULTS = { "claude-code": { permissionMode: "plan" }, codex: { sandboxMode: "read-only" } }; var TASK_SCHEMA = { type: "object", properties: { task: { type: "string", description: "The whole task: what to change, where, and how to check it. The agent sees only this text and the workspace." } }, required: ["task"] }; function isRecord(value) { return typeof value === "object" && value !== null; } /** * Run `open` while holding the lock for `key`. The lock is taken when the * stream is first read, so a stream nobody reads never holds it. */ async function* serialized(tails, key, open) { const previous = tails.get(key) ?? Promise.resolve(); let release = () => {}; const current = new Promise((resolve) => release = resolve); const tail = previous.then(() => current); tails.set(key, tail); await previous; try { yield* open(); } finally { release(); if (tails.get(key) === tail) tails.delete(key); } } /** * A harness plugin that lets the lead model hand coding work to Claude Code, * Codex, Grok Build, or any ACP agent: * * - each agent runs in `sandbox`, so nobody wires `withSandbox` by hand; * - each agent keeps its own coding session per thread, also after a restart * (`/fresh [agent]` starts over); * - the harness `plan` mode starts the agents read-only. * * @example * ```ts * codingAgents({ * sandbox: defineSandbox({ id: 'code', provider: localProcessSandbox() }), * agents: { * claude_code: { adapter: claudeCodeText('claude-opus-4-8'), description: 'Large refactors' }, * codex: { adapter: codexText('gpt-5.5'), description: 'Quick fixes and tests' }, * }, * }) * ``` */ function codingAgents(options) { const shared = (options.workspace ?? "shared") === "shared"; const sandboxOptions = options.sandboxOptions ?? {}; const tails = /* @__PURE__ */ new Map(); const names = Object.keys(options.agents); return definePlugin({ name: "tanstack/coding-agents", setup: (ctx) => { const state = ctx.state({ sessions: {} }); const capture = (agent) => ({ name: "tanstack/coding-agents:session", onChunk: async (_run, chunk) => { if (chunk.type !== EventType.CUSTOM) return; if (!chunk.name.endsWith(".session-id")) return; const value = chunk.value; if (!isRecord(value) || typeof value.sessionId !== "string") return; const sessionId = value.sessionId; await state.update((current) => ({ sessions: { ...current.sessions, [agent]: sessionId } })); } }); return { subagents: Object.entries(options.agents).map(([name, config]) => defineAgent({ name, description: config.description, inputSchema: TASK_SCHEMA, run: async (run) => { const input = run.input; const task = isRecord(input) && typeof input.task === "string" ? input.task : ""; const plan = ctx.config.get("mode") === "plan" ? config.planModelOptions ?? PLAN_DEFAULTS[config.adapter.name] : {}; if (plan === void 0) throw new Error(`${name} cannot run in plan mode: it has no planModelOptions.`); const thread = ctx.session.threadId; const threadId = shared ? thread : `${thread}:${name}`; async function* open() { const sessionId = (await state.get()).sessions[name]; const modelOptions = { ...config.modelOptions, ...plan, ...sessionId ? { sessionId } : {} }; yield* run.chat({ adapter: config.adapter, messages: [{ role: "user", content: task }], threadId, ...Object.keys(modelOptions).length > 0 ? { modelOptions } : {}, middleware: [withSandbox(options.sandbox, sandboxOptions), capture(name)] }); } return serialized(tails, threadId, open); } })), commands: { fresh: defineCommand({ description: `Start new coding-agent sessions (all, or one of: ${names.join(", ")})`, run: async (input) => { const name = typeof input === "string" ? input.trim() : ""; if (name && !names.includes(name)) return `Unknown coding agent "${name}". Agents: ${names.join(", ")}.`; await state.update((current) => ({ sessions: name ? Object.fromEntries(Object.entries(current.sessions).filter(([agent]) => agent !== name)) : {} })); return name ? `${name} starts a new session next time.` : "Every coding agent starts a new session next time."; } }) } }; } }); } //#endregion export { codingAgents, sandboxWorkspaceBackend }; //# sourceMappingURL=harness.js.map