Agents: add nodes invoke action

This commit is contained in:
Mariano Belinky
2026-01-31 18:13:22 +01:00
committed by Mariano Belinky
parent a4382607d7
commit d9cadf9737
2 changed files with 79 additions and 1 deletions

View File

@@ -133,3 +133,52 @@ describe("nodes run", () => {
});
});
});
describe("nodes invoke", () => {
beforeEach(() => {
callGateway.mockReset();
});
it("invokes arbitrary commands with params JSON", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "ios-1" }] };
}
if (method === "node.invoke") {
expect(params).toMatchObject({
nodeId: "ios-1",
command: "device.info",
params: { includeBattery: true },
timeoutMs: 12_000,
});
return {
ok: true,
nodeId: "ios-1",
command: "device.info",
payload: { deviceName: "iPhone" },
};
}
throw new Error(`unexpected method: ${String(method)}`);
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
const result = await tool.execute("call1", {
action: "invoke",
node: "ios-1",
invokeCommand: "device.info",
invokeParamsJson: JSON.stringify({ includeBattery: true }),
invokeTimeoutMs: 12_000,
});
expect(result.details).toMatchObject({
ok: true,
nodeId: "ios-1",
command: "device.info",
payload: { deviceName: "iPhone" },
});
});
});

View File

@@ -37,6 +37,7 @@ const NODES_TOOL_ACTIONS = [
"screen_record",
"location_get",
"run",
"invoke",
] as const;
const NOTIFY_PRIORITIES = ["passive", "active", "timeSensitive"] as const;
@@ -84,6 +85,9 @@ const NodesToolSchema = Type.Object({
commandTimeoutMs: Type.Optional(Type.Number()),
invokeTimeoutMs: Type.Optional(Type.Number()),
needsScreenRecording: Type.Optional(Type.Boolean()),
// invoke
invokeCommand: Type.Optional(Type.String()),
invokeParamsJson: Type.Optional(Type.String()),
});
export function createNodesTool(options?: {
@@ -99,7 +103,7 @@ export function createNodesTool(options?: {
label: "Nodes",
name: "nodes",
description:
"Discover and control paired nodes (status/describe/pairing/notify/camera/screen/location/run).",
"Discover and control paired nodes (status/describe/pairing/notify/camera/screen/location/run/invoke).",
parameters: NodesToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
@@ -438,6 +442,31 @@ export function createNodesTool(options?: {
});
return jsonResult(raw?.payload ?? {});
}
case "invoke": {
const node = readStringParam(params, "node", { required: true });
const nodeId = await resolveNodeId(gatewayOpts, node);
const invokeCommand = readStringParam(params, "invokeCommand", { required: true });
const invokeParamsJson =
typeof params.invokeParamsJson === "string" ? params.invokeParamsJson.trim() : "";
let invokeParams: unknown = {};
if (invokeParamsJson) {
try {
invokeParams = JSON.parse(invokeParamsJson);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`invokeParamsJson must be valid JSON: ${message}`);
}
}
const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs);
const raw = await callGatewayTool("node.invoke", gatewayOpts, {
nodeId,
command: invokeCommand,
params: invokeParams,
timeoutMs: invokeTimeoutMs,
idempotencyKey: crypto.randomUUID(),
});
return jsonResult(raw ?? {});
}
default:
throw new Error(`Unknown action: ${action}`);
}