@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.
87 lines (86 loc) • 3.38 kB
JavaScript
//#region src/remote-tools.ts
/** Narrow an unknown body into a {@link ToolExecRequest} (project rule: no `as`). */
function isToolExecRequest(value) {
return value !== null && typeof value === "object" && "name" in value && typeof value.name === "string";
}
/**
* Rebuild `chat()` tool objects (container side) from serialized descriptors.
* Each stub advertises the descriptor's JSON-schema and delegates `execute` to
* the executor; the harness adapter bridges them like any other tool. The
* harness's `abortSignal` is forwarded so a cancelled run cancels the in-flight
* remote call too.
*/
function remoteToolStubs(descriptors, executor) {
return descriptors.map((descriptor) => ({
name: descriptor.name,
description: descriptor.description ?? "",
inputSchema: descriptor.inputSchema,
execute: (args, options) => executor.execute(descriptor.name, args, options?.abortSignal !== void 0 ? { signal: options.abortSignal } : {})
}));
}
/**
* Serialize `chat()` tools to wire descriptors to send into the container.
* `inputSchema` must already be a plain JSON-schema object (convert Standard
* Schemas before calling, the same way harness adapters advertise tools).
*/
function toolDescriptors(tools) {
return tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: isJsonSchemaObject(tool.inputSchema) ? tool.inputSchema : {
type: "object",
properties: {}
}
}));
}
function isJsonSchemaObject(value) {
return value !== null && typeof value === "object" && "type" in value && value.type === "object";
}
function isToolExecResponse(value) {
return value !== null && typeof value === "object" && "result" in value;
}
/**
* The default {@link RemoteToolExecutor}: POST `{ name, args }` (bearer-gated)
* to the orchestrator's tool-exec endpoint and return its `result`. A non-2xx
* or malformed response throws (surfaced to the agent as a failed tool call by
* the bridge) — never silently swallowed.
*/
function httpRemoteToolExecutor(url, token) {
return { async execute(name, args, options) {
const res = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${token}`
},
body: JSON.stringify({
name,
args
}),
...options?.signal !== void 0 ? { signal: options.signal } : {}
});
if (!res.ok) {
const text = await res.text();
throw new Error(`remote tool "${name}" failed: ${res.status} ${text.slice(0, 200)}`);
}
const body = await res.json();
if (!isToolExecResponse(body)) throw new Error(`remote tool "${name}": malformed orchestrator response`);
return body.result;
} };
}
/**
* Run a host tool by name with the given args, returning its raw result
* (orchestrator side of {@link httpRemoteToolExecutor}). Throws for an unknown
* tool or one with no `execute` — the orchestrator surfaces that as a 4xx/5xx.
*/
function executeHostTool(tools, name, args, options = {}) {
const tool = tools.find((candidate) => candidate.name === name);
if (!tool?.execute) return Promise.reject(/* @__PURE__ */ new Error(`Unknown tool: ${name}`));
return Promise.resolve(tool.execute(args ?? {}, {
context: options.context,
abortSignal: options.signal
}));
}
//#endregion
export { executeHostTool, httpRemoteToolExecutor, isToolExecRequest, remoteToolStubs, toolDescriptors };
//# sourceMappingURL=remote-tools.js.map