agenticros 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -3
- package/dist/util/skill-manifest.d.ts.map +1 -1
- package/dist/util/skill-manifest.js +22 -1
- package/dist/util/skill-manifest.js.map +1 -1
- package/package.json +1 -1
- package/runtime/BUNDLE.json +1 -1
- package/runtime/README.md +31 -6
- package/runtime/docs/cli.md +1 -1
- package/runtime/docs/robot-setup.md +1 -1
- package/runtime/packages/agenticros/openclaw.plugin.json +147 -1
- package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +2 -0
- package/runtime/packages/agenticros/src/routes.ts +4 -3
- package/runtime/packages/agenticros/src/tools/index.ts +6 -5
- package/runtime/packages/agenticros/src/tools/mission-pause.ts +49 -0
- package/runtime/packages/agenticros/src/tools/mission-resume.ts +41 -0
- package/runtime/packages/agenticros/src/tools/ros2-mission.ts +33 -84
- package/runtime/packages/agenticros-claude-code/README.md +2 -0
- package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/tools.js +130 -111
- package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/src/tools.ts +140 -99
- package/runtime/packages/agenticros-gemini/package.json +2 -0
- package/runtime/packages/agenticros-gemini/src/find-object/coco-classes.ts +38 -0
- package/runtime/packages/agenticros-gemini/src/find-object/find-object.ts +191 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/controller.ts +109 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/depth-loop.ts +452 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/detector.ts +303 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/loop.ts +359 -0
- package/runtime/packages/agenticros-gemini/src/tools.ts +351 -90
- package/runtime/packages/core/README.md +1 -1
- package/runtime/packages/core/package.json +1 -1
- package/runtime/packages/core/src/__tests__/config-persistence.test.ts +26 -0
- package/runtime/packages/core/src/__tests__/external-capability.test.ts +72 -0
- package/runtime/packages/core/src/__tests__/heartbeat-fleet.test.ts +152 -0
- package/runtime/packages/core/src/__tests__/mission-bindings.test.ts +101 -0
- package/runtime/packages/core/src/__tests__/mission-pause.test.ts +104 -0
- package/runtime/packages/core/src/capabilities.ts +38 -3
- package/runtime/packages/core/src/capability-schema.ts +58 -0
- package/runtime/packages/core/src/config.ts +20 -0
- package/runtime/packages/core/src/discovery.ts +21 -3
- package/runtime/packages/core/src/external-capability.ts +219 -0
- package/runtime/packages/core/src/fleet-config.ts +91 -0
- package/runtime/packages/core/src/heartbeat.ts +167 -0
- package/runtime/packages/core/src/index.ts +55 -0
- package/runtime/packages/core/src/mission-bindings.ts +189 -0
- package/runtime/packages/core/src/mission-registry.ts +31 -3
- package/runtime/packages/core/src/mission.ts +64 -7
- package/runtime/packages/core/src/robots.ts +6 -2
- package/runtime/packages/core/src/transport/rosbridge/client.ts +7 -0
- package/runtime/pnpm-lock.yaml +6 -0
- package/templates/skills/camera/package.json +1 -1
- package/templates/skills/depth/package.json +1 -1
- package/templates/skills/robot/package.json +1 -1
|
@@ -22,7 +22,6 @@ import { Type } from "@sinclair/typebox";
|
|
|
22
22
|
import type { AgentTool, OpenClawPluginApi } from "../plugin-api.js";
|
|
23
23
|
import type {
|
|
24
24
|
AgenticROSConfig,
|
|
25
|
-
CapabilityToolBindings,
|
|
26
25
|
Mission,
|
|
27
26
|
MissionToolDispatcher,
|
|
28
27
|
} from "@agenticros/core";
|
|
@@ -33,10 +32,15 @@ import {
|
|
|
33
32
|
createMemoryTranscriptSink,
|
|
34
33
|
missionTranscriptNamespace,
|
|
35
34
|
compileGoalToMission,
|
|
35
|
+
buildMissionBindings,
|
|
36
|
+
isExternalToolName,
|
|
37
|
+
capabilityIdFromExternalTool,
|
|
38
|
+
executeExternalCapability,
|
|
36
39
|
} from "@agenticros/core";
|
|
37
40
|
import { resolveRobotForTool } from "./_robot-helpers.js";
|
|
38
41
|
import { getMissionRegistry } from "../mission-registry.js";
|
|
39
42
|
import { getMemory } from "../memory.js";
|
|
43
|
+
import { getTransportForRobot } from "../service.js";
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Snapshot of the registered tools, keyed by tool name. Built in
|
|
@@ -45,85 +49,6 @@ import { getMemory } from "../memory.js";
|
|
|
45
49
|
*/
|
|
46
50
|
export type ToolRegistry = Map<string, AgentTool>;
|
|
47
51
|
|
|
48
|
-
const MISSION_BINDINGS: CapabilityToolBindings = {
|
|
49
|
-
drive_base: {
|
|
50
|
-
tool: "ros2_publish",
|
|
51
|
-
buildArgs: (inputs) => {
|
|
52
|
-
const lx = Number(inputs.linear_x ?? 0) || 0;
|
|
53
|
-
const az = Number(inputs.angular_z ?? 0) || 0;
|
|
54
|
-
return {
|
|
55
|
-
topic: "/cmd_vel",
|
|
56
|
-
type: "geometry_msgs/msg/Twist",
|
|
57
|
-
message: {
|
|
58
|
-
linear: { x: lx, y: 0, z: 0 },
|
|
59
|
-
angular: { x: 0, y: 0, z: az },
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
take_snapshot: {
|
|
65
|
-
tool: "ros2_camera_snapshot",
|
|
66
|
-
buildArgs: (inputs) => {
|
|
67
|
-
const out: Record<string, unknown> = {};
|
|
68
|
-
if (typeof inputs.topic === "string") out.topic = inputs.topic;
|
|
69
|
-
if (typeof inputs.message_type === "string") out.message_type = inputs.message_type;
|
|
70
|
-
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
71
|
-
return out;
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
measure_depth: {
|
|
75
|
-
tool: "ros2_depth_distance",
|
|
76
|
-
buildArgs: (inputs) => {
|
|
77
|
-
const out: Record<string, unknown> = {};
|
|
78
|
-
if (typeof inputs.topic === "string") out.topic = inputs.topic;
|
|
79
|
-
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
80
|
-
return out;
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
list_topics: {
|
|
84
|
-
tool: "ros2_list_topics",
|
|
85
|
-
buildArgs: () => ({}),
|
|
86
|
-
},
|
|
87
|
-
publish_topic: {
|
|
88
|
-
tool: "ros2_publish",
|
|
89
|
-
buildArgs: (inputs) => ({
|
|
90
|
-
topic: String(inputs.topic ?? ""),
|
|
91
|
-
type: String(inputs.type ?? inputs.msg_type ?? ""),
|
|
92
|
-
message: inputs.message ?? inputs.msg ?? {},
|
|
93
|
-
}),
|
|
94
|
-
},
|
|
95
|
-
subscribe_once: {
|
|
96
|
-
tool: "ros2_subscribe_once",
|
|
97
|
-
buildArgs: (inputs) => {
|
|
98
|
-
const out: Record<string, unknown> = { topic: String(inputs.topic ?? "") };
|
|
99
|
-
if (typeof inputs.type === "string") out.type = inputs.type;
|
|
100
|
-
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
101
|
-
return out;
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
follow_person: {
|
|
105
|
-
tool: "ros2_follow_me_start",
|
|
106
|
-
buildArgs: (inputs) => {
|
|
107
|
-
const out: Record<string, unknown> = {};
|
|
108
|
-
if (typeof inputs.target_distance === "number") out.target_distance = inputs.target_distance;
|
|
109
|
-
if (typeof inputs.mode === "string") out.mode = inputs.mode;
|
|
110
|
-
return out;
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
find_object: {
|
|
114
|
-
tool: "ros2_find_object",
|
|
115
|
-
buildArgs: (inputs) => {
|
|
116
|
-
const target = String(inputs.target ?? "");
|
|
117
|
-
const out: Record<string, unknown> = { target };
|
|
118
|
-
if (typeof inputs.angular_speed === "number") out.angular_speed = inputs.angular_speed;
|
|
119
|
-
if (typeof inputs.clockwise === "boolean") out.clockwise = inputs.clockwise;
|
|
120
|
-
if (typeof inputs.timeout_seconds === "number") out.timeout_seconds = inputs.timeout_seconds;
|
|
121
|
-
if (typeof inputs.min_confidence === "number") out.min_confidence = inputs.min_confidence;
|
|
122
|
-
return out;
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
52
|
export function registerMissionTool(
|
|
128
53
|
api: OpenClawPluginApi,
|
|
129
54
|
config: AgenticROSConfig,
|
|
@@ -143,9 +68,9 @@ export function registerMissionTool(
|
|
|
143
68
|
"provided) the compiled plan + candidate match list so you can see what the planner did. " +
|
|
144
69
|
"When memory is enabled, every step is also written to the shared memory under namespace " +
|
|
145
70
|
"mission:<mission_id> so a second agent can recall the timeline via memory_recall. " +
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
71
|
+
"Bindings are built from the capability registry (builtins + skill-declared). " +
|
|
72
|
+
"Pass mission.robot_id (or top-level robot_id with goal) to target every step at one robot. " +
|
|
73
|
+
"Use mission_pause / mission_resume / mission_cancel with the returned mission_id.",
|
|
149
74
|
parameters: Type.Object({
|
|
150
75
|
goal: Type.Optional(
|
|
151
76
|
Type.String({
|
|
@@ -186,6 +111,13 @@ export function registerMissionTool(
|
|
|
186
111
|
|
|
187
112
|
async execute(toolCallId, params, signal) {
|
|
188
113
|
const caps = listAllCapabilities(config);
|
|
114
|
+
const missionBindings = buildMissionBindings(caps, {
|
|
115
|
+
toolNameResolver: (cap) => {
|
|
116
|
+
const preferred = `ros2_${cap.id}`;
|
|
117
|
+
if (registry.has(preferred)) return preferred;
|
|
118
|
+
return undefined;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
189
121
|
const missionRaw = params["mission"];
|
|
190
122
|
const goalRaw = params["goal"];
|
|
191
123
|
const topLevelRobotId = typeof params["robot_id"] === "string" ? (params["robot_id"] as string) : undefined;
|
|
@@ -236,6 +168,23 @@ export function registerMissionTool(
|
|
|
236
168
|
}
|
|
237
169
|
|
|
238
170
|
const dispatcher: MissionToolDispatcher = async (toolName, toolArgs) => {
|
|
171
|
+
if (isExternalToolName(toolName)) {
|
|
172
|
+
const capId = capabilityIdFromExternalTool(toolName);
|
|
173
|
+
const cap = caps.find((c) => c.id === capId);
|
|
174
|
+
if (!cap) {
|
|
175
|
+
return { text: `Unknown external capability "${capId}".`, isError: true };
|
|
176
|
+
}
|
|
177
|
+
const resolved = resolveRobotForTool(config, toolArgs);
|
|
178
|
+
if ("error" in resolved) {
|
|
179
|
+
const errText = resolved.error.content.map((c) => ("text" in c ? c.text : "")).join("\n");
|
|
180
|
+
return { text: errText, isError: true };
|
|
181
|
+
}
|
|
182
|
+
const transport = await getTransportForRobot(config, resolved.robot);
|
|
183
|
+
const ext = await executeExternalCapability(cap, toolArgs, transport, {
|
|
184
|
+
namespace: resolved.robot.namespace,
|
|
185
|
+
});
|
|
186
|
+
return { text: ext.text, outputs: ext.outputs, isError: ext.isError };
|
|
187
|
+
}
|
|
239
188
|
const tool = registry.get(toolName);
|
|
240
189
|
if (!tool) {
|
|
241
190
|
return {
|
|
@@ -273,7 +222,7 @@ export function registerMissionTool(
|
|
|
273
222
|
|
|
274
223
|
let result;
|
|
275
224
|
try {
|
|
276
|
-
result = await runMission(mission, caps,
|
|
225
|
+
result = await runMission(mission, caps, missionBindings, dispatcher, {
|
|
277
226
|
mission_id: missionId,
|
|
278
227
|
cancellation: regEntry.cancellation,
|
|
279
228
|
transcript,
|
|
@@ -4,6 +4,8 @@ MCP (Model Context Protocol) server that exposes AgenticROS ROS2 tools to **Clau
|
|
|
4
4
|
|
|
5
5
|
This adapter does **not** provide the config or teleop web UI; use the [OpenClaw plugin](../../packages/agenticros) for that, or run the gateway for the browser-based teleop page.
|
|
6
6
|
|
|
7
|
+
**Local models (no cloud API):** Claude and Codex still use cloud LLMs. For **fully local** VLM inference with Ollama, use the [OpenClaw plugin](../../packages/agenticros) or [Hermes Agent](../../docs/hermes-setup.md) — see **[docs/local-vlm.md](../../docs/local-vlm.md)**.
|
|
8
|
+
|
|
7
9
|
## Prerequisites
|
|
8
10
|
|
|
9
11
|
- Node.js 20+
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,gBAAgB,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAKjB,MAAM,kBAAkB,CAAC;AAyC1B;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAK5B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,aAsBlC,CAAC;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;QACvF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,KAAK,EAAE,OAAO,EAkZ1B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AA4W7G,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAsmBxD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tool definitions and handler. Mirrors OpenClaw adapter tools.
|
|
3
3
|
*/
|
|
4
|
-
import { resolveCameraSubscribeTopic, toNamespacedTopic, toNamespacedTopicFull, listAllCapabilities, runMission, listRobots, getActiveRobotId, resolveRobotFromArgs, discoverRobots, findRobotsFor, generateMissionId, createMemoryTranscriptSink, missionTranscriptNamespace, compileGoalToMission, } from "@agenticros/core";
|
|
4
|
+
import { resolveCameraSubscribeTopic, toNamespacedTopic, toNamespacedTopicFull, listAllCapabilities, runMission, listRobots, getActiveRobotId, resolveRobotFromArgs, discoverRobots, findRobotsFor, generateMissionId, createMemoryTranscriptSink, missionTranscriptNamespace, compileGoalToMission, buildMissionBindings, isExternalToolName, capabilityIdFromExternalTool, executeExternalCapability, } from "@agenticros/core";
|
|
5
5
|
import { getMissionRegistry } from "./mission-registry.js";
|
|
6
6
|
import { ROS_MSG_COMPRESSED_IMAGE, ROS_MSG_IMAGE, cameraSnapshotFromPlainMessage, mimeTypeForSnapshotBase64, rosNumericField, } from "@agenticros/ros-camera";
|
|
7
7
|
import { resolveMemoryNamespace } from "@agenticros/core";
|
|
@@ -39,9 +39,12 @@ export const NO_TRANSPORT_TOOL_NAMES = new Set([
|
|
|
39
39
|
// tool falls back to a transport-driven discovery only when the caller
|
|
40
40
|
// passes online=true. See the handler for the precise branch.
|
|
41
41
|
"ros2_find_robots_for",
|
|
42
|
-
// mission_cancel
|
|
43
|
-
// never
|
|
42
|
+
// mission_cancel / mission_pause / mission_resume only mutate the
|
|
43
|
+
// in-process MissionRegistry — they never publish to ROS, so they
|
|
44
|
+
// must work even when zenohd is down.
|
|
44
45
|
"mission_cancel",
|
|
46
|
+
"mission_pause",
|
|
47
|
+
"mission_resume",
|
|
45
48
|
// run_mission's *outer* call doesn't need transport — its handler
|
|
46
49
|
// first compiles the goal (Phase 1.g, no transport) or validates
|
|
47
50
|
// the mission shape, then per-step calls go through the transport
|
|
@@ -392,6 +395,38 @@ export const TOOLS = [
|
|
|
392
395
|
required: ["mission_id"],
|
|
393
396
|
},
|
|
394
397
|
},
|
|
398
|
+
{
|
|
399
|
+
name: "mission_pause",
|
|
400
|
+
description: "Pause a mission that's currently running in this MCP server. Pass the mission_id returned by run_mission. The runner waits at the next step boundary until mission_resume (or mission_cancel). Idempotent if already paused.",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties: {
|
|
404
|
+
mission_id: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "The mission_id echoed by run_mission. Required.",
|
|
407
|
+
},
|
|
408
|
+
reason: {
|
|
409
|
+
type: "string",
|
|
410
|
+
description: "Optional free-text reason — surfaced in the paused transcript entry.",
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
required: ["mission_id"],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "mission_resume",
|
|
418
|
+
description: "Resume a mission previously paused with mission_pause. Pass the mission_id returned by run_mission. Idempotent if the mission is not paused.",
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: "object",
|
|
421
|
+
properties: {
|
|
422
|
+
mission_id: {
|
|
423
|
+
type: "string",
|
|
424
|
+
description: "The mission_id echoed by run_mission. Required.",
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
required: ["mission_id"],
|
|
428
|
+
},
|
|
429
|
+
},
|
|
395
430
|
];
|
|
396
431
|
function followMeMode(args) {
|
|
397
432
|
const raw = String(args["mode"] ?? "depth").toLowerCase().trim();
|
|
@@ -485,107 +520,6 @@ function formatFindRobotsForResponse(result) {
|
|
|
485
520
|
})),
|
|
486
521
|
});
|
|
487
522
|
}
|
|
488
|
-
/**
|
|
489
|
-
* Capability → MCP tool dispatch table used by `run_mission`.
|
|
490
|
-
*
|
|
491
|
-
* Every capability returned by `ros2_list_capabilities` that the mission
|
|
492
|
-
* runner can actually execute must appear here. If you add a new
|
|
493
|
-
* intrinsic capability to BUILTIN_CAPABILITIES, mirror it here. For new
|
|
494
|
-
* skill-declared capabilities, this is where you map them to the
|
|
495
|
-
* corresponding MCP tool. Steps that name a capability without an entry
|
|
496
|
-
* fail with an actionable "no mission-runner tool binding" error.
|
|
497
|
-
*/
|
|
498
|
-
const MISSION_BINDINGS = {
|
|
499
|
-
drive_base: {
|
|
500
|
-
tool: "ros2_publish",
|
|
501
|
-
buildArgs: (inputs) => {
|
|
502
|
-
const lx = Number(inputs.linear_x ?? 0) || 0;
|
|
503
|
-
const az = Number(inputs.angular_z ?? 0) || 0;
|
|
504
|
-
return {
|
|
505
|
-
topic: "/cmd_vel",
|
|
506
|
-
type: "geometry_msgs/msg/Twist",
|
|
507
|
-
message: {
|
|
508
|
-
linear: { x: lx, y: 0, z: 0 },
|
|
509
|
-
angular: { x: 0, y: 0, z: az },
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
take_snapshot: {
|
|
515
|
-
tool: "ros2_camera_snapshot",
|
|
516
|
-
buildArgs: (inputs) => {
|
|
517
|
-
const out = {};
|
|
518
|
-
if (typeof inputs.topic === "string")
|
|
519
|
-
out.topic = inputs.topic;
|
|
520
|
-
if (typeof inputs.message_type === "string")
|
|
521
|
-
out.message_type = inputs.message_type;
|
|
522
|
-
if (typeof inputs.timeout === "number")
|
|
523
|
-
out.timeout = inputs.timeout;
|
|
524
|
-
return out;
|
|
525
|
-
},
|
|
526
|
-
},
|
|
527
|
-
measure_depth: {
|
|
528
|
-
tool: "ros2_depth_distance",
|
|
529
|
-
buildArgs: (inputs) => {
|
|
530
|
-
const out = {};
|
|
531
|
-
if (typeof inputs.topic === "string")
|
|
532
|
-
out.topic = inputs.topic;
|
|
533
|
-
if (typeof inputs.timeout === "number")
|
|
534
|
-
out.timeout = inputs.timeout;
|
|
535
|
-
return out;
|
|
536
|
-
},
|
|
537
|
-
},
|
|
538
|
-
list_topics: {
|
|
539
|
-
tool: "ros2_list_topics",
|
|
540
|
-
buildArgs: () => ({}),
|
|
541
|
-
},
|
|
542
|
-
publish_topic: {
|
|
543
|
-
tool: "ros2_publish",
|
|
544
|
-
buildArgs: (inputs) => ({
|
|
545
|
-
topic: String(inputs.topic ?? ""),
|
|
546
|
-
type: String(inputs.type ?? inputs.msg_type ?? ""),
|
|
547
|
-
message: inputs.message ?? inputs.msg ?? {},
|
|
548
|
-
}),
|
|
549
|
-
},
|
|
550
|
-
subscribe_once: {
|
|
551
|
-
tool: "ros2_subscribe_once",
|
|
552
|
-
buildArgs: (inputs) => {
|
|
553
|
-
const out = { topic: String(inputs.topic ?? "") };
|
|
554
|
-
if (typeof inputs.type === "string")
|
|
555
|
-
out.type = inputs.type;
|
|
556
|
-
if (typeof inputs.timeout === "number")
|
|
557
|
-
out.timeout = inputs.timeout;
|
|
558
|
-
return out;
|
|
559
|
-
},
|
|
560
|
-
},
|
|
561
|
-
follow_person: {
|
|
562
|
-
tool: "ros2_follow_me_start",
|
|
563
|
-
buildArgs: (inputs) => {
|
|
564
|
-
const out = {};
|
|
565
|
-
if (typeof inputs.target_distance === "number")
|
|
566
|
-
out.target_distance = inputs.target_distance;
|
|
567
|
-
if (typeof inputs.mode === "string")
|
|
568
|
-
out.mode = inputs.mode;
|
|
569
|
-
return out;
|
|
570
|
-
},
|
|
571
|
-
},
|
|
572
|
-
find_object: {
|
|
573
|
-
tool: "ros2_find_object",
|
|
574
|
-
buildArgs: (inputs) => {
|
|
575
|
-
const target = String(inputs.target ?? "");
|
|
576
|
-
const out = { target };
|
|
577
|
-
if (typeof inputs.angular_speed === "number")
|
|
578
|
-
out.angular_speed = inputs.angular_speed;
|
|
579
|
-
if (typeof inputs.clockwise === "boolean")
|
|
580
|
-
out.clockwise = inputs.clockwise;
|
|
581
|
-
if (typeof inputs.timeout_seconds === "number")
|
|
582
|
-
out.timeout_seconds = inputs.timeout_seconds;
|
|
583
|
-
if (typeof inputs.min_confidence === "number")
|
|
584
|
-
out.min_confidence = inputs.min_confidence;
|
|
585
|
-
return out;
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
};
|
|
589
523
|
/**
|
|
590
524
|
* Phase 1.f — `mission_cancel` handler. Lives at module scope so the
|
|
591
525
|
* outer `handleToolCall` can invoke it WITHOUT first resolving a
|
|
@@ -622,6 +556,64 @@ function handleMissionCancel(args) {
|
|
|
622
556
|
],
|
|
623
557
|
};
|
|
624
558
|
}
|
|
559
|
+
function handleMissionPause(args) {
|
|
560
|
+
const missionId = typeof args["mission_id"] === "string" ? args["mission_id"].trim() : "";
|
|
561
|
+
if (!missionId) {
|
|
562
|
+
return {
|
|
563
|
+
content: [
|
|
564
|
+
{
|
|
565
|
+
type: "text",
|
|
566
|
+
text: "mission_pause requires 'mission_id' (a non-empty string returned by run_mission).",
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
isError: true,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const reason = typeof args["reason"] === "string" ? args["reason"] : undefined;
|
|
573
|
+
const outcome = getMissionRegistry().pause(missionId, reason);
|
|
574
|
+
return {
|
|
575
|
+
content: [
|
|
576
|
+
{
|
|
577
|
+
type: "text",
|
|
578
|
+
text: JSON.stringify({
|
|
579
|
+
success: true,
|
|
580
|
+
mission_id: missionId,
|
|
581
|
+
found: outcome.found,
|
|
582
|
+
already_paused: outcome.alreadyPaused,
|
|
583
|
+
reason: reason ?? null,
|
|
584
|
+
}),
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function handleMissionResume(args) {
|
|
590
|
+
const missionId = typeof args["mission_id"] === "string" ? args["mission_id"].trim() : "";
|
|
591
|
+
if (!missionId) {
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
595
|
+
type: "text",
|
|
596
|
+
text: "mission_resume requires 'mission_id' (a non-empty string returned by run_mission).",
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
isError: true,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const outcome = getMissionRegistry().resume(missionId);
|
|
603
|
+
return {
|
|
604
|
+
content: [
|
|
605
|
+
{
|
|
606
|
+
type: "text",
|
|
607
|
+
text: JSON.stringify({
|
|
608
|
+
success: true,
|
|
609
|
+
mission_id: missionId,
|
|
610
|
+
found: outcome.found,
|
|
611
|
+
was_paused: outcome.wasPaused,
|
|
612
|
+
}),
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
625
617
|
/**
|
|
626
618
|
* Phase 1.c + 1.f + 1.g — `run_mission` handler. Lives at module scope
|
|
627
619
|
* so the outer `handleToolCall` can short-circuit BEFORE
|
|
@@ -696,6 +688,31 @@ async function handleRunMission(args, config) {
|
|
|
696
688
|
}
|
|
697
689
|
}
|
|
698
690
|
const dispatcher = async (toolName, toolArgs) => {
|
|
691
|
+
if (isExternalToolName(toolName)) {
|
|
692
|
+
const capId = capabilityIdFromExternalTool(toolName);
|
|
693
|
+
const cap = caps.find((c) => c.id === capId);
|
|
694
|
+
if (!cap) {
|
|
695
|
+
return {
|
|
696
|
+
text: `Unknown external capability "${capId}".`,
|
|
697
|
+
isError: true,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
let robot;
|
|
701
|
+
try {
|
|
702
|
+
robot = resolveRobotFromArgs(config, toolArgs);
|
|
703
|
+
}
|
|
704
|
+
catch (err) {
|
|
705
|
+
return {
|
|
706
|
+
text: err instanceof Error ? err.message : String(err),
|
|
707
|
+
isError: true,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const transport = await getTransportForRobot(config, robot);
|
|
711
|
+
const ext = await executeExternalCapability(cap, toolArgs, transport, {
|
|
712
|
+
namespace: robot.namespace,
|
|
713
|
+
});
|
|
714
|
+
return { text: ext.text, outputs: ext.outputs, isError: ext.isError };
|
|
715
|
+
}
|
|
699
716
|
const res = await handleToolCall(toolName, toolArgs, config);
|
|
700
717
|
const text = res.content
|
|
701
718
|
.map((c) => (c.type === "text" ? c.text : `[image: ${c.mimeType}]`))
|
|
@@ -711,7 +728,7 @@ async function handleRunMission(args, config) {
|
|
|
711
728
|
const transcript = memory ? createMemoryTranscriptSink(memory, missionId) : undefined;
|
|
712
729
|
let result;
|
|
713
730
|
try {
|
|
714
|
-
result = await runMission(mission, caps,
|
|
731
|
+
result = await runMission(mission, caps, buildMissionBindings(caps), dispatcher, {
|
|
715
732
|
mission_id: missionId,
|
|
716
733
|
cancellation: regEntry.cancellation,
|
|
717
734
|
transcript,
|
|
@@ -828,15 +845,17 @@ export async function handleToolCall(name, args, config) {
|
|
|
828
845
|
};
|
|
829
846
|
}
|
|
830
847
|
}
|
|
831
|
-
// mission_cancel + run_mission must NOT trigger a
|
|
832
|
-
// at this outer layer
|
|
833
|
-
// MissionRegistry, and run_mission's handler does its OWN per-step
|
|
834
|
-
// dispatch (each step's inner `handleToolCall` connects as needed).
|
|
835
|
-
// Routing the outer call through `getTransportForRobot()` would
|
|
836
|
-
// hang or fail when an agent only wanted to compile / cancel.
|
|
848
|
+
// mission_cancel / pause / resume + run_mission must NOT trigger a
|
|
849
|
+
// transport connect at this outer layer.
|
|
837
850
|
if (name === "mission_cancel") {
|
|
838
851
|
return handleMissionCancel(args);
|
|
839
852
|
}
|
|
853
|
+
if (name === "mission_pause") {
|
|
854
|
+
return handleMissionPause(args);
|
|
855
|
+
}
|
|
856
|
+
if (name === "mission_resume") {
|
|
857
|
+
return handleMissionResume(args);
|
|
858
|
+
}
|
|
840
859
|
if (name === "run_mission") {
|
|
841
860
|
return handleRunMission(args, config);
|
|
842
861
|
}
|