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.
Files changed (100) hide show
  1. package/dist/__tests__/robot-config.test.d.ts +2 -0
  2. package/dist/__tests__/robot-config.test.d.ts.map +1 -0
  3. package/dist/__tests__/robot-config.test.js +374 -0
  4. package/dist/__tests__/robot-config.test.js.map +1 -0
  5. package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
  6. package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
  7. package/dist/__tests__/transport-shorthand.test.js +88 -0
  8. package/dist/__tests__/transport-shorthand.test.js.map +1 -0
  9. package/dist/commands/down.d.ts.map +1 -1
  10. package/dist/commands/down.js +55 -3
  11. package/dist/commands/down.js.map +1 -1
  12. package/dist/commands/robots.d.ts +57 -0
  13. package/dist/commands/robots.d.ts.map +1 -0
  14. package/dist/commands/robots.js +678 -0
  15. package/dist/commands/robots.js.map +1 -0
  16. package/dist/index.js +28 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/util/mcp-discovery.d.ts +57 -0
  19. package/dist/util/mcp-discovery.d.ts.map +1 -0
  20. package/dist/util/mcp-discovery.js +153 -0
  21. package/dist/util/mcp-discovery.js.map +1 -0
  22. package/dist/util/robot-config.d.ts +167 -0
  23. package/dist/util/robot-config.d.ts.map +1 -0
  24. package/dist/util/robot-config.js +375 -0
  25. package/dist/util/robot-config.js.map +1 -0
  26. package/dist/util/transport-shorthand.d.ts +66 -0
  27. package/dist/util/transport-shorthand.d.ts.map +1 -0
  28. package/dist/util/transport-shorthand.js +102 -0
  29. package/dist/util/transport-shorthand.js.map +1 -0
  30. package/package.json +2 -1
  31. package/runtime/BUNDLE.json +1 -1
  32. package/runtime/README.md +4 -0
  33. package/runtime/package.json +6 -1
  34. package/runtime/packages/agenticros/openclaw.plugin.json +6 -0
  35. package/runtime/packages/agenticros/package.json +2 -1
  36. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
  37. package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
  38. package/runtime/packages/agenticros/src/service.ts +120 -2
  39. package/runtime/packages/agenticros/src/skill-loader.ts +9 -1
  40. package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
  41. package/runtime/packages/agenticros/src/tools/index.ts +49 -9
  42. package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
  43. package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
  44. package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
  45. package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
  46. package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
  47. package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
  48. package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
  49. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
  50. package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
  51. package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
  52. package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
  53. package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
  54. package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
  55. package/runtime/packages/agenticros-claude-code/package.json +2 -1
  56. package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +964 -0
  57. package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
  58. package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
  59. package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
  60. package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
  61. package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
  62. package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
  63. package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
  64. package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
  65. package/runtime/packages/agenticros-gemini/package.json +1 -0
  66. package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
  67. package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
  68. package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
  69. package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
  70. package/runtime/packages/core/package.json +1 -1
  71. package/runtime/packages/core/src/__tests__/capabilities.test.ts +261 -0
  72. package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
  73. package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
  74. package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
  75. package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
  76. package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
  77. package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
  78. package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
  79. package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
  80. package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
  81. package/runtime/packages/core/src/capabilities.ts +340 -0
  82. package/runtime/packages/core/src/config.ts +220 -38
  83. package/runtime/packages/core/src/discovery.ts +193 -0
  84. package/runtime/packages/core/src/find-robots-for.ts +164 -0
  85. package/runtime/packages/core/src/index.ts +67 -1
  86. package/runtime/packages/core/src/mission-registry.ts +142 -0
  87. package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
  88. package/runtime/packages/core/src/mission.ts +574 -0
  89. package/runtime/packages/core/src/planner/index.ts +564 -0
  90. package/runtime/packages/core/src/robots.ts +282 -0
  91. package/runtime/packages/core/src/topic-utils.ts +47 -9
  92. package/runtime/packages/core/src/transport-pool.ts +171 -0
  93. package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
  94. package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
  95. package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
  96. package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
  97. package/runtime/scripts/refresh-skill-deps.mjs +381 -0
  98. package/runtime/scripts/setup_gateway_plugin.sh +32 -5
  99. package/runtime/scripts/start_demo.sh +26 -3
  100. package/runtime/scripts/sync-skill-tools.mjs +67 -10
@@ -1,6 +1,11 @@
1
1
  import type { TransportConfig } from "@agenticros/core";
