@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
JavaScript
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