agenticros 0.1.17 → 0.1.19
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/dist/__tests__/robot-config.test.d.ts +2 -0
- package/dist/__tests__/robot-config.test.d.ts.map +1 -0
- package/dist/__tests__/robot-config.test.js +374 -0
- package/dist/__tests__/robot-config.test.js.map +1 -0
- package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
- package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
- package/dist/__tests__/transport-shorthand.test.js +88 -0
- package/dist/__tests__/transport-shorthand.test.js.map +1 -0
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +55 -3
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/robots.d.ts +57 -0
- package/dist/commands/robots.d.ts.map +1 -0
- package/dist/commands/robots.js +678 -0
- package/dist/commands/robots.js.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/util/mcp-discovery.d.ts +57 -0
- package/dist/util/mcp-discovery.d.ts.map +1 -0
- package/dist/util/mcp-discovery.js +153 -0
- package/dist/util/mcp-discovery.js.map +1 -0
- package/dist/util/robot-config.d.ts +167 -0
- package/dist/util/robot-config.d.ts.map +1 -0
- package/dist/util/robot-config.js +375 -0
- package/dist/util/robot-config.js.map +1 -0
- package/dist/util/transport-shorthand.d.ts +66 -0
- package/dist/util/transport-shorthand.d.ts.map +1 -0
- package/dist/util/transport-shorthand.js +102 -0
- package/dist/util/transport-shorthand.js.map +1 -0
- package/package.json +2 -1
- package/runtime/BUNDLE.json +1 -1
- package/runtime/README.md +4 -0
- package/runtime/package.json +6 -1
- package/runtime/packages/agenticros/openclaw.plugin.json +6 -0
- package/runtime/packages/agenticros/package.json +2 -1
- package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
- package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
- package/runtime/packages/agenticros/src/service.ts +120 -2
- package/runtime/packages/agenticros/src/skill-loader.ts +9 -1
- package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
- package/runtime/packages/agenticros/src/tools/index.ts +49 -9
- package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
- package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
- package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
- package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
- package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
- package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
- package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
- package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
- package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
- package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
- package/runtime/packages/agenticros-claude-code/package.json +2 -1
- package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +964 -0
- package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
- package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
- package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
- package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
- package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
- package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
- package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
- package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
- package/runtime/packages/agenticros-gemini/package.json +1 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
- package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
- package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
- package/runtime/packages/core/package.json +1 -1
- package/runtime/packages/core/src/__tests__/capabilities.test.ts +261 -0
- package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
- package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
- package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
- package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
- package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
- package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
- package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
- package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
- package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
- package/runtime/packages/core/src/capabilities.ts +340 -0
- package/runtime/packages/core/src/config.ts +220 -38
- package/runtime/packages/core/src/discovery.ts +193 -0
- package/runtime/packages/core/src/find-robots-for.ts +164 -0
- package/runtime/packages/core/src/index.ts +67 -1
- package/runtime/packages/core/src/mission-registry.ts +142 -0
- package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
- package/runtime/packages/core/src/mission.ts +574 -0
- package/runtime/packages/core/src/planner/index.ts +564 -0
- package/runtime/packages/core/src/robots.ts +282 -0
- package/runtime/packages/core/src/topic-utils.ts +47 -9
- package/runtime/packages/core/src/transport-pool.ts +171 -0
- package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
- package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
- package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
- package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
- package/runtime/scripts/refresh-skill-deps.mjs +381 -0
- package/runtime/scripts/setup_gateway_plugin.sh +32 -5
- package/runtime/scripts/start_demo.sh +26 -3
- package/runtime/scripts/sync-skill-tools.mjs +67 -10
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
* Gemini function declarations and tool execution. Same ROS2 tool set as Claude Code.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AgenticROSConfig,
|
|
7
|
+
Capability,
|
|
8
|
+
CapabilityToolBindings,
|
|
9
|
+
Mission,
|
|
10
|
+
MissionToolDispatcher,
|
|
11
|
+
} from "@agenticros/core";
|
|
6
12
|
import {
|
|
7
13
|
resolveCameraSubscribeTopic,
|
|
8
14
|
resolveMemoryNamespace,
|
|
9
15
|
toNamespacedTopic,
|
|
16
|
+
listAllCapabilities,
|
|
17
|
+
runMission,
|
|
18
|
+
listRobots,
|
|
19
|
+
getActiveRobotId,
|
|
20
|
+
resolveRobotFromArgs,
|
|
21
|
+
discoverRobots,
|
|
22
|
+
findRobotsFor,
|
|
23
|
+
type ResolvedRobot,
|
|
24
|
+
generateMissionId,
|
|
25
|
+
createMemoryTranscriptSink,
|
|
26
|
+
missionTranscriptNamespace,
|
|
27
|
+
MissionRegistry,
|
|
28
|
+
compileGoalToMission,
|
|
10
29
|
} from "@agenticros/core";
|
|
11
30
|
import {
|
|
12
31
|
ROS_MSG_COMPRESSED_IMAGE,
|
|
@@ -17,7 +36,7 @@ import {
|
|
|
17
36
|
} from "@agenticros/ros-camera";
|
|
18
37
|
import type { FunctionDeclaration, FunctionResponsePart } from "@google/genai";
|
|
19
38
|
import { createFunctionResponsePartFromBase64 } from "@google/genai";
|
|
20
|
-
import {
|
|
39
|
+
import { getTransportForRobot } from "./transport.js";
|
|
21
40
|
import { checkPublishSafety } from "./safety.js";
|
|
22
41
|
import { getDepthDistance } from "./depth.js";
|
|
23
42
|
import { ensureMemory } from "./memory.js";
|
|
@@ -29,6 +48,100 @@ const MEMORY_TOOL_NAMES = new Set([
|
|
|
29
48
|
"memory_status",
|
|
30
49
|
]);
|
|
31
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Per-process mission registry — Phase 1.f. Each `run_mission`
|
|
53
|
+
* invocation registers a fresh mission_id; a sibling `mission_cancel`
|
|
54
|
+
* tool call looks it up and flips the cancellation token. Module scope
|
|
55
|
+
* so independent tool dispatches share state. Mirrors the singleton
|
|
56
|
+
* pattern used by the Claude Code MCP server and the OpenClaw plugin.
|
|
57
|
+
*/
|
|
58
|
+
const MISSION_REGISTRY = new MissionRegistry();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Capability → MCP tool dispatch table for `run_mission` (Phase 1.c).
|
|
62
|
+
* Mirrors the table in packages/agenticros-claude-code/src/tools.ts and
|
|
63
|
+
* packages/agenticros/src/tools/ros2-mission.ts — all three adapters
|
|
64
|
+
* agree on what each capability does.
|
|
65
|
+
*/
|
|
66
|
+
const MISSION_BINDINGS: CapabilityToolBindings = {
|
|
67
|
+
drive_base: {
|
|
68
|
+
tool: "ros2_publish",
|
|
69
|
+
buildArgs: (inputs) => {
|
|
70
|
+
const lx = Number(inputs.linear_x ?? 0) || 0;
|
|
71
|
+
const az = Number(inputs.angular_z ?? 0) || 0;
|
|
72
|
+
return {
|
|
73
|
+
topic: "/cmd_vel",
|
|
74
|
+
type: "geometry_msgs/msg/Twist",
|
|
75
|
+
message: {
|
|
76
|
+
linear: { x: lx, y: 0, z: 0 },
|
|
77
|
+
angular: { x: 0, y: 0, z: az },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
take_snapshot: {
|
|
83
|
+
tool: "ros2_camera_snapshot",
|
|
84
|
+
buildArgs: (inputs) => {
|
|
85
|
+
const out: Record<string, unknown> = {};
|
|
86
|
+
if (typeof inputs.topic === "string") out.topic = inputs.topic;
|
|
87
|
+
if (typeof inputs.message_type === "string") out.message_type = inputs.message_type;
|
|
88
|
+
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
89
|
+
return out;
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
measure_depth: {
|
|
93
|
+
tool: "ros2_depth_distance",
|
|
94
|
+
buildArgs: (inputs) => {
|
|
95
|
+
const out: Record<string, unknown> = {};
|
|
96
|
+
if (typeof inputs.topic === "string") out.topic = inputs.topic;
|
|
97
|
+
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
98
|
+
return out;
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
list_topics: {
|
|
102
|
+
tool: "ros2_list_topics",
|
|
103
|
+
buildArgs: () => ({}),
|
|
104
|
+
},
|
|
105
|
+
publish_topic: {
|
|
106
|
+
tool: "ros2_publish",
|
|
107
|
+
buildArgs: (inputs) => ({
|
|
108
|
+
topic: String(inputs.topic ?? ""),
|
|
109
|
+
type: String(inputs.type ?? inputs.msg_type ?? ""),
|
|
110
|
+
message: inputs.message ?? inputs.msg ?? {},
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
subscribe_once: {
|
|
114
|
+
tool: "ros2_subscribe_once",
|
|
115
|
+
buildArgs: (inputs) => {
|
|
116
|
+
const out: Record<string, unknown> = { topic: String(inputs.topic ?? "") };
|
|
117
|
+
if (typeof inputs.type === "string") out.type = inputs.type;
|
|
118
|
+
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
119
|
+
return out;
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
follow_person: {
|
|
123
|
+
tool: "ros2_follow_me_start",
|
|
124
|
+
buildArgs: (inputs) => {
|
|
125
|
+
const out: Record<string, unknown> = {};
|
|
126
|
+
if (typeof inputs.target_distance === "number") out.target_distance = inputs.target_distance;
|
|
127
|
+
if (typeof inputs.mode === "string") out.mode = inputs.mode;
|
|
128
|
+
return out;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
find_object: {
|
|
132
|
+
tool: "ros2_find_object",
|
|
133
|
+
buildArgs: (inputs) => {
|
|
134
|
+
const target = String(inputs.target ?? "");
|
|
135
|
+
const out: Record<string, unknown> = { target };
|
|
136
|
+
if (typeof inputs.angular_speed === "number") out.angular_speed = inputs.angular_speed;
|
|
137
|
+
if (typeof inputs.clockwise === "boolean") out.clockwise = inputs.clockwise;
|
|
138
|
+
if (typeof inputs.timeout_seconds === "number") out.timeout_seconds = inputs.timeout_seconds;
|
|
139
|
+
if (typeof inputs.min_confidence === "number") out.min_confidence = inputs.min_confidence;
|
|
140
|
+
return out;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
32
145
|
const DEFAULT_DEPTH_TOPIC = "/camera/camera/depth/image_rect_raw";
|
|
33
146
|
const ENABLE_MULTIMODAL_FUNCTION_RESPONSE =
|
|
34
147
|
(process.env.GEMINI_ENABLE_MULTIMODAL_TOOL_RESPONSE ?? "").toLowerCase() === "true";
|
|
@@ -43,92 +156,173 @@ function schemaFromProps(properties: Record<string, { type: string; description?
|
|
|
43
156
|
return schema;
|
|
44
157
|
}
|
|
45
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Phase 1.d — schema fragment for an optional `robot_id` parameter,
|
|
161
|
+
* spread into every transport-bound tool's properties object. The
|
|
162
|
+
* description matches the claude-code and OpenClaw adapters so the
|
|
163
|
+
* agent gets a consistent contract across hosts.
|
|
164
|
+
*/
|
|
165
|
+
const ROBOT_ID_PROP = {
|
|
166
|
+
robot_id: {
|
|
167
|
+
type: "string",
|
|
168
|
+
description:
|
|
169
|
+
"Optional robot id (from ros2_list_robots) to scope this call. When omitted, the active robot is used.",
|
|
170
|
+
},
|
|
171
|
+
} as const;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve the target robot from a function-call's args. On success
|
|
175
|
+
* returns `{ robot }`; on unknown id returns `{ error }` already
|
|
176
|
+
* formatted as a Gemini tool result so the caller can pass it straight
|
|
177
|
+
* back. The error message already lists known ids and recommends
|
|
178
|
+
* ros2_list_robots, so the agent self-corrects.
|
|
179
|
+
*/
|
|
180
|
+
function resolveRobotForTool(
|
|
181
|
+
config: AgenticROSConfig,
|
|
182
|
+
args: Record<string, unknown>,
|
|
183
|
+
): { robot: ResolvedRobot } | { error: { output: string } } {
|
|
184
|
+
try {
|
|
185
|
+
return { robot: resolveRobotFromArgs(config, args) };
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
return { error: { output: msg } };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
46
192
|
export const GEMINI_FUNCTION_DECLARATIONS: FunctionDeclaration[] = [
|
|
47
193
|
{
|
|
48
194
|
name: "ros2_list_topics",
|
|
49
195
|
description: "List all available ROS2 topics and their message types. Use this to discover what data the robot publishes and what commands it accepts.",
|
|
50
196
|
parametersJsonSchema: schemaFromProps({}),
|
|
51
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: "ros2_list_capabilities",
|
|
200
|
+
description:
|
|
201
|
+
"List the high-level capabilities (named verbs) this robot can perform — built-in verbs like drive_base / take_snapshot / measure_depth plus every capability declared by installed AgenticROS skills (e.g. follow_person, find_object). PREFER this over ros2_list_topics for high-level planning: capabilities are agent-meaningful verbs with typed inputs/outputs, not raw topic names. Returns one structured response listing every capability the robot supports right now.",
|
|
202
|
+
parametersJsonSchema: schemaFromProps({ ...ROBOT_ID_PROP }),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "ros2_list_robots",
|
|
206
|
+
description:
|
|
207
|
+
"List the robots this gateway knows about (id, name, ROS2 namespace, default camera topic) and which one is the active default. Use this FIRST when the user mentions multiple robots, asks 'which robots can you see?', or names a specific robot you haven't heard of. The returned `id` is what other tools accept as a `robot_id` parameter.",
|
|
208
|
+
parametersJsonSchema: schemaFromProps({}),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "ros2_discover_robots",
|
|
212
|
+
description:
|
|
213
|
+
"Scan the ROS2 topic graph and report which robots are actually on the wire right now, classified against the gateway's configured robot list. Returns: (1) every namespace inferred from `<ns>/cmd_vel` topics, with topicCount per namespace, (2) configured_online — configured robots currently publishing, (3) configured_offline — configured robots that are silent, (4) unknown_detected — robots on the wire that aren't in config yet. Use this when the user asks 'which robots are online right now', 'is my robot connected', or wants to find a robot that isn't in ros2_list_robots. Requires the ROS transport to be connected.",
|
|
214
|
+
parametersJsonSchema: schemaFromProps({}),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "ros2_find_robots_for",
|
|
218
|
+
description:
|
|
219
|
+
"Find the robots in the configured fleet that match a capability + kind + online filter, ranked best-first. PREFER this over ros2_list_robots whenever the user names a verb ('which robot can find a chair', 'do I have an arm robot that can grasp', 'is there an AMR online that can follow a person'). Capability matches the verbs from ros2_list_capabilities — by default robots inherit the gateway-wide registry; declaring per-robot capabilities in config narrows it. Kind matches robot.kind exactly ('amr' | 'arm' | 'drone' | 'rover'). When online=true, only currently-reachable robots are returned (uses the same `<ns>/cmd_vel` heuristic as ros2_discover_robots and requires the transport). The result lists matched robots with id/name/namespace/kind/sensors/online flag, ranked so explicit capability declarations + online robots come first.",
|
|
220
|
+
parametersJsonSchema: schemaFromProps({
|
|
221
|
+
capability: {
|
|
222
|
+
type: "string",
|
|
223
|
+
description:
|
|
224
|
+
"Capability id to match (e.g. 'follow_person', 'find_object', 'drive_base'). Case-sensitive — use ros2_list_capabilities to get the exact list.",
|
|
225
|
+
},
|
|
226
|
+
kind: {
|
|
227
|
+
type: "string",
|
|
228
|
+
description:
|
|
229
|
+
"Robot kind filter (case-insensitive exact match). Common values: 'amr', 'arm', 'drone', 'rover'.",
|
|
230
|
+
},
|
|
231
|
+
online: {
|
|
232
|
+
type: "boolean",
|
|
233
|
+
description:
|
|
234
|
+
"When true, only return robots currently reachable on the ROS2 graph (requires the transport). When false, only return robots NOT reachable. When omitted, online status is annotated on every match but doesn't filter.",
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
52
238
|
{
|
|
53
239
|
name: "ros2_publish",
|
|
54
|
-
description: "Publish a message to a ROS2 topic. Use this to send commands to the robot (e.g., velocity commands to /cmd_vel, navigation goals).",
|
|
240
|
+
description: "Publish a message to a ROS2 topic. Use this to send commands to the robot (e.g., velocity commands to /cmd_vel, navigation goals). Pass robot_id (from ros2_list_robots) to target a specific robot.",
|
|
55
241
|
parametersJsonSchema: schemaFromProps(
|
|
56
242
|
{
|
|
57
243
|
topic: { type: "string", description: "The ROS2 topic name (e.g., '/cmd_vel')" },
|
|
58
244
|
type: { type: "string", description: "The ROS2 message type (e.g., 'geometry_msgs/msg/Twist')" },
|
|
59
245
|
message: { type: "object", description: "The message payload matching the ROS2 message type schema" },
|
|
246
|
+
...ROBOT_ID_PROP,
|
|
60
247
|
},
|
|
61
248
|
["topic", "type", "message"],
|
|
62
249
|
),
|
|
63
250
|
},
|
|
64
251
|
{
|
|
65
252
|
name: "ros2_subscribe_once",
|
|
66
|
-
description: "Subscribe to a ROS2 topic and return the next message. Use this to read sensor data, check robot state, or get the current value of a topic.",
|
|
253
|
+
description: "Subscribe to a ROS2 topic and return the next message. Use this to read sensor data, check robot state, or get the current value of a topic. Pass robot_id to target a specific robot.",
|
|
67
254
|
parametersJsonSchema: schemaFromProps({
|
|
68
255
|
topic: { type: "string", description: "The ROS2 topic name (e.g., '/battery_state')" },
|
|
69
256
|
type: { type: "string", description: "The ROS2 message type (optional)" },
|
|
70
257
|
timeout: { type: "number", description: "Timeout in milliseconds (default: 5000)" },
|
|
258
|
+
...ROBOT_ID_PROP,
|
|
71
259
|
}, ["topic"]),
|
|
72
260
|
},
|
|
73
261
|
{
|
|
74
262
|
name: "ros2_service_call",
|
|
75
|
-
description: "Call a ROS2 service and return the response. Use for request/response operations like setting parameters or querying node state.",
|
|
263
|
+
description: "Call a ROS2 service and return the response. Use for request/response operations like setting parameters or querying node state. Pass robot_id to target a specific robot.",
|
|
76
264
|
parametersJsonSchema: schemaFromProps({
|
|
77
265
|
service: { type: "string", description: "The ROS2 service name (e.g., '/spawn_entity')" },
|
|
78
266
|
type: { type: "string", description: "The ROS2 service type (optional)" },
|
|
79
267
|
args: { type: "object", description: "The service request arguments" },
|
|
268
|
+
...ROBOT_ID_PROP,
|
|
80
269
|
}, ["service"]),
|
|
81
270
|
},
|
|
82
271
|
{
|
|
83
272
|
name: "ros2_action_goal",
|
|
84
|
-
description: "Send a goal to a ROS2 action server. Use for long-running operations like navigation or arm movements.",
|
|
273
|
+
description: "Send a goal to a ROS2 action server. Use for long-running operations like navigation or arm movements. Pass robot_id to target a specific robot.",
|
|
85
274
|
parametersJsonSchema: schemaFromProps(
|
|
86
275
|
{
|
|
87
276
|
action: { type: "string", description: "The ROS2 action server name (e.g., '/navigate_to_pose')" },
|
|
88
277
|
actionType: { type: "string", description: "The ROS2 action type (e.g., 'nav2_msgs/action/NavigateToPose')" },
|
|
89
278
|
goal: { type: "object", description: "The action goal parameters" },
|
|
279
|
+
...ROBOT_ID_PROP,
|
|
90
280
|
},
|
|
91
281
|
["action", "actionType", "goal"],
|
|
92
282
|
),
|
|
93
283
|
},
|
|
94
284
|
{
|
|
95
285
|
name: "ros2_param_get",
|
|
96
|
-
description: "Get the value of a ROS2 parameter from a node. Use to check robot configuration values.",
|
|
286
|
+
description: "Get the value of a ROS2 parameter from a node. Use to check robot configuration values. Pass robot_id to target a specific robot.",
|
|
97
287
|
parametersJsonSchema: schemaFromProps(
|
|
98
288
|
{
|
|
99
289
|
node: { type: "string", description: "The fully qualified node name (e.g., '/turtlebot3/controller')" },
|
|
100
290
|
parameter: { type: "string", description: "The parameter name (e.g., 'max_velocity')" },
|
|
291
|
+
...ROBOT_ID_PROP,
|
|
101
292
|
},
|
|
102
293
|
["node", "parameter"],
|
|
103
294
|
),
|
|
104
295
|
},
|
|
105
296
|
{
|
|
106
297
|
name: "ros2_param_set",
|
|
107
|
-
description: "Set the value of a ROS2 parameter on a node. Use to change robot configuration at runtime.",
|
|
298
|
+
description: "Set the value of a ROS2 parameter on a node. Use to change robot configuration at runtime. Pass robot_id to target a specific robot.",
|
|
108
299
|
parametersJsonSchema: schemaFromProps(
|
|
109
300
|
{
|
|
110
301
|
node: { type: "string", description: "The fully qualified node name" },
|
|
111
302
|
parameter: { type: "string", description: "The parameter name" },
|
|
112
303
|
value: { type: "object", description: "The new parameter value" },
|
|
304
|
+
...ROBOT_ID_PROP,
|
|
113
305
|
},
|
|
114
306
|
["node", "parameter", "value"],
|
|
115
307
|
),
|
|
116
308
|
},
|
|
117
309
|
{
|
|
118
310
|
name: "ros2_camera_snapshot",
|
|
119
|
-
description: "Capture a single image from a ROS2 camera topic. Use when the user asks what the robot sees or requests a photo. Supports CompressedImage and raw Image.",
|
|
311
|
+
description: "Capture a single image from a ROS2 camera topic. Use when the user asks what the robot sees or requests a photo. Supports CompressedImage and raw Image. Pass robot_id to capture from a specific robot's camera.",
|
|
120
312
|
parametersJsonSchema: schemaFromProps({
|
|
121
|
-
topic: { type: "string", description: "Camera image topic (default from
|
|
313
|
+
topic: { type: "string", description: "Camera image topic (default from the robot's cameraTopic or /camera/camera/color/image_raw/compressed)" },
|
|
122
314
|
message_type: { type: "string", description: "'CompressedImage' or 'Image' (default: CompressedImage)" },
|
|
123
315
|
timeout: { type: "number", description: "Timeout in milliseconds (default: 10000)" },
|
|
316
|
+
...ROBOT_ID_PROP,
|
|
124
317
|
}),
|
|
125
318
|
},
|
|
126
319
|
{
|
|
127
320
|
name: "ros2_depth_distance",
|
|
128
|
-
description: "Get distance in meters from the robot's depth camera. Samples the center of the depth image. Use when the user asks how far they are from the robot.",
|
|
321
|
+
description: "Get distance in meters from the robot's depth camera. Samples the center of the depth image. Use when the user asks how far they are from the robot. Pass robot_id to sample a specific robot's depth camera.",
|
|
129
322
|
parametersJsonSchema: schemaFromProps({
|
|
130
323
|
topic: { type: "string", description: `Depth image topic (default: ${DEFAULT_DEPTH_TOPIC})` },
|
|
131
324
|
timeout: { type: "number", description: "Timeout in ms (default 5000)" },
|
|
325
|
+
...ROBOT_ID_PROP,
|
|
132
326
|
}),
|
|
133
327
|
},
|
|
134
328
|
{
|
|
@@ -176,6 +370,49 @@ export const GEMINI_FUNCTION_DECLARATIONS: FunctionDeclaration[] = [
|
|
|
176
370
|
namespace: { type: "string", description: "Optional namespace override; defaults to the robot namespace." },
|
|
177
371
|
}),
|
|
178
372
|
},
|
|
373
|
+
{
|
|
374
|
+
name: "run_mission",
|
|
375
|
+
description:
|
|
376
|
+
"Execute a multi-step mission by chaining capabilities (the verbs returned by ros2_list_capabilities). PASS EITHER a natural-language `goal` (recommended for simple verbs like 'find a chair', 'take a picture', 'follow me', 'find a chair and drive toward it') OR an explicit `mission.steps[]` plan when you need precise control. Steps run sequentially; each step's outputs are available to later steps via {{stepId.outputs.fieldName}} template references. Default on_fail behaviour is 'stop' (abort on first error). Returns a per-step result list, a summary line, a mission_id you can pass to mission_cancel to abort mid-run, and (when a goal was provided) the compiled plan + candidate match list so you can see what the planner did. When memory is enabled, every step is also written to the shared memory under namespace mission:<mission_id> so a second agent can recall the timeline via memory_recall. Today the runner supports: drive_base, take_snapshot, measure_depth, list_topics, publish_topic, subscribe_once, follow_person, find_object. Pass mission.robot_id (or top-level robot_id with goal) to target every step at one robot.",
|
|
377
|
+
parametersJsonSchema: {
|
|
378
|
+
type: "object",
|
|
379
|
+
properties: {
|
|
380
|
+
goal: {
|
|
381
|
+
type: "string",
|
|
382
|
+
description:
|
|
383
|
+
"Natural-language goal — the local planner compiles it into a mission against the capability registry. Examples: 'find a chair', 'find a chair and drive toward it', 'take a picture', 'follow me', 'measure depth', 'drive forward at 0.3 m/s', 'turn left', 'stop'. Either goal or mission must be provided; mission wins if both are set.",
|
|
384
|
+
},
|
|
385
|
+
robot_id: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: "Optional robot id (from ros2_list_robots) — used when 'goal' is provided to scope every compiled step to one robot.",
|
|
388
|
+
},
|
|
389
|
+
mission: {
|
|
390
|
+
type: "object",
|
|
391
|
+
description:
|
|
392
|
+
'Declarative mission plan: { name?: string, goal?: string, robot_id?: string, steps: [{ id: string, capability: string, inputs?: object, on_fail?: "stop"|"continue" }] }. mission.robot_id (from ros2_list_robots) is the default robot for every step; individual steps can override via inputs.robot_id. Example: { "name": "find then approach chair", "robot_id": "robotA", "steps": [{ "id": "find", "capability": "find_object", "inputs": { "target": "chair" } }, { "id": "go", "capability": "drive_base", "inputs": { "linear_x": 0.2, "angular_z": "{{find.outputs.horizontal_offset}}" } }] }',
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "mission_cancel",
|
|
399
|
+
description:
|
|
400
|
+
"Cancel a mission that's currently running in this Gemini CLI process. Pass the mission_id returned by run_mission. The mission runner stops at the next step boundary (the in-flight step finishes naturally), marks remaining steps as 'cancelled', and returns. If the mission has already finished (or the id is unknown), this is a no-op that returns found=false. Optional 'reason' is recorded in the cancelled step results for traceability.",
|
|
401
|
+
parametersJsonSchema: {
|
|
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 cancelled mission result.",
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
required: ["mission_id"],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
179
416
|
];
|
|
180
417
|
|
|
181
418
|
/** Single tool object for Gemini (one item in config.tools array). */
|
|
@@ -213,7 +450,235 @@ export async function executeTool(
|
|
|
213
450
|
if (MEMORY_TOOL_NAMES.has(name)) {
|
|
214
451
|
return executeMemoryTool(name, args, config);
|
|
215
452
|
}
|
|
216
|
-
|
|
453
|
+
// ros2_list_capabilities reads skill manifests + intrinsic verbs from
|
|
454
|
+
// local config — no transport required. robot_id is accepted but
|
|
455
|
+
// doesn't change today's response (every robot exposes the same
|
|
456
|
+
// capabilities); we still validate it so unknown ids surface as a
|
|
457
|
+
// clean tool error.
|
|
458
|
+
if (name === "ros2_list_capabilities") {
|
|
459
|
+
const resolved = resolveRobotForTool(config, args);
|
|
460
|
+
if ("error" in resolved) return resolved.error;
|
|
461
|
+
const caps: Capability[] = listAllCapabilities(config);
|
|
462
|
+
const intrinsic = caps.filter((c) => c.source?.kind === "builtin").length;
|
|
463
|
+
const skill = caps.filter((c) => c.source?.kind === "skill").length;
|
|
464
|
+
return {
|
|
465
|
+
output: JSON.stringify({
|
|
466
|
+
success: true,
|
|
467
|
+
total: caps.length,
|
|
468
|
+
intrinsic_count: intrinsic,
|
|
469
|
+
skill_count: skill,
|
|
470
|
+
capabilities: caps,
|
|
471
|
+
}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
// ros2_list_robots reads the multi-robot section of the config (with
|
|
475
|
+
// legacy fallback) — no transport required either.
|
|
476
|
+
if (name === "ros2_list_robots") {
|
|
477
|
+
const robots = listRobots(config);
|
|
478
|
+
const active = getActiveRobotId(config);
|
|
479
|
+
return {
|
|
480
|
+
output: JSON.stringify({
|
|
481
|
+
success: true,
|
|
482
|
+
total: robots.length,
|
|
483
|
+
active_robot_id: active,
|
|
484
|
+
robots,
|
|
485
|
+
}),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// ros2_find_robots_for is config-driven by default. Only the
|
|
489
|
+
// online=true|false branch touches the transport (same heuristic as
|
|
490
|
+
// ros2_discover_robots). Stays above the unconditional transport
|
|
491
|
+
// resolution so static-fleet planning works offline.
|
|
492
|
+
if (name === "ros2_find_robots_for") {
|
|
493
|
+
const cap = typeof args["capability"] === "string" ? args["capability"] : undefined;
|
|
494
|
+
const kind = typeof args["kind"] === "string" ? args["kind"] : undefined;
|
|
495
|
+
const online = typeof args["online"] === "boolean" ? args["online"] : undefined;
|
|
496
|
+
let onlineIds: Set<string> | undefined;
|
|
497
|
+
if (online !== undefined) {
|
|
498
|
+
// Need any one live transport — use the active robot's. Errors
|
|
499
|
+
// here surface as `success: false` so the model can recover.
|
|
500
|
+
const resolved = resolveRobotForTool(config, {});
|
|
501
|
+
if ("error" in resolved) return resolved.error;
|
|
502
|
+
const transport = await getTransportForRobot(config, resolved.robot);
|
|
503
|
+
if (transport.getStatus() !== "connected") {
|
|
504
|
+
return {
|
|
505
|
+
output: JSON.stringify({
|
|
506
|
+
success: false,
|
|
507
|
+
error:
|
|
508
|
+
"online filter requires the ROS transport to be connected. Drop the 'online' arg to run config-only, or check that zenohd / rosbridge is up.",
|
|
509
|
+
}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const topics = await transport.listTopics();
|
|
513
|
+
const disc = discoverRobots(topics, config);
|
|
514
|
+
onlineIds = new Set(disc.configured_online.map((r) => r.id));
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
const result = findRobotsFor(config, { capability: cap, kind, online }, onlineIds);
|
|
518
|
+
return {
|
|
519
|
+
output: JSON.stringify({
|
|
520
|
+
success: true,
|
|
521
|
+
query: result.query,
|
|
522
|
+
total: result.total,
|
|
523
|
+
robots: result.robots.map((m) => ({
|
|
524
|
+
id: m.robot.id,
|
|
525
|
+
name: m.robot.name,
|
|
526
|
+
namespace: m.robot.namespace,
|
|
527
|
+
kind: m.robot.kind,
|
|
528
|
+
sensors: m.robot.sensors,
|
|
529
|
+
capabilities: m.robot.capabilities ?? null,
|
|
530
|
+
cameraTopic: m.robot.cameraTopic,
|
|
531
|
+
online: m.online,
|
|
532
|
+
matched_capability_explicitly: m.matched_capability_explicitly,
|
|
533
|
+
score: m.score,
|
|
534
|
+
})),
|
|
535
|
+
}),
|
|
536
|
+
};
|
|
537
|
+
} catch (e) {
|
|
538
|
+
return {
|
|
539
|
+
output: JSON.stringify({
|
|
540
|
+
success: false,
|
|
541
|
+
error: e instanceof Error ? e.message : String(e),
|
|
542
|
+
}),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// mission_cancel mutates only the in-process MissionRegistry and
|
|
547
|
+
// must work without a ROS transport — keep it above the
|
|
548
|
+
// unconditional transport resolution.
|
|
549
|
+
if (name === "mission_cancel") {
|
|
550
|
+
const missionId = typeof args["mission_id"] === "string" ? (args["mission_id"] as string).trim() : "";
|
|
551
|
+
if (!missionId) {
|
|
552
|
+
return {
|
|
553
|
+
output: JSON.stringify({
|
|
554
|
+
success: false,
|
|
555
|
+
error: "mission_cancel requires 'mission_id' (a non-empty string returned by run_mission).",
|
|
556
|
+
}),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const reason = typeof args["reason"] === "string" ? (args["reason"] as string) : undefined;
|
|
560
|
+
const outcome = MISSION_REGISTRY.cancel(missionId, reason);
|
|
561
|
+
return {
|
|
562
|
+
output: JSON.stringify({
|
|
563
|
+
success: true,
|
|
564
|
+
mission_id: missionId,
|
|
565
|
+
found: outcome.found,
|
|
566
|
+
already_cancelled: outcome.alreadyCancelled,
|
|
567
|
+
reason: reason ?? null,
|
|
568
|
+
}),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// run_mission dispatches to other tools recursively — let each sub-tool
|
|
572
|
+
// handle its own transport gating instead of requiring a connection up
|
|
573
|
+
// front. Validate the input shape here; defer execution to runMission().
|
|
574
|
+
if (name === "run_mission") {
|
|
575
|
+
const caps = listAllCapabilities(config);
|
|
576
|
+
const missionArg = args["mission"];
|
|
577
|
+
const goalArg = args["goal"];
|
|
578
|
+
const topLevelRobotId = typeof args["robot_id"] === "string" ? (args["robot_id"] as string) : undefined;
|
|
579
|
+
|
|
580
|
+
// Phase 1.g — accept either an explicit mission OR a natural-language
|
|
581
|
+
// goal. We surface the planner's candidates + suggestions in the
|
|
582
|
+
// response so the agent can self-correct without an extra round-trip.
|
|
583
|
+
let mission: Mission;
|
|
584
|
+
let plannerInfo:
|
|
585
|
+
| { compiled_from_goal: string; candidates: unknown[]; unmatched_verbs?: string[] }
|
|
586
|
+
| undefined;
|
|
587
|
+
if (missionArg && typeof missionArg === "object" && !Array.isArray(missionArg)) {
|
|
588
|
+
mission = missionArg as Mission;
|
|
589
|
+
if (!Array.isArray(mission.steps)) {
|
|
590
|
+
return { output: "mission.steps must be an array of step objects." };
|
|
591
|
+
}
|
|
592
|
+
} else if (typeof goalArg === "string" && goalArg.trim().length > 0) {
|
|
593
|
+
const planned = compileGoalToMission(goalArg, caps, { robot_id: topLevelRobotId });
|
|
594
|
+
if (!planned.mission) {
|
|
595
|
+
return {
|
|
596
|
+
output: JSON.stringify({
|
|
597
|
+
success: false,
|
|
598
|
+
error: planned.error,
|
|
599
|
+
goal: goalArg,
|
|
600
|
+
suggestions: planned.suggestions,
|
|
601
|
+
...(planned.unmatched_verbs ? { unmatched_verbs: planned.unmatched_verbs } : {}),
|
|
602
|
+
}),
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
mission = planned.mission;
|
|
606
|
+
plannerInfo = {
|
|
607
|
+
compiled_from_goal: goalArg,
|
|
608
|
+
candidates: planned.candidates,
|
|
609
|
+
...(planned.unmatched_verbs ? { unmatched_verbs: planned.unmatched_verbs } : {}),
|
|
610
|
+
};
|
|
611
|
+
} else {
|
|
612
|
+
return {
|
|
613
|
+
output:
|
|
614
|
+
'run_mission requires either "mission" (object with steps[]) or "goal" (natural-language string). Pass at least one.',
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
// Validate mission.robot_id up-front so the agent gets a single
|
|
618
|
+
// clean error (with known ids) instead of one per step.
|
|
619
|
+
if (typeof mission.robot_id === "string" && mission.robot_id.trim().length > 0) {
|
|
620
|
+
const resolved = resolveRobotForTool(config, { robot_id: mission.robot_id });
|
|
621
|
+
if ("error" in resolved) return resolved.error;
|
|
622
|
+
}
|
|
623
|
+
const dispatcher: MissionToolDispatcher = async (toolName, toolArgs) => {
|
|
624
|
+
const sub = await executeTool(toolName, toolArgs, config);
|
|
625
|
+
return { text: sub.output };
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// Phase 1.f — register a mission_id so a sibling mission_cancel
|
|
629
|
+
// call can flip the cancellation token; wire a transcript sink
|
|
630
|
+
// when memory is enabled so a second agent can recall the timeline.
|
|
631
|
+
const missionId = generateMissionId();
|
|
632
|
+
const { entry: regEntry, dispose: disposeRegistry } = MISSION_REGISTRY.register(
|
|
633
|
+
missionId,
|
|
634
|
+
{ name: mission.name },
|
|
635
|
+
);
|
|
636
|
+
const memory = await ensureMemory(config);
|
|
637
|
+
const transcript = memory ? createMemoryTranscriptSink(memory, missionId) : undefined;
|
|
638
|
+
|
|
639
|
+
let result;
|
|
640
|
+
try {
|
|
641
|
+
result = await runMission(mission, caps, MISSION_BINDINGS, dispatcher, {
|
|
642
|
+
mission_id: missionId,
|
|
643
|
+
cancellation: regEntry.cancellation,
|
|
644
|
+
transcript,
|
|
645
|
+
adapter: "gemini",
|
|
646
|
+
});
|
|
647
|
+
} finally {
|
|
648
|
+
disposeRegistry();
|
|
649
|
+
}
|
|
650
|
+
const compact = {
|
|
651
|
+
status: result.status,
|
|
652
|
+
mission_id: result.mission_id,
|
|
653
|
+
...(result.cancellation_reason ? { cancellation_reason: result.cancellation_reason } : {}),
|
|
654
|
+
...(transcript ? { transcript_namespace: missionTranscriptNamespace(missionId) } : {}),
|
|
655
|
+
...(plannerInfo ? { planner: plannerInfo } : {}),
|
|
656
|
+
steps_run: result.steps_run,
|
|
657
|
+
steps_total: result.steps_total,
|
|
658
|
+
duration_ms: result.duration_ms,
|
|
659
|
+
summary: result.summary,
|
|
660
|
+
steps: result.steps.map((s) => ({
|
|
661
|
+
id: s.id,
|
|
662
|
+
capability: s.capability,
|
|
663
|
+
status: s.status,
|
|
664
|
+
inputs: s.inputs,
|
|
665
|
+
outputs: s.outputs,
|
|
666
|
+
...(s.error ? { error: s.error } : {}),
|
|
667
|
+
duration_ms: s.duration_ms,
|
|
668
|
+
})),
|
|
669
|
+
};
|
|
670
|
+
return { output: `${result.summary}\n${JSON.stringify(compact)}` };
|
|
671
|
+
}
|
|
672
|
+
// Resolve target robot once for every transport-bound tool. Unknown
|
|
673
|
+
// robot_id surfaces as a tool error (not a thrown exception).
|
|
674
|
+
const resolvedRobot = resolveRobotForTool(config, args);
|
|
675
|
+
if ("error" in resolvedRobot) return resolvedRobot.error;
|
|
676
|
+
const { robot } = resolvedRobot;
|
|
677
|
+
|
|
678
|
+
// Route through the per-robot pool: returns the shared `__global__`
|
|
679
|
+
// when this robot has no override (the common case), OR a dedicated
|
|
680
|
+
// transport on first use when the robot declares its own.
|
|
681
|
+
const transport = await getTransportForRobot(config, robot);
|
|
217
682
|
|
|
218
683
|
switch (name) {
|
|
219
684
|
case "ros2_list_topics": {
|
|
@@ -229,6 +694,24 @@ export async function executeTool(
|
|
|
229
694
|
return { output: text };
|
|
230
695
|
}
|
|
231
696
|
|
|
697
|
+
case "ros2_discover_robots": {
|
|
698
|
+
// Live discovery: detect /<ns>/cmd_vel namespaces, classify
|
|
699
|
+
// against config. Pure-function classifier in @agenticros/core
|
|
700
|
+
// so all three adapters return the same shape.
|
|
701
|
+
const topics = await transport.listTopics();
|
|
702
|
+
const result = discoverRobots(topics, config);
|
|
703
|
+
return {
|
|
704
|
+
output: JSON.stringify({
|
|
705
|
+
success: true,
|
|
706
|
+
total_topics: result.total_topics,
|
|
707
|
+
detected: result.detected,
|
|
708
|
+
configured_online: result.configured_online,
|
|
709
|
+
configured_offline: result.configured_offline,
|
|
710
|
+
unknown_detected: result.unknown_detected,
|
|
711
|
+
}),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
232
715
|
case "ros2_publish": {
|
|
233
716
|
const rawTopicIn = String(args["topic"] ?? "").trim();
|
|
234
717
|
if (process.stderr?.write) {
|
|
@@ -248,7 +731,7 @@ export async function executeTool(
|
|
|
248
731
|
const topic =
|
|
249
732
|
cmdVelMatch && !segment.toLowerCase().startsWith("robot")
|
|
250
733
|
? `/robot${segment.replace(/-/g, "")}/cmd_vel`
|
|
251
|
-
: toNamespacedTopic(
|
|
734
|
+
: toNamespacedTopic(robot.namespace, rawTopicIn);
|
|
252
735
|
if (process.stderr?.write) {
|
|
253
736
|
process.stderr.write(`[AgenticROS] ros2_publish: → topic=${topic}\n`);
|
|
254
737
|
}
|
|
@@ -270,7 +753,7 @@ export async function executeTool(
|
|
|
270
753
|
|
|
271
754
|
case "ros2_subscribe_once": {
|
|
272
755
|
const rawTopic = args["topic"] as string;
|
|
273
|
-
const topic = toNamespacedTopic(
|
|
756
|
+
const topic = toNamespacedTopic(robot.namespace, rawTopic);
|
|
274
757
|
let msgType = args["type"] as string | undefined;
|
|
275
758
|
const timeout = (args["timeout"] as number | undefined) ?? 5000;
|
|
276
759
|
if (!msgType && /\/?(camera|image|color|depth)/i.test(rawTopic)) {
|
|
@@ -305,7 +788,7 @@ export async function executeTool(
|
|
|
305
788
|
|
|
306
789
|
case "ros2_service_call": {
|
|
307
790
|
const rawService = args["service"] as string;
|
|
308
|
-
const service = toNamespacedTopic(
|
|
791
|
+
const service = toNamespacedTopic(robot.namespace, rawService);
|
|
309
792
|
const type = args["type"] as string | undefined;
|
|
310
793
|
const reqArgs = args["args"] as Record<string, unknown> | undefined;
|
|
311
794
|
const response = await transport.callService({ service, type, args: reqArgs });
|
|
@@ -319,7 +802,7 @@ export async function executeTool(
|
|
|
319
802
|
|
|
320
803
|
case "ros2_action_goal": {
|
|
321
804
|
const rawAction = args["action"] as string;
|
|
322
|
-
const action = toNamespacedTopic(
|
|
805
|
+
const action = toNamespacedTopic(robot.namespace, rawAction);
|
|
323
806
|
const actionType = args["actionType"] as string;
|
|
324
807
|
const goal = args["goal"] as Record<string, unknown>;
|
|
325
808
|
const actionResult = await transport.sendActionGoal({ action, actionType, args: goal });
|
|
@@ -333,7 +816,7 @@ export async function executeTool(
|
|
|
333
816
|
|
|
334
817
|
case "ros2_param_get": {
|
|
335
818
|
const rawNode = args["node"] as string;
|
|
336
|
-
const node = toNamespacedTopic(
|
|
819
|
+
const node = toNamespacedTopic(robot.namespace, rawNode);
|
|
337
820
|
const parameter = args["parameter"] as string;
|
|
338
821
|
const response = await transport.callService({
|
|
339
822
|
service: `${node}/get_parameters`,
|
|
@@ -351,7 +834,7 @@ export async function executeTool(
|
|
|
351
834
|
|
|
352
835
|
case "ros2_param_set": {
|
|
353
836
|
const rawNode = args["node"] as string;
|
|
354
|
-
const node = toNamespacedTopic(
|
|
837
|
+
const node = toNamespacedTopic(robot.namespace, rawNode);
|
|
355
838
|
const parameter = args["parameter"] as string;
|
|
356
839
|
const value = args["value"];
|
|
357
840
|
const response = await transport.callService({
|
|
@@ -369,9 +852,9 @@ export async function executeTool(
|
|
|
369
852
|
|
|
370
853
|
case "ros2_camera_snapshot": {
|
|
371
854
|
const defaultTopic =
|
|
372
|
-
(
|
|
855
|
+
(robot.cameraTopic ?? "").trim() || "/camera/camera/color/image_raw/compressed";
|
|
373
856
|
const rawTopic = (args["topic"] as string | undefined) ?? defaultTopic;
|
|
374
|
-
const topic = resolveCameraSubscribeTopic(
|
|
857
|
+
const topic = resolveCameraSubscribeTopic(robot.namespace, rawTopic);
|
|
375
858
|
const rawMsgType = args["message_type"] as string | undefined;
|
|
376
859
|
const messageType: "CompressedImage" | "Image" = rawMsgType === "Image" ? "Image" : "CompressedImage";
|
|
377
860
|
const timeout = (args["timeout"] as number | undefined) ?? 10000;
|
|
@@ -445,7 +928,7 @@ export async function executeTool(
|
|
|
445
928
|
|
|
446
929
|
case "ros2_depth_distance": {
|
|
447
930
|
const rawTopic = (args["topic"] as string | undefined)?.trim() || DEFAULT_DEPTH_TOPIC;
|
|
448
|
-
const topic = resolveCameraSubscribeTopic(
|
|
931
|
+
const topic = resolveCameraSubscribeTopic(robot.namespace, rawTopic);
|
|
449
932
|
const timeout = (args["timeout"] as number | undefined) ?? 5000;
|
|
450
933
|
try {
|
|
451
934
|
const result = await getDepthDistance(transport, topic, timeout);
|