2
- import type { RosTransport } from "@agenticros/core";
3
- import { createTransport, getTransportConfig } from "@agenticros/core";
2
+ import type { RosTransport, ResolvedRobot } from "@agenticros/core";
3
+ import {
4
+ createTransport,
5
+ getTransportConfig,
6
+ getTransportConfigForRobot,
7
+ hasRobotTransportOverride,
8
+ } from "@agenticros/core";
4
9
  import type { OpenClawPluginApi } from "./plugin-api.js";
5
10
  import type { PluginLogger } from "./plugin-api.js";
6
11
  import type { AgenticROSConfig } from "@agenticros/core";
@@ -238,6 +243,112 @@ async function ensureTransportConnected(
238
243
  await next;
239
244
  }
240
245
 
246
+ // ─────────────────────────────────────────────────────────────────────────────
247
+ // Phase 1.d-pool: per-robot transport override pool.
248
+ //
249
+ // The existing `transport` singleton above (with eager-retry / preflight /
250
+ // poll-reconnect) stays the source of truth for every robot WITHOUT a
251
+ // per-robot `transport` override in config. That's the common case and
252
+ // the plugin's hard-won robustness lives in that singleton.
253
+ //
254
+ // For robots that DO declare an override we maintain a smaller, lazier
255
+ // pool here. Override entries don't (yet) get the eager-connect /
256
+ // preflight / polling treatment — they connect on first tool call, fail
257
+ // loudly to the agent on connect timeout, and self-heal on the next
258
+ // acquire. That's intentional: most fleets have one or two override
259
+ // robots and the agent-facing error path is sufficient feedback.
260
+ //
261
+ // Pool key: `robot.id` (validated by `hasRobotTransportOverride`).
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+
264
+ const overrideTransports = new Map<string, RosTransport>();
265
+ const overrideInFlight = new Map<string, Promise<RosTransport>>();
266
+ const OVERRIDE_CONNECT_TIMEOUT_MS = 15_000;
267
+
268
+ /**
269
+ * Return the right transport for `robot`. Per-robot tools call this
270
+ * INSTEAD of `getTransport()` so a robot with a custom transport gets
271
+ * its own connection. Robots without an override fall through to the
272
+ * service-managed singleton with all its retry / preflight machinery.
273
+ *
274
+ * Throws when the singleton is requested but the service hasn't
275
+ * connected yet — same contract as `getTransport()`.
276
+ *
277
+ * Throws (or surfaces as a tool error after the timeout) when an
278
+ * override entry can't connect — the agent sees a clean failure and
279
+ * can fall back to a different robot id.
280
+ */
281
+ export async function getTransportForRobot(
282
+ config: AgenticROSConfig,
283
+ robot: ResolvedRobot,
284
+ ): Promise<RosTransport> {
285
+ if (!hasRobotTransportOverride(config, robot.id)) {
286
+ // Fast path: no override → reuse the service-managed singleton
287
+ // (with its retries and preflight). Same as the pre-pool behaviour.
288
+ return getTransport();
289
+ }
290
+ return acquireOverrideTransport(config, robot);
291
+ }
292
+
293
+ async function acquireOverrideTransport(
294
+ config: AgenticROSConfig,
295
+ robot: ResolvedRobot,
296
+ ): Promise<RosTransport> {
297
+ const cached = overrideTransports.get(robot.id);
298
+ if (cached && cached.getStatus() === "connected") return cached;
299
+
300
+ // Concurrent first-acquires share one connect promise so two near-
301
+ // simultaneous tool calls don't double-open the same Zenoh socket.
302
+ const flight = overrideInFlight.get(robot.id);
303
+ if (flight) return flight;
304
+
305
+ if (cached) {
306
+ // Stale entry — drop cleanly so a fresh build can proceed.
307
+ overrideTransports.delete(robot.id);
308
+ cached.disconnect().catch(() => {});
309
+ }
310
+
311
+ const connectFlight = (async (): Promise<RosTransport> => {
312
+ const transportCfg = buildTransportConfig(getTransportConfigForRobot(config, robot.id));
313
+ const t = await createTransport(transportCfg);
314
+ await connectWithTimeout(t, OVERRIDE_CONNECT_TIMEOUT_MS);
315
+ overrideTransports.set(robot.id, t);
316
+ return t;
317
+ })();
318
+ overrideInFlight.set(robot.id, connectFlight);
319
+ try {
320
+ return await connectFlight;
321
+ } finally {
322
+ overrideInFlight.delete(robot.id);
323
+ }
324
+ }
325
+
326
+ async function connectWithTimeout(t: RosTransport, timeoutMs: number): Promise<void> {
327
+ const timeout = new Promise<never>((_, reject) => {
328
+ setTimeout(
329
+ () =>
330
+ reject(
331
+ new Error(
332
+ `Per-robot transport connect timed out after ${timeoutMs / 1000}s. ` +
333
+ "Check the robot's transport override in config (router endpoint, mode).",
334
+ ),
335
+ ),
336
+ timeoutMs,
337
+ );
338
+ });
339
+ await Promise.race([t.connect(), timeout]);
340
+ }
341
+
342
+ async function drainOverridePool(): Promise<void> {
343
+ const pending: Promise<unknown>[] = [];
344
+ for (const t of overrideTransports.values()) {
345
+ pending.push(t.disconnect().catch(() => {}));
346
+ }
347
+ overrideTransports.clear();
348
+ overrideInFlight.clear();
349
+ await Promise.all(pending);
350
+ }
351
+
241
352
  const RETRY_INTERVAL_MS = 10000;
