mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 11:01:34 +00:00
feat(code-mode): first-class nodes API (#114877)
* feat(agents): add nodes to code mode * fix(agents): add code mode nodes discovery hint
This commit is contained in:
committed by
GitHub
parent
b244adf430
commit
ea707382fd
@@ -565,6 +565,23 @@ type ToolCatalog = {
|
||||
};
|
||||
```
|
||||
|
||||
Paired Gateway nodes are available through the `nodes` global:
|
||||
|
||||
```typescript
|
||||
const available = await nodes.list();
|
||||
const node = await nodes.get(available[0].id);
|
||||
const status = await node.invoke("device.status");
|
||||
```
|
||||
|
||||
`nodes.list()` returns paired node ids, names, platforms, connection state, and
|
||||
advertised commands. `nodes.get(idOrName)` resolves an exact id before a display
|
||||
name and returns a handle with `id`, `name`, and `invoke(command, params?)`.
|
||||
Invocation uses the normal `nodes` tool path, so pairing, command policy, scopes,
|
||||
approvals, timeouts, hooks, and telemetry are unchanged. A handle includes
|
||||
`listDir(path)` only when the node advertises `fs.listDir`. It does not include
|
||||
`exec`: the generic nodes surface reserves `system.run` for the normal shell
|
||||
`exec` tool with a node host.
|
||||
|
||||
Convenience tool functions are installed only for unambiguous safe names:
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { NODE_FS_LIST_DIR_COMMAND } from "../infra/node-commands.js";
|
||||
import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
|
||||
import { parseNodeList } from "../shared/node-list-parse.js";
|
||||
import type { NodeListNode } from "../shared/node-list-types.js";
|
||||
import { toCodeModeJsonSafe } from "./code-mode-json.js";
|
||||
import type { CodeModeNamespaceRuntime } from "./code-mode-namespaces.js";
|
||||
import {
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
type CollectorCompletionResult,
|
||||
} from "./tools/agents-wait-tool.js";
|
||||
import { ToolInputError } from "./tools/common.js";
|
||||
import { resolveEligibleNodeFromList } from "./tools/nodes-utils.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./tools/sessions-helpers.js";
|
||||
|
||||
type CodeModeSwarmDeps = {
|
||||
@@ -39,6 +43,124 @@ const defaultCodeModeSwarmDeps: CodeModeSwarmDeps = {
|
||||
waitForCollectorCompletion,
|
||||
};
|
||||
|
||||
const CODE_MODE_NODES_TOOL_ID = "openclaw:core:nodes";
|
||||
|
||||
type CodeModeNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
platform?: string;
|
||||
connected: boolean;
|
||||
commands: string[];
|
||||
};
|
||||
|
||||
function projectCodeModeNode(node: NodeListNode): CodeModeNode {
|
||||
return {
|
||||
id: node.nodeId,
|
||||
name: node.displayName?.trim() || node.nodeId,
|
||||
...(node.platform ? { platform: node.platform } : {}),
|
||||
connected: node.connected === true,
|
||||
commands: Array.isArray(node.commands)
|
||||
? node.commands.filter((command): command is string => typeof command === "string")
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function callNodesTool(params: {
|
||||
runtime: ToolSearchRuntime;
|
||||
parentToolCallId: string;
|
||||
signal?: AbortSignal;
|
||||
onUpdate?: AgentToolUpdateCallback;
|
||||
input: Record<string, unknown>;
|
||||
}): Promise<unknown> {
|
||||
return await params.runtime.callValue(CODE_MODE_NODES_TOOL_ID, params.input, {
|
||||
includeMcp: false,
|
||||
parentToolCallId: params.parentToolCallId,
|
||||
signal: params.signal,
|
||||
onUpdate: params.onUpdate,
|
||||
recoverySurface: "tools",
|
||||
});
|
||||
}
|
||||
|
||||
async function listCodeModeNodes(params: {
|
||||
runtime: ToolSearchRuntime;
|
||||
parentToolCallId: string;
|
||||
signal?: AbortSignal;
|
||||
onUpdate?: AgentToolUpdateCallback;
|
||||
}): Promise<NodeListNode[]> {
|
||||
return parseNodeList(
|
||||
await callNodesTool({
|
||||
...params,
|
||||
input: { action: "status" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function runNodesBridge(params: {
|
||||
runtime: ToolSearchRuntime;
|
||||
parentToolCallId: string;
|
||||
request: PendingBridgeRequest;
|
||||
signal?: AbortSignal;
|
||||
onUpdate?: AgentToolUpdateCallback;
|
||||
}): Promise<unknown> {
|
||||
const values = params.request.args;
|
||||
const action = values[0];
|
||||
if (action === "list") {
|
||||
return (await listCodeModeNodes(params))
|
||||
.filter((node) => node.paired === true)
|
||||
.map(projectCodeModeNode);
|
||||
}
|
||||
if (action === "get") {
|
||||
const query = values[1];
|
||||
if (typeof query !== "string" || !query.trim()) {
|
||||
throw new ToolInputError("nodes.get id or name must be a non-empty string.");
|
||||
}
|
||||
const node = resolveEligibleNodeFromList(
|
||||
await listCodeModeNodes(params),
|
||||
query,
|
||||
(candidate) => candidate.paired === true,
|
||||
{
|
||||
ineligibleExact: (id, eligibleIds) =>
|
||||
`node "${id}" is not paired (paired node ids: ${eligibleIds})`,
|
||||
nameResolveFailed: (reason, eligibleIds) => `${reason} (paired node ids: ${eligibleIds})`,
|
||||
noneEligible: () => "no paired nodes",
|
||||
multipleEligible: (eligible) =>
|
||||
`multiple nodes paired: ${eligible
|
||||
.map((candidate) => candidate.nodeId)
|
||||
.toSorted()
|
||||
.join(", ")}`,
|
||||
},
|
||||
);
|
||||
const projected = projectCodeModeNode(node);
|
||||
return {
|
||||
id: projected.id,
|
||||
name: projected.name,
|
||||
...(projected.commands.includes(NODE_FS_LIST_DIR_COMMAND)
|
||||
? { listDirCommand: NODE_FS_LIST_DIR_COMMAND }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
if (action === "invoke") {
|
||||
const node = values[1];
|
||||
const command = values[2];
|
||||
if (typeof node !== "string" || !node.trim()) {
|
||||
throw new ToolInputError("nodes.invoke node id must be a non-empty string.");
|
||||
}
|
||||
if (typeof command !== "string" || !command.trim()) {
|
||||
throw new ToolInputError("nodes.invoke command must be a non-empty string.");
|
||||
}
|
||||
return await callNodesTool({
|
||||
...params,
|
||||
input: {
|
||||
action: "invoke",
|
||||
node,
|
||||
invokeCommand: command,
|
||||
invokeParamsJson: JSON.stringify(values[3] ?? {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new ToolInputError("unsupported nodes bridge action.");
|
||||
}
|
||||
|
||||
let codeModeSwarmDeps = defaultCodeModeSwarmDeps;
|
||||
|
||||
export function codeModeReplayIdForToolCall(
|
||||
@@ -324,6 +446,10 @@ export async function runBridgeRequest(params: {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "nodes": {
|
||||
value = await runNodesBridge(params);
|
||||
break;
|
||||
}
|
||||
case "yield": {
|
||||
value = { status: "yielded", reason: values[0] ?? null };
|
||||
break;
|
||||
|
||||
284
src/agents/code-mode-nodes.test.ts
Normal file
284
src/agents/code-mode-nodes.test.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/** Tests the first-class Code Mode nodes API through the real generic nodes tool. */
|
||||
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ToolSearchCatalogToolExecutor } from "./tool-search.js";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
const gatewayMocks = vi.hoisted(() => ({
|
||||
callGatewayTool: vi.fn(),
|
||||
readGatewayCallOptions: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
vi.mock("./tools/gateway.js", () => ({
|
||||
callGatewayTool: gatewayMocks.callGatewayTool,
|
||||
readGatewayCallOptions: gatewayMocks.readGatewayCallOptions,
|
||||
}));
|
||||
|
||||
const nodeSnapshot = [
|
||||
{
|
||||
nodeId: "node-1",
|
||||
displayName: "Desk",
|
||||
platform: "darwin",
|
||||
paired: true,
|
||||
connected: true,
|
||||
commands: ["device.status", "fs.listDir", "computer.act", "danger.read"],
|
||||
},
|
||||
{
|
||||
nodeId: "shadow-id",
|
||||
displayName: "Offline",
|
||||
platform: "linux",
|
||||
paired: false,
|
||||
connected: false,
|
||||
commands: ["device.status"],
|
||||
},
|
||||
{
|
||||
nodeId: "node-2",
|
||||
displayName: "shadow-id",
|
||||
paired: true,
|
||||
connected: false,
|
||||
commands: ["device.status"],
|
||||
},
|
||||
];
|
||||
|
||||
let applyCodeModeCatalog: typeof import("./code-mode.js").applyCodeModeCatalog;
|
||||
let createCodeModeTools: typeof import("./code-mode.js").createCodeModeTools;
|
||||
let createToolSearchCatalogRef: typeof import("./tool-search.js").createToolSearchCatalogRef;
|
||||
let createNodesTool: typeof import("./tools/nodes-tool.js").createNodesTool;
|
||||
let testing: typeof import("./code-mode.test-support.js").testing;
|
||||
|
||||
function resultDetails(result: { details?: unknown }): Record<string, unknown> {
|
||||
expect(result.details).toBeDefined();
|
||||
expect(typeof result.details).toBe("object");
|
||||
return result.details as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function createHarness() {
|
||||
const catalogRef = createToolSearchCatalogRef();
|
||||
const config = { tools: { codeMode: true } } as never;
|
||||
const nestedCalls: Parameters<ToolSearchCatalogToolExecutor>[0][] = [];
|
||||
const executeTool: ToolSearchCatalogToolExecutor = async (params) => {
|
||||
nestedCalls.push(params);
|
||||
const result = await params.tool.execute(
|
||||
params.toolCallId,
|
||||
params.input,
|
||||
params.signal,
|
||||
params.onUpdate,
|
||||
undefined as never,
|
||||
);
|
||||
return await params.acceptResultBeforeProjection(result);
|
||||
};
|
||||
const ctx = {
|
||||
config,
|
||||
runtimeConfig: config,
|
||||
sessionId: "session-code-mode-nodes",
|
||||
sessionKey: "agent:main:main",
|
||||
runId: "run-code-mode-nodes",
|
||||
catalogRef,
|
||||
executeTool,
|
||||
};
|
||||
const codeModeTools = createCodeModeTools(ctx);
|
||||
const nodesTool = createNodesTool({ agentSessionKey: ctx.sessionKey });
|
||||
applyCodeModeCatalog({
|
||||
tools: [...codeModeTools, nodesTool],
|
||||
config,
|
||||
sessionId: ctx.sessionId,
|
||||
sessionKey: ctx.sessionKey,
|
||||
runId: ctx.runId,
|
||||
catalogRef,
|
||||
});
|
||||
return {
|
||||
execTool: expectDefined(codeModeTools[0], "code mode exec tool"),
|
||||
waitTool: expectDefined(codeModeTools[1], "code mode wait tool"),
|
||||
nestedCalls,
|
||||
};
|
||||
}
|
||||
|
||||
async function runUntilCompleted(params: {
|
||||
execTool: AnyAgentTool;
|
||||
waitTool: AnyAgentTool;
|
||||
code: string;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
let details = resultDetails(
|
||||
await params.execTool.execute("code-nodes-call", { code: params.code }),
|
||||
);
|
||||
for (let index = 0; index < 8 && details.status === "waiting"; index += 1) {
|
||||
details = resultDetails(
|
||||
await params.waitTool.execute(`code-nodes-wait-${index}`, { runId: details.runId }),
|
||||
);
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
describe("Code Mode nodes", () => {
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
({ applyCodeModeCatalog, createCodeModeTools } = await import("./code-mode.js"));
|
||||
({ createToolSearchCatalogRef } = await import("./tool-search.js"));
|
||||
({ createNodesTool } = await import("./tools/nodes-tool.js"));
|
||||
({ testing } = await import("./code-mode.test-support.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
gatewayMocks.callGatewayTool.mockReset();
|
||||
gatewayMocks.readGatewayCallOptions.mockReset();
|
||||
gatewayMocks.readGatewayCallOptions.mockReturnValue({});
|
||||
gatewayMocks.callGatewayTool.mockImplementation(async (method, _options, input) => {
|
||||
if (method === "node.list") {
|
||||
return { nodes: nodeSnapshot };
|
||||
}
|
||||
if (method === "node.invoke") {
|
||||
const invocation = input as {
|
||||
nodeId: string;
|
||||
command: string;
|
||||
params: unknown;
|
||||
};
|
||||
if (invocation.command === "danger.read") {
|
||||
throw new Error(
|
||||
'node command not allowed: "danger.read" is blocked by gateway.nodes.commands.deny',
|
||||
);
|
||||
}
|
||||
return {
|
||||
payload: {
|
||||
nodeId: invocation.nodeId,
|
||||
command: invocation.command,
|
||||
params: invocation.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected gateway method: ${String(method)}`);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testing.activeRuns.clear();
|
||||
testing.resumingRunIds.clear();
|
||||
});
|
||||
|
||||
it("lists nodes and returns a callable handle with conditional directory sugar", async () => {
|
||||
const harness = createHarness();
|
||||
const details = await runUntilCompleted({
|
||||
...harness,
|
||||
code: `
|
||||
const listed = await nodes.list();
|
||||
const node = await nodes.get("Desk");
|
||||
const invoked = await node.invoke("device.status", { detail: true });
|
||||
const directory = await node.listDir("/tmp");
|
||||
return {
|
||||
listed,
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
methods: Object.keys(node).sort(),
|
||||
invoked,
|
||||
directory,
|
||||
hasExec: "exec" in node,
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
expect(details.status).toBe("completed");
|
||||
expect(details.value).toEqual({
|
||||
listed: [
|
||||
{
|
||||
id: "node-1",
|
||||
name: "Desk",
|
||||
platform: "darwin",
|
||||
connected: true,
|
||||
commands: ["device.status", "fs.listDir", "computer.act", "danger.read"],
|
||||
},
|
||||
{
|
||||
id: "node-2",
|
||||
name: "shadow-id",
|
||||
connected: false,
|
||||
commands: ["device.status"],
|
||||
},
|
||||
],
|
||||
id: "node-1",
|
||||
name: "Desk",
|
||||
methods: ["id", "invoke", "listDir", "name"],
|
||||
invoked: {
|
||||
payload: { nodeId: "node-1", command: "device.status", params: { detail: true } },
|
||||
},
|
||||
directory: {
|
||||
payload: { nodeId: "node-1", command: "fs.listDir", params: { path: "/tmp" } },
|
||||
},
|
||||
hasExec: false,
|
||||
});
|
||||
expect(harness.nestedCalls).not.toHaveLength(0);
|
||||
expect(harness.nestedCalls.every((call) => call.parentToolCallId === "code-nodes-call")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("omits listDir when the node does not advertise fs.listDir", async () => {
|
||||
const harness = createHarness();
|
||||
const details = await runUntilCompleted({
|
||||
...harness,
|
||||
code: `const node = await nodes.get("node-2"); return Object.keys(node).sort();`,
|
||||
});
|
||||
|
||||
expect(details.value).toEqual(["id", "invoke", "name"]);
|
||||
});
|
||||
|
||||
it("surfaces the generic nodes policy refusal with the command name", async () => {
|
||||
const harness = createHarness();
|
||||
const details = await runUntilCompleted({
|
||||
...harness,
|
||||
code: `
|
||||
const node = await nodes.get("Desk");
|
||||
try {
|
||||
await node.invoke("danger.read");
|
||||
return "unexpected success";
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(details.value).toContain('node command not allowed: "danger.read"');
|
||||
expect(details.value).toContain("blocked by gateway.nodes.commands.deny");
|
||||
});
|
||||
|
||||
it("keeps computer.act blocked by the generic nodes surface", async () => {
|
||||
const harness = createHarness();
|
||||
const details = await runUntilCompleted({
|
||||
...harness,
|
||||
code: `
|
||||
const node = await nodes.get("Desk");
|
||||
try {
|
||||
await node.invoke("computer.act", { action: "left_click", x: 1, y: 1 });
|
||||
return "unexpected success";
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(details.value).toContain('invokeCommand "computer.act"');
|
||||
expect(details.value).toContain("use the dedicated computer tool");
|
||||
expect(
|
||||
gatewayMocks.callGatewayTool.mock.calls.some(
|
||||
([method, , input]) =>
|
||||
method === "node.invoke" &&
|
||||
(input as { command?: string } | undefined)?.command === "computer.act",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects an ineligible exact id before matching an eligible node name", async () => {
|
||||
const harness = createHarness();
|
||||
const details = await runUntilCompleted({
|
||||
...harness,
|
||||
code: `
|
||||
try {
|
||||
await nodes.get("shadow-id");
|
||||
return "unexpected success";
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(details.value).toBe('node "shadow-id" is not paired (paired node ids: node-1, node-2)');
|
||||
});
|
||||
});
|
||||
@@ -55,6 +55,7 @@ type CodeModeBridgeMethod =
|
||||
| "describe"
|
||||
| "call"
|
||||
| "callValue"
|
||||
| "nodes"
|
||||
| "yield"
|
||||
| "namespace"
|
||||
| "agentSpawn"
|
||||
|
||||
@@ -6,6 +6,7 @@ type CodeModeBridgeMethod =
|
||||
| "describe"
|
||||
| "call"
|
||||
| "callValue"
|
||||
| "nodes"
|
||||
| "yield"
|
||||
| "namespace"
|
||||
| "agentSpawn"
|
||||
|
||||
@@ -439,6 +439,12 @@ describe("Code Mode", () => {
|
||||
expect(execTool.description).toContain('"javascript" or "typescript"');
|
||||
expect(execTool.description).toContain("never a shell command");
|
||||
expect(execTool.description).toContain("do not retry failed shell source");
|
||||
const nodesGuidance =
|
||||
"- nodes: paired Gateway nodes; nodes.list(), (await nodes.get(id)).invoke(command, params)";
|
||||
expect(execTool.description).toContain(nodesGuidance);
|
||||
expect(execTool.description.indexOf(nodesGuidance)).toBe(
|
||||
execTool.description.lastIndexOf(nodesGuidance),
|
||||
);
|
||||
|
||||
expect(parameters.properties?.code?.description).toContain(
|
||||
"`tools.search` takes a query string, not an object",
|
||||
|
||||
@@ -144,12 +144,15 @@ function createCodeModeExecDescription(
|
||||
const swarmGuidance = swarmEnabled
|
||||
? " Swarm globals `agents.run`, `phase`, and `log` are available; read `agents.d.ts` for types and orchestration idioms."
|
||||
: "";
|
||||
const nodesGuidance =
|
||||
"\n- nodes: paired Gateway nodes; nodes.list(), (await nodes.get(id)).invoke(command, params)\n";
|
||||
const catalogIndex = catalog ? formatCodeModeCatalogIndex(catalog) : "";
|
||||
return (
|
||||
"Run JavaScript or TypeScript in OpenClaw code mode. Use `return` to pass the final value back; otherwise the result is `null`. Quick-index arrows show trusted declared output hints; `-> ?` means never guess result field names. For declared fields, process them in the first exec; do not spend another exec inspecting them. Perform dependent reads, checks, and follow-up calls in order; parallelize independent work only. For an unknown output, including a final dependent call after declared-output calls, return the raw tool value unchanged; do not wrap it in the requested answer shape or guess fields; filter or map it only in a later exec. Nested calls enforce normal tool policy and approvals. `ALL_TOOLS` is the complete compact catalog. Select exact ids directly or with `tools.search(query: string, options?)`; use `tools.describe(id: string)` only when needed. Never invent or transform a tool id. `tools.callValue(id: string, args?)` returns its JSON value directly; `tools.call(id: string, args?)` preserves `{ tool, result }`. Example: `const hit = ALL_TOOLS.find((entry) => entry.description.includes('weather')) ?? (await tools.search('weather'))[0]; return await tools.callValue(hit.id, {});`. Node.js modules and `require`/`import` are NOT available; use enabled catalog tools allowed by policy for shell, file, network, or external actions." +
|
||||
apiGuidance +
|
||||
mcpGuidance +
|
||||
swarmGuidance +
|
||||
nodesGuidance +
|
||||
' The `language` field accepts only "javascript" or "typescript"; do not pass "bash", "shell", or other values.' +
|
||||
" Both `code` and `command` contain JavaScript or TypeScript, never a shell command. " +
|
||||
"For shell or file operations, call the exact catalog tool from guest JavaScript; do not retry failed shell source." +
|
||||
|
||||
@@ -188,6 +188,30 @@ const CONTROLLER_SOURCE = String.raw`
|
||||
return true;
|
||||
}
|
||||
|
||||
function nodeHandle(descriptor) {
|
||||
const handle = Object.create(null);
|
||||
Object.defineProperties(handle, {
|
||||
id: { value: descriptor.id, enumerable: true },
|
||||
name: { value: descriptor.name, enumerable: true },
|
||||
invoke: {
|
||||
value: (command, params) => request("nodes", ["invoke", descriptor.id, command, params]),
|
||||
enumerable: true,
|
||||
},
|
||||
});
|
||||
if (typeof descriptor.listDirCommand === "string") {
|
||||
Object.defineProperty(handle, "listDir", {
|
||||
value: (path) => request("nodes", ["invoke", descriptor.id, descriptor.listDirCommand, { path }]),
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
return Object.freeze(handle);
|
||||
}
|
||||
|
||||
const nodes = Object.freeze({
|
||||
list: () => request("nodes", ["list"]),
|
||||
get: async (idOrName) => nodeHandle(await request("nodes", ["get", idOrName])),
|
||||
});
|
||||
|
||||
const baseTools = Object.create(null);
|
||||
Object.defineProperties(baseTools, {
|
||||
search: { value: (query, options) => request("search", [query, options]), enumerable: true },
|
||||
@@ -291,6 +315,7 @@ const CONTROLLER_SOURCE = String.raw`
|
||||
Object.defineProperties(globalThis, {
|
||||
ALL_TOOLS: { value: Object.freeze(catalog.slice()), enumerable: true },
|
||||
API: { value: api, enumerable: true },
|
||||
nodes: { value: nodes, enumerable: true },
|
||||
namespaces: { value: Object.freeze(namespaceGlobals), enumerable: true },
|
||||
tools: { value: Object.freeze(baseTools), enumerable: true },
|
||||
text: { value: (value) => output.push({ type: "text", text: asText(value) }), enumerable: true },
|
||||
@@ -326,6 +351,7 @@ function createHostRequestHandler(params: {
|
||||
method !== "describe" &&
|
||||
method !== "call" &&
|
||||
method !== "callValue" &&
|
||||
method !== "nodes" &&
|
||||
method !== "yield" &&
|
||||
method !== "namespace" &&
|
||||
method !== "agentSpawn" &&
|
||||
|
||||
Reference in New Issue
Block a user