agenticros 0.1.18 → 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/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/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 {
|
|
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: {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
|
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(
|
|
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 =
|
|
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 {
|
|
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
|
-
(
|
|
77
|
+
(robot.cameraTopic ?? "").trim() || "/camera/camera/color/image_raw/compressed";
|
|
72
78
|
const rawTopic = (params["topic"] as string | undefined) ?? defaultTopic;
|
|
73
|
-
const topic = resolveCameraSubscribeTopic(
|
|
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 =
|
|
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 {
|
|
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(
|
|
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 =
|
|
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
|
+
}
|