242
353
  const DISCONNECTED_POLL_MS = 15000;
243
354
 
@@ -283,6 +394,13 @@ export function registerService(api: OpenClawPluginApi, config: AgenticROSConfig
283
394
  currentMode = null;
284
395
  api.logger.info("ROS2 transport disconnected");
285
396
  }
397
+ // Drain any per-robot override connections too — otherwise they'd
398
+ // leak open Zenoh sockets across plugin reloads.
399
+ const overrideCount = overrideTransports.size;
400
+ if (overrideCount > 0) {
401
+ await drainOverridePool();
402
+ api.logger.info(`Disconnected ${overrideCount} per-robot override transport(s)`);
403
+ }
286
404
  },
287
405
  });
288
406
 
@@ -129,12 +129,20 @@ function findSkillInPath(dirPath: string): { entry: string; packageName: string
129
129
  if (!existsSync(dirPath)) return null;
130
130
  const pkgPath = join(dirPath, "package.json");
131
131
  if (!existsSync(pkgPath)) return null;
132
- let pkg: { agenticrosSkill?: boolean; main?: string; name?: string };
132
+ let pkg: {
133
+ agenticrosSkill?: boolean | Record<string, unknown>;
134
+ main?: string;
135
+ name?: string;
136
+ };
133
137
  try {
134
138
  pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
135
139
  } catch {
136
140
  return null;
137
141
  }
142
+ // Accept either the legacy boolean (`"agenticrosSkill": true`) or the
143
+ // Phase-1 object form (`"agenticrosSkill": { capabilities: [...] }`).
144
+ // Anything truthy registers the package as a skill; the capability
145
+ // schema is read separately by @agenticros/core.
138
146
  if (!pkg.agenticrosSkill) return null;
139
147
  const main = pkg.main ?? "index.js";
140
148
  const entry = join(dirPath, main);
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Phase 1.d helper — shared by every OpenClaw ROS2 tool that accepts an
3
+ * optional `robot_id` parameter. Keeping it in one place means the
4
+ * schema definition and resolution error format stay consistent across
5
+ * tools and match the claude-code adapter.
6
+ *
7
+ * Usage in a tool's parameters:
8
+ *
9
+ * parameters: Type.Object({
10
+ * topic: Type.String({ description: "..." }),
11
+ * ...ROBOT_ID_SCHEMA,
12
+ * })
13
+ *
14
+ * Usage in execute():
15
+ *
16
+ * const resolved = resolveRobotForTool(config, params);
17
+ * if ("error" in resolved) return resolved.error;
18
+ * const ns = resolved.robot.namespace;
19
+ */
20
+
21
+ import { Type } from "@sinclair/typebox";
22
+ import { resolveRobotFromArgs, type ResolvedRobot } from "@agenticros/core";
23
+ import type { AgenticROSConfig } from "@agenticros/core";
24
+
25
+ /**
26
+ * TypeBox schema fragment — spread into a tool's parameters object to
27
+ * advertise an optional `robot_id` argument.
28
+ */
29
+ export const ROBOT_ID_SCHEMA = {
30
+ robot_id: Type.Optional(
31
+ Type.String({
32
+ description:
33
+ "Optional robot id (from ros2_list_robots) to scope this call. When omitted, the active robot is used.",
34
+ }),
35
+ ),
36
+ };
37
+
38
+ /**
39
+ * Try to resolve the target robot from the tool's params. On success
40
+ * returns `{ robot }`. On unknown-id failure returns `{ error }` —
41
+ * already shaped as an OpenClaw tool response (content + details) so
42
+ * the caller can `return resolved.error` directly. The error text comes
43
+ * from `@agenticros/core/resolveRobot` and already lists known ids +
44
+ * recommends ros2_list_robots, so the agent can self-correct.
45
+ */
46
+ export function resolveRobotForTool(
47
+ config: AgenticROSConfig,
48
+ params: Record<string, unknown>,
49
+ ):
50
+ | { robot: ResolvedRobot }
51
+ | {
52
+ error: {
53
+ content: Array<{ type: "text"; text: string }>;
54
+ details: { success: false; error: string };
55
+ };
56
+ } {
57
+ try {
58
+ return { robot: resolveRobotFromArgs(config, params) };
59
+ } catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ return {
62
+ error: {
63
+ content: [{ type: "text", text: msg }],
64
+ details: { success: false, error: msg },
65
+ },
66
+ };
67
+ }
68
+ }
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "../plugin-api.js";
1
+ import type { AgentTool, OpenClawPluginApi } from "../plugin-api.js";
2
2
  import type { AgenticROSConfig } from "@agenticros/core";
3
3
  import { registerPublishTool } from "./ros2-publish.js";
4
4
  import { registerSubscribeTool } from "./ros2-subscribe.js";
@@ -8,6 +8,39 @@ import { registerParamTools } from "./ros2-param.js";
8
8
  import { registerIntrospectTool } from "./ros2-introspect.js";
9
9
  import { registerCameraTool } from "./ros2-camera.js";
10
10
  import { registerDepthDistanceTool } from "./ros2-depth-distance.js";
11
+ import { registerCapabilitiesTool } from "./ros2-capabilities.js";
12
+ import { registerRobotsTool } from "./ros2-robots.js";
13
+ import { registerDiscoverRobotsTool } from "./ros2-discover.js";
14
+ import { registerFindRobotsForTool } from "./ros2-find-robots-for.js";
15
+ import { registerMissionTool, type ToolRegistry } from "./ros2-mission.js";
16
+ import { registerMissionCancelTool } from "./mission-cancel.js";
17
+
18
+ /**
19
+ * Wrap the OpenClaw API so every registerTool() call is also recorded in
20
+ * a local tool registry. The mission runner uses the registry to
21
+ * dispatch sub-tool calls by name (e.g. capability "drive_base" routes
22
+ * to the registered "ros2_publish" tool's execute()).
23
+ *
24
+ * We keep this internal — skills that register their own tools via
25
+ * `api.registerTool` won't appear in the registry, which is fine for
26
+ * v1: today the mission runner only supports the eight intrinsic
27
+ * capability bindings declared in ros2-mission.ts. Phase 1.d will
28
+ * extend this to capture skill-declared tools as well.
29
+ */
30
+ function wrapApiWithToolCapture(api: OpenClawPluginApi): {
31
+ wrappedApi: OpenClawPluginApi;
32
+ registry: ToolRegistry;
33
+ } {
34
+ const registry: ToolRegistry = new Map();
35
+ const wrappedApi: OpenClawPluginApi = {
36
+ ...api,
37
+ registerTool: (tool: AgentTool, opts) => {
38
+ registry.set(tool.name, tool);
39
+ api.registerTool(tool, opts);
40
+ },
41
+ };
42
+ return { wrappedApi, registry };
43
+ }
11
44
 
12
45
  /**
13
46
  * Register core ROS2 tools with the OpenClaw AI agent.
@@ -15,12 +48,19 @@ import { registerDepthDistanceTool } from "./ros2-depth-distance.js";
15
48
  * Memory tools register asynchronously from index.ts after initMemory resolves.
16
49
  */
17
50
  export function registerTools(api: OpenClawPluginApi, config: AgenticROSConfig): void {
18
- registerPublishTool(api, config);
19
- registerSubscribeTool(api, config);
20
- registerServiceTool(api, config);
21
- registerActionTool(api, config);
22
- registerParamTools(api, config);
23
- registerIntrospectTool(api);
24
- registerCameraTool(api, config);
25
- registerDepthDistanceTool(api, config);
51
+ const { wrappedApi, registry } = wrapApiWithToolCapture(api);
52
+ registerPublishTool(wrappedApi, config);
53
+ registerSubscribeTool(wrappedApi, config);
54
+ registerServiceTool(wrappedApi, config);
55
+ registerActionTool(wrappedApi, config);
56
+ registerParamTools(wrappedApi, config);
57
+ registerIntrospectTool(wrappedApi);
58
+ registerCameraTool(wrappedApi, config);
59
+ registerDepthDistanceTool(wrappedApi, config);
60
+ registerCapabilitiesTool(wrappedApi, config);
61
+ registerRobotsTool(wrappedApi, config);
62
+ registerDiscoverRobotsTool(wrappedApi, config);
63
+ registerFindRobotsForTool(wrappedApi, config);
64
+ registerMissionTool(wrappedApi, config, registry);
65
+ registerMissionCancelTool(wrappedApi);
26
66
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Tool: mission_cancel — Phase 1.f of the AgenticROS strategy.
3
+ *
4
+ * Cancel a mission that's currently running in this OpenClaw plugin
5
+ * process. Pass the `mission_id` returned by `run_mission`. The mission
6
+ * runner stops at the next step boundary (the in-flight step finishes
7
+ * naturally — per-tool preemption is out of scope for Phase 1.f),
8
+ * marks remaining steps as "cancelled", and returns a result with
9
+ * `status: "cancelled"`.
10
+ *
11
+ * Idempotent / safe-by-default:
12
+ * - Unknown mission_id → `found: false`, never an error
13
+ * - Calling twice on the same id → second call returns
14
+ * `already_cancelled: true` and is otherwise a no-op
15
+ * - No transport required — this only mutates an in-process
16
+ * `MissionRegistry`
17
+ *
18
+ * Mirrored across all three adapters (Claude Code, OpenClaw, Gemini)
19
+ * with identical request/response shapes — see
20
+ * `packages/agenticros-claude-code/src/tools.ts` for the canonical one.
21
+ */
22
+
23
+ import { Type } from "@sinclair/typebox";
24
+ import type { OpenClawPluginApi } from "../plugin-api.js";
25
+ import { getMissionRegistry } from "../mission-registry.js";
26
+
27
+ export function registerMissionCancelTool(api: OpenClawPluginApi): void {
28
+ api.registerTool({
29
+ name: "mission_cancel",
30
+ label: "Cancel Mission",
31
+ description:
32
+ "Cancel a mission that's currently running in this OpenClaw plugin process. " +
33
+ "Pass the mission_id returned by run_mission. The mission runner stops at the next " +
34
+ "step boundary (the in-flight step finishes naturally), marks remaining steps as " +
35
+ "'cancelled', and returns. If the mission has already finished (or the id is unknown), " +
36
+ "this is a no-op that returns found=false. Optional 'reason' is recorded in the " +
37
+ "cancelled step results for traceability.",
38
+ parameters: Type.Object({
39
+ mission_id: Type.String({
40
+ description: "The mission_id echoed by run_mission. Required.",
41
+ }),
42
+ reason: Type.Optional(
43
+ Type.String({
44
+ description: "Optional free-text reason — surfaced in the cancelled mission result.",
45
+ }),
46
+ ),
47
+ }),
48
+
49
+ async execute(_toolCallId, params) {
50
+ const missionId = typeof params["mission_id"] === "string" ? (params["mission_id"] as string).trim() : "";
51
+ if (!missionId) {
52
+ const text = "mission_cancel requires 'mission_id' (a non-empty string returned by run_mission).";
53
+ return { content: [{ type: "text", text }], details: { success: false, error: text } };
54
+ }
55
+ const reason = typeof params["reason"] === "string" ? (params["reason"] as string) : undefined;
56
+ const outcome = getMissionRegistry().cancel(missionId, reason);
57
+ const details = {
58
+ success: true,
59
+ mission_id: missionId,
60
+ found: outcome.found,
61
+ already_cancelled: outcome.alreadyCancelled,
62
+ reason: reason ?? null,
63
+ };
64
+ return {
65
+ content: [{ type: "text", text: JSON.stringify(details) }],
66
+ details,
67
+ };
68
+ },
69
+ });
70
+ }
@@ -2,7 +2,8 @@ import { Type } from "@sinclair/typebox";
2
2
  import type { OpenClawPluginApi } from "../plugin-api.js";
3
3
  import type { AgenticROSConfig } from "@agenticros/core";
4
4
  import { toNamespacedTopic } from "@agenticros/core";
5
- import { getTransport } from "../service.js";
5
+ import { getTransportForRobot } from "../service.js";
6
+ import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
6
7
 
7
8
  /**
8
9
  * Register the ros2_action_goal tool with the AI agent.
@@ -14,22 +15,28 @@ export function registerActionTool(api: OpenClawPluginApi, config: AgenticROSCon
14
15
  label: "ROS2 Action Goal",
15
16
  description:
16
17
  "Send a goal to a ROS2 action server and stream feedback. " +
17
- "Use this for long-running operations like navigation or arm movements.",
18
+ "Use this for long-running operations like navigation or arm movements. " +
19
+ "Pass robot_id (from ros2_list_robots) to target a specific robot.",
18
20
  parameters: Type.Object({
19
21
  action: Type.String({ description: "The ROS2 action server name (e.g., '/navigate_to_pose')" }),
20
22
  actionType: Type.String({ description: "The ROS2 action type (e.g., 'nav2_msgs/action/NavigateToPose')" }),
21
23
  goal: Type.Record(Type.String(), Type.Unknown(), {
22
24
  description: "The action goal parameters",
23
25
  }),
26
+ ...ROBOT_ID_SCHEMA,
24
27
  }),
25
28
 
26
29
  async execute(_toolCallId, params) {
30
+ const resolved = resolveRobotForTool(config, params);
31
+ if ("error" in resolved) return resolved.error;
32
+ const { robot } = resolved;
33
+
27
34
  const rawAction = params["action"] as string;
28
- const action = toNamespacedTopic(config, rawAction);
35
+ const action = toNamespacedTopic(robot.namespace, rawAction);
29
36
  const actionType = params["actionType"] as string;
30
37
  const goal = params["goal"] as Record<string, unknown>;
31
38
 
32
- const transport = getTransport();
39
+ const transport = await getTransportForRobot(config, robot);
33
40
  const actionResult = await transport.sendActionGoal({
34
41
  action,
35
42
  actionType,
@@ -9,11 +9,12 @@ import {
9
9
  mimeTypeForSnapshotBase64,
10
10
  rosNumericField,
11
11
  } from "@agenticros/ros-camera";
12
- import { getTransport } from "../service.js";
12
+ import { getTransportForRobot } from "../service.js";
13
13
  import { normalizePluginToolImageBase64 } from "../plugin-image-base64.js";
14
14
  import { trimJpegToLastEoi } from "../image-binary-trim.js";
15
15
  import { storeCameraSnapshot } from "../camera-snapshot-cache.js";
16
16
  import { describeImageBestEffort } from "../describer.js";
17
+ import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
17
18
 
18
19
  /** Known camera topic patterns for common setups (e.g. RealSense). */
19
20
  export const REALSENSE_CAMERA_TOPICS = {
@@ -64,20 +65,25 @@ export function registerCameraTool(api: OpenClawPluginApi, config: AgenticROSCon
64
65
  ),
65
66
  ),
66
67
  timeout: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 10000)" })),
68
+ ...ROBOT_ID_SCHEMA,
67
69
  }),
68
70
 
69
71
  async execute(_toolCallId, params) {
72
+ const resolved = resolveRobotForTool(config, params);
73
+ if ("error" in resolved) return resolved.error;
74
+ const { robot } = resolved;
75
+
70
76
  const defaultTopic =
71
- (config.robot?.cameraTopic ?? "").trim() || "/camera/camera/color/image_raw/compressed";
77
+ (robot.cameraTopic ?? "").trim() || "/camera/camera/color/image_raw/compressed";
72
78
  const rawTopic = (params["topic"] as string | undefined) ?? defaultTopic;
73
- const topic = resolveCameraSubscribeTopic(config, rawTopic);
79
+ const topic = resolveCameraSubscribeTopic(robot.namespace, rawTopic);
74
80
  const rawMsgType = params["message_type"] as string | undefined;
75
81
  const messageType: "CompressedImage" | "Image" =
76
82
  rawMsgType === "Image" ? "Image" : "CompressedImage";
77
83
  const timeout = (params["timeout"] as number | undefined) ?? 10000;
78
84
 
79
85
  try {
80
- const transport = getTransport();
86
+ const transport = await getTransportForRobot(config, robot);
81
87
  const typeSel = messageType === "Image" ? ROS_MSG_IMAGE : ROS_MSG_COMPRESSED_IMAGE;
82
88
 
83
89
  const result = await new Promise<{
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Tool: ros2_list_capabilities — Phase 1.b of the AgenticROS strategy.
3
+ *
4
+ * Returns the merged list of intrinsic robot verbs + every capability
5
+ * declared by installed AgenticROS skills, as read by
6
+ * @agenticros/core. Prefer this over `ros2_list_topics` for high-level
7
+ * planning: capabilities are agent-meaningful verbs with typed
8
+ * inputs/outputs, not raw topic names.
9
+ *
10
+ * Read-only and transport-free: this tool reads skill manifests from
11
+ * local config + filesystem, so it works even when the robot is
12
+ * offline. Mirrored across all three adapters (OpenClaw, Claude Code,
13
+ * Gemini) — see docs/strategy-ai-agents-plus-ros.md §4.
14
+ */
15
+
16
+ import { Type } from "@sinclair/typebox";
17
+ import type { OpenClawPluginApi } from "../plugin-api.js";
18
+ import type { AgenticROSConfig, Capability } from "@agenticros/core";
19
+ import { listAllCapabilities } from "@agenticros/core";
20
+ import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
21
+
22
+ export function registerCapabilitiesTool(
23
+ api: OpenClawPluginApi,
24
+ config: AgenticROSConfig,
25
+ ): void {
26
+ api.registerTool({
27
+ name: "ros2_list_capabilities",
28
+ label: "ROS2 List Capabilities",
29
+ description:
30
+ "List the high-level capabilities (named verbs) this robot can perform — built-in verbs like " +
31
+ "drive_base / take_snapshot / measure_depth plus every capability declared by installed " +
32
+ "AgenticROS skills (e.g. follow_person, find_object). PREFER this over ros2_list_topics for " +
33
+ "high-level planning: capabilities are agent-meaningful verbs with typed inputs/outputs, " +
34
+ "not raw topic names. Returns one structured response listing every capability the robot " +
35
+ "supports right now. Pass robot_id (from ros2_list_robots) to scope to a specific robot; " +
36
+ "today every robot exposes the same capabilities, but the API is in place for per-robot " +
37
+ "capability declarations.",
38
+ parameters: Type.Object({ ...ROBOT_ID_SCHEMA }),
39
+
40
+ async execute(_toolCallId, params) {
41
+ // robot_id is validated even though the response doesn't depend on
42
+ // it yet — unknown ids surface as a tool error, matching every
43
+ // other tool in the suite.
44
+ const resolved = resolveRobotForTool(config, params);
45
+ if ("error" in resolved) return resolved.error;
46
+
47
+ const caps: Capability[] = listAllCapabilities(config);
48
+ const intrinsic = caps.filter((c) => c.source?.kind === "builtin").length;
49
+ const skill = caps.filter((c) => c.source?.kind === "skill").length;
50
+ const result = {
51
+ success: true,
52
+ total: caps.length,
53
+ intrinsic_count: intrinsic,
54
+ skill_count: skill,
55
+ capabilities: caps,
56
+ };
57
+ return {
58
+ content: [{ type: "text", text: JSON.stringify(result) }],
59
+ details: result,
60
+ };
61
+ },
62
+ });
63
+ }
@@ -7,9 +7,10 @@ import { Type } from "@sinclair/typebox";
7
7
  import type { OpenClawPluginApi } from "../plugin-api.js";
8
8
  import type { AgenticROSConfig } from "@agenticros/core";
9
9
  import { toNamespacedTopic } from "@agenticros/core";
10
- import { getTransport } from "../service.js";
10
+ import { getTransportForRobot } from "../service.js";
11
11
  import { getDepthDistance } from "../depth.js";
12
12
  import { REALSENSE_CAMERA_TOPICS } from "./ros2-camera.js";
13
+ import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
13
14
 
14
15
  const DEFAULT_DEPTH_TOPIC = REALSENSE_CAMERA_TOPICS.depth_raw;
15
16
 
@@ -20,7 +21,8 @@ export function registerDepthDistanceTool(api: OpenClawPluginApi, config: Agenti
20
21
  description:
21
22
  "Get distance in meters from the robot's depth camera (e.g. RealSense). " +
22
23
  "Samples the center of the depth image and returns distance biased toward nearer pixels (~12th percentile), not median (median often tracks walls when the person only covers part of the patch). " +
23
- "Use when the user asks how far they are from the robot, or distance to/from the robot.",
24
+ "Use when the user asks how far they are from the robot, or distance to/from the robot. " +
25
+ "Pass robot_id (from ros2_list_robots) to sample a specific robot's depth camera.",
24
26
 
25
27
  parameters: Type.Object({
26
28
  topic: Type.Optional(
@@ -29,15 +31,20 @@ export function registerDepthDistanceTool(api: OpenClawPluginApi, config: Agenti
29
31
  }),
30
32
  ),
31
33
  timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 5000)" })),
34
+ ...ROBOT_ID_SCHEMA,
32
35
  }),
33
36
 
34
37
  async execute(_toolCallId, params) {
38
+ const resolved = resolveRobotForTool(config, params);
39
+ if ("error" in resolved) return resolved.error;
40
+ const { robot } = resolved;
41
+
35
42
  const rawTopic = (params["topic"] as string | undefined)?.trim() || DEFAULT_DEPTH_TOPIC;
36
- const topic = toNamespacedTopic(config, rawTopic);
43
+ const topic = toNamespacedTopic(robot.namespace, rawTopic);
37
44
  const timeout = (params["timeout"] as number | undefined) ?? 5000;
38
45
 
39
46
  try {
40
- const transport = getTransport();
47
+ const transport = await getTransportForRobot(config, robot);
41
48
  const result = await getDepthDistance(transport, topic, timeout);
42
49
  const text = result.valid
43
50
  ? `Distance at center (~12th percentile, nearer surfaces): **${result.distance_m} m** (median in same patch: ${result.median_m} m; range ${result.min_m}–${result.max_m} m; ${result.sample_count} pixels). Topic: ${result.topic}.`
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Tool: ros2_discover_robots — Phase 1.d "fleet awareness on the wire".
3
+ *
4
+ * Scans the ROS2 topic graph via `transport.listTopics()`, infers robot
5
+ * namespaces from `<ns>/cmd_vel` patterns, and cross-references them
6
+ * against the gateway's configured robot list. Returns four buckets so
7
+ * the agent can answer "which robots are online right now?" without
8
+ * forcing the user to hand-edit config:
9
+ *
10
+ * - detected — every robot inferred from the topic graph
11
+ * - configured_online — configured AND on the wire
12
+ * - configured_offline — configured but the wire is silent
13
+ * - unknown_detected — on the wire but NOT in config (candidates)
14
+ *
15
+ * Read-only — nothing is written to config from this tool. Persistence
16
+ * is a separate UX decision handled by the CLI / OpenClaw config UI.
17
+ *
18
+ * Pure-function detection + classification lives in @agenticros/core
19
+ * (`discoverRobots()`) so all three adapters share the same semantics.
20
+ */
21
+
22
+ import { Type } from "@sinclair/typebox";
23
+ import type { OpenClawPluginApi } from "../plugin-api.js";
24
+ import type { AgenticROSConfig } from "@agenticros/core";
25
+ import { discoverRobots } from "@agenticros/core";
26
+ import { getTransport } from "../service.js";
27
+
28
+ export function registerDiscoverRobotsTool(
29
+ api: OpenClawPluginApi,
30
+ config: AgenticROSConfig,
31
+ ): void {
32
+ api.registerTool({
33
+ name: "ros2_discover_robots",
34
+ label: "ROS2 Discover Robots",
35
+ description:
36
+ "Scan the ROS2 topic graph and report which robots are actually on the wire right now, " +
37
+ "classified against the gateway's configured robot list. Returns: (1) every namespace " +
38
+ "inferred from `<ns>/cmd_vel` topics, with a topicCount that says how many corroborating " +
39
+ "topics live under that namespace, (2) configured_online — configured robots currently " +
40
+ "publishing, (3) configured_offline — configured robots that are silent, (4) " +
41
+ "unknown_detected — robots on the wire that aren't in config yet (candidates to add via " +
42
+ "the CLI / config UI). Use this when the user asks 'which robots are online right now', " +
43
+ "'is my robot connected', or wants to find a robot that isn't in ros2_list_robots. " +
44
+ "Requires the ROS transport to be connected.",
45
+ parameters: Type.Object({}),
46
+
47
+ async execute(_toolCallId, _params) {
48
+ const transport = getTransport();
49
+ const topics = await transport.listTopics();
50
+ const result = discoverRobots(topics, config);
51
+ const payload = {
52
+ success: true,
53
+ total_topics: result.total_topics,
54
+ detected: result.detected,
55
+ configured_online: result.configured_online,
56
+ configured_offline: result.configured_offline,
57
+ unknown_detected: result.unknown_detected,
58
+ };
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify(payload) }],
61
+ details: payload,
62
+ };
63
+ },
64
+ });
65
+ }