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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: ros2_find_robots_for — Phase 1.e of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* Capability-aware fleet filter: given a (capability, kind, online)
|
|
5
|
+
* query, returns the configured robots that match, ranked best-first.
|
|
6
|
+
* This is the tool that lets an LLM ask "which robot can find a chair"
|
|
7
|
+
* or "is there an AMR online that can follow a person" and get a
|
|
8
|
+
* structured answer it can plan against — without scanning topic lists.
|
|
9
|
+
*
|
|
10
|
+
* Config-only by default (no transport touched). When the caller sets
|
|
11
|
+
* `online: true|false`, we list topics via the global transport and
|
|
12
|
+
* cross-reference against `<ns>/cmd_vel` patterns to compute the live
|
|
13
|
+
* set — same heuristic as ros2_discover_robots, sharing
|
|
14
|
+
* `discoverRobots()` from @agenticros/core. The transport is only
|
|
15
|
+
* acquired in that branch; offline-mode planning still works.
|
|
16
|
+
*
|
|
17
|
+
* Mirrored across all three adapters (Claude Code, OpenClaw, Gemini)
|
|
18
|
+
* with identical request/response shapes — see
|
|
19
|
+
* `packages/agenticros-claude-code/src/tools.ts` for the canonical one.
|
|
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, findRobotsFor } from "@agenticros/core";
|
|
26
|
+
import { getTransport } from "../service.js";
|
|
27
|
+
|
|
28
|
+
export function registerFindRobotsForTool(
|
|
29
|
+
api: OpenClawPluginApi,
|
|
30
|
+
config: AgenticROSConfig,
|
|
31
|
+
): void {
|
|
32
|
+
api.registerTool({
|
|
33
|
+
name: "ros2_find_robots_for",
|
|
34
|
+
label: "ROS2 Find Robots For",
|
|
35
|
+
description:
|
|
36
|
+
"Find the robots in the configured fleet that match a capability + kind + online filter, " +
|
|
37
|
+
"ranked best-first. PREFER this over ros2_list_robots whenever the user names a verb " +
|
|
38
|
+
"('which robot can find a chair', 'do I have an arm robot that can grasp', 'is there an " +
|
|
39
|
+
"AMR online that can follow a person'). Capability matches the verbs from " +
|
|
40
|
+
"ros2_list_capabilities — by default robots inherit the gateway-wide registry; declaring " +
|
|
41
|
+
"per-robot capabilities in config narrows it. Kind matches robot.kind exactly ('amr' | " +
|
|
42
|
+
"'arm' | 'drone' | 'rover'). When online=true, only currently-reachable robots are " +
|
|
43
|
+
"returned (uses the same `<ns>/cmd_vel` heuristic as ros2_discover_robots and requires " +
|
|
44
|
+
"the transport). The result lists matched robots with id/name/namespace/kind/sensors/" +
|
|
45
|
+
"online flag, ranked so explicit capability declarations + online robots come first.",
|
|
46
|
+
parameters: Type.Object({
|
|
47
|
+
capability: Type.Optional(
|
|
48
|
+
Type.String({
|
|
49
|
+
description:
|
|
50
|
+
"Capability id to match (e.g. 'follow_person', 'find_object', 'drive_base'). " +
|
|
51
|
+
"Case-sensitive — use ros2_list_capabilities to get the exact list.",
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
kind: Type.Optional(
|
|
55
|
+
Type.String({
|
|
56
|
+
description:
|
|
57
|
+
"Robot kind filter (case-insensitive exact match). Common values: 'amr', 'arm', " +
|
|
58
|
+
"'drone', 'rover'.",
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
online: Type.Optional(
|
|
62
|
+
Type.Boolean({
|
|
63
|
+
description:
|
|
64
|
+
"When true, only return robots currently reachable on the ROS2 graph (requires the " +
|
|
65
|
+
"transport to be connected). When false, only return robots NOT reachable. When " +
|
|
66
|
+
"omitted, online status is annotated on every match but doesn't filter the list.",
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
async execute(_toolCallId, params) {
|
|
72
|
+
const p = (params ?? {}) as { capability?: string; kind?: string; online?: boolean };
|
|
73
|
+
let onlineIds: Set<string> | undefined;
|
|
74
|
+
if (p.online !== undefined) {
|
|
75
|
+
const transport = getTransport();
|
|
76
|
+
if (transport.getStatus() !== "connected") {
|
|
77
|
+
const fail = {
|
|
78
|
+
success: false,
|
|
79
|
+
error:
|
|
80
|
+
"online filter requires the ROS transport to be connected. Drop the 'online' " +
|
|
81
|
+
"arg to run config-only, or check that zenohd / rosbridge is up.",
|
|
82
|
+
};
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(fail) }], details: fail };
|
|
84
|
+
}
|
|
85
|
+
const topics = await transport.listTopics();
|
|
86
|
+
const disc = discoverRobots(topics, config);
|
|
87
|
+
onlineIds = new Set(disc.configured_online.map((r) => r.id));
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const result = findRobotsFor(
|
|
91
|
+
config,
|
|
92
|
+
{ capability: p.capability, kind: p.kind, online: p.online },
|
|
93
|
+
onlineIds,
|
|
94
|
+
);
|
|
95
|
+
// Flatten the FindRobotsForMatch shape into one object per
|
|
96
|
+
// robot so the LLM doesn't have to dig through .robot.* —
|
|
97
|
+
// identical shape to the claude-code adapter's formatter.
|
|
98
|
+
const payload = {
|
|
99
|
+
success: true,
|
|
100
|
+
query: result.query,
|
|
101
|
+
total: result.total,
|
|
102
|
+
robots: result.robots.map((m) => ({
|
|
103
|
+
id: m.robot.id,
|
|
104
|
+
name: m.robot.name,
|
|
105
|
+
namespace: m.robot.namespace,
|
|
106
|
+
kind: m.robot.kind,
|
|
107
|
+
sensors: m.robot.sensors,
|
|
108
|
+
capabilities: m.robot.capabilities ?? null,
|
|
109
|
+
cameraTopic: m.robot.cameraTopic,
|
|
110
|
+
online: m.online,
|
|
111
|
+
matched_capability_explicitly: m.matched_capability_explicitly,
|
|
112
|
+
score: m.score,
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
117
|
+
details: payload,
|
|
118
|
+
};
|
|
119
|
+
} catch (e) {
|
|
120
|
+
const fail = {
|
|
121
|
+
success: false,
|
|
122
|
+
error: e instanceof Error ? e.message : String(e),
|
|
123
|
+
};
|
|
124
|
+
return { content: [{ type: "text", text: JSON.stringify(fail) }], details: fail };
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: run_mission — Phase 1.c of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* Executes a multi-step mission by chaining capabilities. Each step
|
|
5
|
+
* names a capability (from `ros2_list_capabilities`); the runner maps
|
|
6
|
+
* the capability to a previously-registered OpenClaw tool, calls that
|
|
7
|
+
* tool's execute() with the step's resolved inputs, and feeds the
|
|
8
|
+
* structured output into later steps via `{{stepId.outputs.field}}`
|
|
9
|
+
* template references.
|
|
10
|
+
*
|
|
11
|
+
* The transport-agnostic runner itself lives in @agenticros/core; this
|
|
12
|
+
* file just provides the OpenClaw-flavoured tool wrapper plus the
|
|
13
|
+
* capability → tool-name binding map (which mirrors the one in
|
|
14
|
+
* packages/agenticros-claude-code/src/tools.ts so all adapters agree on
|
|
15
|
+
* what `drive_base`, `find_object`, etc. actually do).
|
|
16
|
+
*
|
|
17
|
+
* Mirrored across all three adapters — see
|
|
18
|
+
* docs/strategy-ai-agents-plus-ros.md §4 Phase 1.c.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Type } from "@sinclair/typebox";
|
|
22
|
+
import type { AgentTool, OpenClawPluginApi } from "../plugin-api.js";
|
|
23
|
+
import type {
|
|
24
|
+
AgenticROSConfig,
|
|
25
|
+
CapabilityToolBindings,
|
|
26
|
+
Mission,
|
|
27
|
+
MissionToolDispatcher,
|
|
28
|
+
} from "@agenticros/core";
|
|
29
|
+
import {
|
|
30
|
+
listAllCapabilities,
|
|
31
|
+
runMission,
|
|
32
|
+
generateMissionId,
|
|
33
|
+
createMemoryTranscriptSink,
|
|
34
|
+
missionTranscriptNamespace,
|
|
35
|
+
compileGoalToMission,
|
|
36
|
+
} from "@agenticros/core";
|
|
37
|
+
import { resolveRobotForTool } from "./_robot-helpers.js";
|
|
38
|
+
import { getMissionRegistry } from "../mission-registry.js";
|
|
39
|
+
import { getMemory } from "../memory.js";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Snapshot of the registered tools, keyed by tool name. Built in
|
|
43
|
+
* `tools/index.ts` as each tool is registered, then passed in so this
|
|
44
|
+
* tool can dispatch sub-tool calls by name.
|
|
45
|
+
*/
|
|
46
|
+
export type ToolRegistry = Map<string, AgentTool>;
|
|
47
|
+
|
|
48
|
+
const MISSION_BINDINGS: CapabilityToolBindings = {
|
|
49
|
+
drive_base: {
|
|
50
|
+
tool: "ros2_publish",
|
|
51
|
+
buildArgs: (inputs) => {
|
|
52
|
+
const lx = Number(inputs.linear_x ?? 0) || 0;
|
|
53
|
+
const az = Number(inputs.angular_z ?? 0) || 0;
|
|
54
|
+
return {
|
|
55
|
+
topic: "/cmd_vel",
|
|
56
|
+
type: "geometry_msgs/msg/Twist",
|
|
57
|
+
message: {
|
|
58
|
+
linear: { x: lx, y: 0, z: 0 },
|
|
59
|
+
angular: { x: 0, y: 0, z: az },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
take_snapshot: {
|
|
65
|
+
tool: "ros2_camera_snapshot",
|
|
66
|
+
buildArgs: (inputs) => {
|
|
67
|
+
const out: Record<string, unknown> = {};
|
|
68
|
+
if (typeof inputs.topic === "string") out.topic = inputs.topic;
|
|
69
|
+
if (typeof inputs.message_type === "string") out.message_type = inputs.message_type;
|
|
70
|
+
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
71
|
+
return out;
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
measure_depth: {
|
|
75
|
+
tool: "ros2_depth_distance",
|
|
76
|
+
buildArgs: (inputs) => {
|
|
77
|
+
const out: Record<string, unknown> = {};
|
|
78
|
+
if (typeof inputs.topic === "string") out.topic = inputs.topic;
|
|
79
|
+
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
80
|
+
return out;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
list_topics: {
|
|
84
|
+
tool: "ros2_list_topics",
|
|
85
|
+
buildArgs: () => ({}),
|
|
86
|
+
},
|
|
87
|
+
publish_topic: {
|
|
88
|
+
tool: "ros2_publish",
|
|
89
|
+
buildArgs: (inputs) => ({
|
|
90
|
+
topic: String(inputs.topic ?? ""),
|
|
91
|
+
type: String(inputs.type ?? inputs.msg_type ?? ""),
|
|
92
|
+
message: inputs.message ?? inputs.msg ?? {},
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
subscribe_once: {
|
|
96
|
+
tool: "ros2_subscribe_once",
|
|
97
|
+
buildArgs: (inputs) => {
|
|
98
|
+
const out: Record<string, unknown> = { topic: String(inputs.topic ?? "") };
|
|
99
|
+
if (typeof inputs.type === "string") out.type = inputs.type;
|
|
100
|
+
if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
|
|
101
|
+
return out;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
follow_person: {
|
|
105
|
+
tool: "ros2_follow_me_start",
|
|
106
|
+
buildArgs: (inputs) => {
|
|
107
|
+
const out: Record<string, unknown> = {};
|
|
108
|
+
if (typeof inputs.target_distance === "number") out.target_distance = inputs.target_distance;
|
|
109
|
+
if (typeof inputs.mode === "string") out.mode = inputs.mode;
|
|
110
|
+
return out;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
find_object: {
|
|
114
|
+
tool: "ros2_find_object",
|
|
115
|
+
buildArgs: (inputs) => {
|
|
116
|
+
const target = String(inputs.target ?? "");
|
|
117
|
+
const out: Record<string, unknown> = { target };
|
|
118
|
+
if (typeof inputs.angular_speed === "number") out.angular_speed = inputs.angular_speed;
|
|
119
|
+
if (typeof inputs.clockwise === "boolean") out.clockwise = inputs.clockwise;
|
|
120
|
+
if (typeof inputs.timeout_seconds === "number") out.timeout_seconds = inputs.timeout_seconds;
|
|
121
|
+
if (typeof inputs.min_confidence === "number") out.min_confidence = inputs.min_confidence;
|
|
122
|
+
return out;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export function registerMissionTool(
|
|
128
|
+
api: OpenClawPluginApi,
|
|
129
|
+
config: AgenticROSConfig,
|
|
130
|
+
registry: ToolRegistry,
|
|
131
|
+
): void {
|
|
132
|
+
api.registerTool({
|
|
133
|
+
name: "run_mission",
|
|
134
|
+
label: "ROS2 Run Mission",
|
|
135
|
+
description:
|
|
136
|
+
"Execute a multi-step mission by chaining capabilities (the verbs returned by ros2_list_capabilities). " +
|
|
137
|
+
"PASS EITHER a natural-language `goal` (recommended for simple verbs like 'find a chair', " +
|
|
138
|
+
"'take a picture', 'follow me', 'find a chair and drive toward it') OR an explicit `mission.steps[]` " +
|
|
139
|
+
"plan when you need precise control. Steps run sequentially; each step's outputs are available to " +
|
|
140
|
+
"later steps via {{stepId.outputs.fieldName}} template references. " +
|
|
141
|
+
"Default on_fail behaviour is 'stop' (abort on first error). Returns a per-step result list, a " +
|
|
142
|
+
"summary line, a mission_id you can pass to mission_cancel to abort mid-run, and (when a goal was " +
|
|
143
|
+
"provided) the compiled plan + candidate match list so you can see what the planner did. " +
|
|
144
|
+
"When memory is enabled, every step is also written to the shared memory under namespace " +
|
|
145
|
+
"mission:<mission_id> so a second agent can recall the timeline via memory_recall. " +
|
|
146
|
+
"Today the runner supports: drive_base, take_snapshot, measure_depth, list_topics, " +
|
|
147
|
+
"publish_topic, subscribe_once, follow_person, find_object. " +
|
|
148
|
+
"Pass mission.robot_id (or top-level robot_id with goal) to target every step at one robot.",
|
|
149
|
+
parameters: Type.Object({
|
|
150
|
+
goal: Type.Optional(
|
|
151
|
+
Type.String({
|
|
152
|
+
description:
|
|
153
|
+
"Natural-language goal — the local planner compiles it into a mission against the capability registry. " +
|
|
154
|
+
"Examples: 'find a chair', 'find a chair and drive toward it', 'take a picture', 'follow me', " +
|
|
155
|
+
"'measure depth', 'drive forward at 0.3 m/s', 'turn left', 'stop'. " +
|
|
156
|
+
"Either goal or mission must be provided; mission wins if both are set.",
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
robot_id: Type.Optional(
|
|
160
|
+
Type.String({
|
|
161
|
+
description:
|
|
162
|
+
"Optional robot id (from ros2_list_robots) — used when 'goal' is provided to scope every compiled step to one robot.",
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
mission: Type.Optional(
|
|
166
|
+
Type.Object({
|
|
167
|
+
name: Type.Optional(Type.String()),
|
|
168
|
+
goal: Type.Optional(Type.String()),
|
|
169
|
+
robot_id: Type.Optional(
|
|
170
|
+
Type.String({
|
|
171
|
+
description:
|
|
172
|
+
"Default robot id (from ros2_list_robots) injected into every step's tool args. Per-step inputs.robot_id overrides this for individual steps.",
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
steps: Type.Array(
|
|
176
|
+
Type.Object({
|
|
177
|
+
id: Type.String(),
|
|
178
|
+
capability: Type.String(),
|
|
179
|
+
inputs: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
180
|
+
on_fail: Type.Optional(Type.Union([Type.Literal("stop"), Type.Literal("continue")])),
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
async execute(toolCallId, params, signal) {
|
|
188
|
+
const caps = listAllCapabilities(config);
|
|
189
|
+
const missionRaw = params["mission"];
|
|
190
|
+
const goalRaw = params["goal"];
|
|
191
|
+
const topLevelRobotId = typeof params["robot_id"] === "string" ? (params["robot_id"] as string) : undefined;
|
|
192
|
+
|
|
193
|
+
// Phase 1.g — accept either an explicit mission OR a
|
|
194
|
+
// natural-language goal. The planner is deterministic + rule-based,
|
|
195
|
+
// so an agent can preview the compile via the same call. We surface
|
|
196
|
+
// candidates/suggestions in details so a failed compile is actionable.
|
|
197
|
+
let mission: Mission;
|
|
198
|
+
let plannerInfo:
|
|
199
|
+
| { compiled_from_goal: string; candidates: unknown[]; unmatched_verbs?: string[] }
|
|
200
|
+
| undefined;
|
|
201
|
+
if (missionRaw && typeof missionRaw === "object" && !Array.isArray(missionRaw)) {
|
|
202
|
+
mission = missionRaw as Mission;
|
|
203
|
+
if (!Array.isArray(mission.steps)) {
|
|
204
|
+
const text = "mission.steps must be an array of step objects.";
|
|
205
|
+
return { content: [{ type: "text", text }], details: { success: false, error: text } };
|
|
206
|
+
}
|
|
207
|
+
} else if (typeof goalRaw === "string" && goalRaw.trim().length > 0) {
|
|
208
|
+
const planned = compileGoalToMission(goalRaw, caps, { robot_id: topLevelRobotId });
|
|
209
|
+
if (!planned.mission) {
|
|
210
|
+
const details = {
|
|
211
|
+
success: false,
|
|
212
|
+
error: planned.error,
|
|
213
|
+
goal: goalRaw,
|
|
214
|
+
suggestions: planned.suggestions,
|
|
215
|
+
...(planned.unmatched_verbs ? { unmatched_verbs: planned.unmatched_verbs } : {}),
|
|
216
|
+
};
|
|
217
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
218
|
+
}
|
|
219
|
+
mission = planned.mission;
|
|
220
|
+
plannerInfo = {
|
|
221
|
+
compiled_from_goal: goalRaw,
|
|
222
|
+
candidates: planned.candidates,
|
|
223
|
+
...(planned.unmatched_verbs ? { unmatched_verbs: planned.unmatched_verbs } : {}),
|
|
224
|
+
};
|
|
225
|
+
} else {
|
|
226
|
+
const text =
|
|
227
|
+
'run_mission requires either "mission" (object with steps[]) or "goal" (natural-language string). Pass at least one.';
|
|
228
|
+
return { content: [{ type: "text", text }], details: { success: false, error: text } };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Validate mission.robot_id up-front so the agent gets a single
|
|
232
|
+
// clean error (with known ids) instead of one per step.
|
|
233
|
+
if (typeof mission.robot_id === "string" && mission.robot_id.trim().length > 0) {
|
|
234
|
+
const resolved = resolveRobotForTool(config, { robot_id: mission.robot_id });
|
|
235
|
+
if ("error" in resolved) return resolved.error;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const dispatcher: MissionToolDispatcher = async (toolName, toolArgs) => {
|
|
239
|
+
const tool = registry.get(toolName);
|
|
240
|
+
if (!tool) {
|
|
241
|
+
return {
|
|
242
|
+
text: `Tool "${toolName}" is not registered with this OpenClaw plugin instance.`,
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const res = await tool.execute(toolCallId, toolArgs, signal);
|
|
247
|
+
const text = res.content
|
|
248
|
+
.map((c) => (c.type === "text" ? c.text : `[image: ${c.mimeType}]`))
|
|
249
|
+
.join("\n");
|
|
250
|
+
// Tool results don't carry an isError flag in the OpenClaw shape;
|
|
251
|
+
// surface details if it's an object so the runner can pick it up
|
|
252
|
+
// as structured outputs.
|
|
253
|
+
const outputs =
|
|
254
|
+
res.details && typeof res.details === "object" && !Array.isArray(res.details)
|
|
255
|
+
? (res.details as Record<string, unknown>)
|
|
256
|
+
: undefined;
|
|
257
|
+
return { text, outputs };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Phase 1.f — generate + register a mission_id, wire transcripts.
|
|
261
|
+
// mission_cancel uses the registry to find this entry mid-run.
|
|
262
|
+
// The transcript sink is only attached when memory is enabled
|
|
263
|
+
// (initMemory() populated getMemory()); otherwise it stays a
|
|
264
|
+
// no-op so opt-in deployments aren't affected.
|
|
265
|
+
const missionId = generateMissionId();
|
|
266
|
+
const missionRegistry = getMissionRegistry();
|
|
267
|
+
const { entry: regEntry, dispose: disposeRegistry } = missionRegistry.register(
|
|
268
|
+
missionId,
|
|
269
|
+
{ name: mission.name },
|
|
270
|
+
);
|
|
271
|
+
const memory = getMemory();
|
|
272
|
+
const transcript = memory ? createMemoryTranscriptSink(memory, missionId) : undefined;
|
|
273
|
+
|
|
274
|
+
let result;
|
|
275
|
+
try {
|
|
276
|
+
result = await runMission(mission, caps, MISSION_BINDINGS, dispatcher, {
|
|
277
|
+
mission_id: missionId,
|
|
278
|
+
cancellation: regEntry.cancellation,
|
|
279
|
+
transcript,
|
|
280
|
+
adapter: "openclaw",
|
|
281
|
+
});
|
|
282
|
+
} finally {
|
|
283
|
+
disposeRegistry();
|
|
284
|
+
}
|
|
285
|
+
const compact = {
|
|
286
|
+
status: result.status,
|
|
287
|
+
mission_id: result.mission_id,
|
|
288
|
+
...(result.cancellation_reason ? { cancellation_reason: result.cancellation_reason } : {}),
|
|
289
|
+
...(transcript ? { transcript_namespace: missionTranscriptNamespace(missionId) } : {}),
|
|
290
|
+
...(plannerInfo ? { planner: plannerInfo } : {}),
|
|
291
|
+
steps_run: result.steps_run,
|
|
292
|
+
steps_total: result.steps_total,
|
|
293
|
+
duration_ms: result.duration_ms,
|
|
294
|
+
summary: result.summary,
|
|
295
|
+
steps: result.steps.map((s) => ({
|
|
296
|
+
id: s.id,
|
|
297
|
+
capability: s.capability,
|
|
298
|
+
status: s.status,
|
|
299
|
+
inputs: s.inputs,
|
|
300
|
+
outputs: s.outputs,
|
|
301
|
+
...(s.error ? { error: s.error } : {}),
|
|
302
|
+
duration_ms: s.duration_ms,
|
|
303
|
+
})),
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: `${result.summary}\n${JSON.stringify(compact)}` }],
|
|
307
|
+
details: compact,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
@@ -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 ros2_param_get and ros2_param_set tools with the AI agent.
|
|
@@ -13,18 +14,24 @@ export function registerParamTools(api: OpenClawPluginApi, config: AgenticROSCon
|
|
|
13
14
|
label: "ROS2 Get Parameter",
|
|
14
15
|
description:
|
|
15
16
|
"Get the value of a ROS2 parameter from a node. " +
|
|
16
|
-
"Use this to check robot configuration values."
|
|
17
|
+
"Use this to check robot configuration values. " +
|
|
18
|
+
"Pass robot_id (from ros2_list_robots) to target a specific robot.",
|
|
17
19
|
parameters: Type.Object({
|
|
18
20
|
node: Type.String({ description: "The fully qualified node name (e.g., '/turtlebot3/controller')" }),
|
|
19
21
|
parameter: Type.String({ description: "The parameter name (e.g., 'max_velocity')" }),
|
|
22
|
+
...ROBOT_ID_SCHEMA,
|
|
20
23
|
}),
|
|
21
24
|
|
|
22
25
|
async execute(_toolCallId, params) {
|
|
26
|
+
const resolved = resolveRobotForTool(config, params);
|
|
27
|
+
if ("error" in resolved) return resolved.error;
|
|
28
|
+
const { robot } = resolved;
|
|
29
|
+
|
|
23
30
|
const rawNode = params["node"] as string;
|
|
24
|
-
const node = toNamespacedTopic(
|
|
31
|
+
const node = toNamespacedTopic(robot.namespace, rawNode);
|
|
25
32
|
const parameter = params["parameter"] as string;
|
|
26
33
|
|
|
27
|
-
const transport =
|
|
34
|
+
const transport = await getTransportForRobot(config, robot);
|
|
28
35
|
const response = await transport.callService({
|
|
29
36
|
service: `${node}/get_parameters`,
|
|
30
37
|
type: "rcl_interfaces/srv/GetParameters",
|
|
@@ -49,20 +56,26 @@ export function registerParamTools(api: OpenClawPluginApi, config: AgenticROSCon
|
|
|
49
56
|
label: "ROS2 Set Parameter",
|
|
50
57
|
description:
|
|
51
58
|
"Set the value of a ROS2 parameter on a node. " +
|
|
52
|
-
"Use this to change robot configuration at runtime."
|
|
59
|
+
"Use this to change robot configuration at runtime. " +
|
|
60
|
+
"Pass robot_id (from ros2_list_robots) to target a specific robot.",
|
|
53
61
|
parameters: Type.Object({
|
|
54
62
|
node: Type.String({ description: "The fully qualified node name" }),
|
|
55
63
|
parameter: Type.String({ description: "The parameter name" }),
|
|
56
64
|
value: Type.Unknown({ description: "The new parameter value" }),
|
|
65
|
+
...ROBOT_ID_SCHEMA,
|
|
57
66
|
}),
|
|
58
67
|
|
|
59
68
|
async execute(_toolCallId, params) {
|
|
69
|
+
const resolved = resolveRobotForTool(config, params);
|
|
70
|
+
if ("error" in resolved) return resolved.error;
|
|
71
|
+
const { robot } = resolved;
|
|
72
|
+
|
|
60
73
|
const rawNode = params["node"] as string;
|
|
61
|
-
const node = toNamespacedTopic(
|
|
74
|
+
const node = toNamespacedTopic(robot.namespace, rawNode);
|
|
62
75
|
const parameter = params["parameter"] as string;
|
|
63
76
|
const value = params["value"];
|
|
64
77
|
|
|
65
|
-
const transport =
|
|
78
|
+
const transport = await getTransportForRobot(config, robot);
|
|
66
79
|
const response = await transport.callService({
|
|
67
80
|
service: `${node}/set_parameters`,
|
|
68
81
|
type: "rcl_interfaces/srv/SetParameters",
|
|
@@ -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, applyCmdVelTwistSignConvention } 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_publish tool with the AI agent.
|
|
@@ -14,37 +15,43 @@ export function registerPublishTool(api: OpenClawPluginApi, config: AgenticROSCo
|
|
|
14
15
|
label: "ROS2 Publish",
|
|
15
16
|
description:
|
|
16
17
|
"Publish a message to a ROS2 topic. Use this to send commands to the robot " +
|
|
17
|
-
"(e.g., velocity commands to /cmd_vel, navigation goals, etc.)."
|
|
18
|
+
"(e.g., velocity commands to /cmd_vel, navigation goals, etc.). " +
|
|
19
|
+
"Pass robot_id (from ros2_list_robots) to target a specific robot; omitted = active robot.",
|
|
18
20
|
parameters: Type.Object({
|
|
19
21
|
topic: Type.String({ description: "The ROS2 topic name (e.g., '/cmd_vel')" }),
|
|
20
22
|
type: Type.String({ description: "The ROS2 message type (e.g., 'geometry_msgs/msg/Twist')" }),
|
|
21
23
|
message: Type.Record(Type.String(), Type.Unknown(), {
|
|
22
24
|
description: "The message payload matching the ROS2 message type schema",
|
|
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 rawTopic = params["topic"] as string;
|
|
28
|
-
const topic = toNamespacedTopic(
|
|
35
|
+
const topic = toNamespacedTopic(robot.namespace, rawTopic);
|
|
29
36
|
const type = params["type"] as string;
|
|
30
37
|
let message = params["message"] as Record<string, unknown>;
|
|
31
38
|
message = applyCmdVelTwistSignConvention(topic, type, message);
|
|
32
39
|
|
|
33
|
-
const transport =
|
|
40
|
+
const transport = await getTransportForRobot(config, robot);
|
|
34
41
|
transport.publish({ topic, type, msg: message });
|
|
35
42
|
|
|
36
|
-
const ns = (
|
|
43
|
+
const ns = (robot.namespace ?? "").trim();
|
|
37
44
|
const namespaceApplied = ns && topic.startsWith(`/${ns}/`);
|
|
38
45
|
let summary = namespaceApplied
|
|
39
|
-
? `Published to ${topic} (robot.namespace="${ns}" applied).`
|
|
46
|
+
? `Published to ${topic} (robot="${robot.id}", namespace="${ns}" applied).`
|
|
40
47
|
: `Published to ${topic}.`;
|
|
41
48
|
if (!ns && (rawTopic === "/cmd_vel" || rawTopic.trim().replace(/^\/+/, "") === "cmd_vel")) {
|
|
42
49
|
summary += " If the robot did not move, set robot.namespace in plugin config to the robot's cmd_vel prefix (e.g. robot3946b404c33e4aa39a8d16deb1c5c593), then restart the gateway.";
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
const result = { success: true, topic, type, summary };
|
|
52
|
+
const result = { success: true, topic, type, robot_id: robot.id, summary };
|
|
46
53
|
return {
|
|
47
|
-
content: [{ type: "text", text: summary + "\n" + JSON.stringify({ success: true, topic, type }) }],
|
|
54
|
+
content: [{ type: "text", text: summary + "\n" + JSON.stringify({ success: true, topic, type, robot_id: robot.id }) }],
|
|
48
55
|
details: result,
|
|
49
56
|
};
|
|
50
57
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: ros2_list_robots — Phase 1.d of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* Returns the list of robots this gateway knows about (id, name, ROS2
|
|
5
|
+
* namespace, default camera topic) and which one is active. The agent
|
|
6
|
+
* uses this to discover the fleet and (in a follow-up iteration) target
|
|
7
|
+
* specific robots via a `robot_id` parameter on other tools.
|
|
8
|
+
*
|
|
9
|
+
* Read-only and transport-free: this tool reads the multi-robot section
|
|
10
|
+
* of the config (with legacy single-robot fallback) so it works even
|
|
11
|
+
* when the robot is offline. Mirrored across all three adapters
|
|
12
|
+
* (OpenClaw, Claude Code, Gemini) — see
|
|
13
|
+
* docs/strategy-ai-agents-plus-ros.md §4 Phase 1.d.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Type } from "@sinclair/typebox";
|
|
17
|
+
import type { OpenClawPluginApi } from "../plugin-api.js";
|
|
18
|
+
import type { AgenticROSConfig } from "@agenticros/core";
|
|
19
|
+
import { getActiveRobotId, listRobots } from "@agenticros/core";
|
|
20
|
+
|
|
21
|
+
export function registerRobotsTool(
|
|
22
|
+
api: OpenClawPluginApi,
|
|
23
|
+
config: AgenticROSConfig,
|
|
24
|
+
): void {
|
|
25
|
+
api.registerTool({
|
|
26
|
+
name: "ros2_list_robots",
|
|
27
|
+
label: "ROS2 List Robots",
|
|
28
|
+
description:
|
|
29
|
+
"List the robots this gateway knows about (id, name, ROS2 namespace, default camera topic) " +
|
|
30
|
+
"and which one is the active default. Use this FIRST when the user mentions multiple robots, " +
|
|
31
|
+
"asks 'which robots can you see?', or names a specific robot you haven't heard of. The " +
|
|
32
|
+
"returned `id` is what later tools (in upcoming iterations) will accept as a `robot_id` " +
|
|
33
|
+
"parameter — today there's a single active robot, but the field will scope per-tool calls " +
|
|
34
|
+
"in fleet deployments.",
|
|
35
|
+
parameters: Type.Object({}),
|
|
36
|
+
|
|
37
|
+
async execute(_toolCallId, _params) {
|
|
38
|
+
const robots = listRobots(config);
|
|
39
|
+
const active = getActiveRobotId(config);
|
|
40
|
+
const result = {
|
|
41
|
+
success: true,
|
|
42
|
+
total: robots.length,
|
|
43
|
+
active_robot_id: active,
|
|
44
|
+
robots,
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
48
|
+
details: result,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -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_service_call tool with the AI agent.
|
|
@@ -14,22 +15,28 @@ export function registerServiceTool(api: OpenClawPluginApi, config: AgenticROSCo
|
|
|
14
15
|
label: "ROS2 Service Call",
|
|
15
16
|
description:
|
|
16
17
|
"Call a ROS2 service and return the response. Use this for request/response operations " +
|
|
17
|
-
"like setting parameters, triggering behaviors, or querying node state."
|
|
18
|
+
"like setting parameters, triggering behaviors, or querying node state. " +
|
|
19
|
+
"Pass robot_id (from ros2_list_robots) to target a specific robot.",
|
|
18
20
|
parameters: Type.Object({
|
|
19
21
|
service: Type.String({ description: "The ROS2 service name (e.g., '/spawn_entity')" }),
|
|
20
22
|
type: Type.Optional(Type.String({ description: "The ROS2 service type (e.g., 'gazebo_msgs/srv/SpawnEntity')" })),
|
|
21
23
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
22
24
|
description: "The service request arguments",
|
|
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 rawService = params["service"] as string;
|
|
28
|
-
const service = toNamespacedTopic(
|
|
35
|
+
const service = toNamespacedTopic(robot.namespace, rawService);
|
|
29
36
|
const type = params["type"] as string | undefined;
|
|
30
37
|
const args = params["args"] as Record<string, unknown> | undefined;
|
|
31
38
|
|
|
32
|
-
const transport =
|
|
39
|
+
const transport = await getTransportForRobot(config, robot);
|
|
33
40
|
const response = await transport.callService({ service, type, args });
|
|
34
41
|
|
|
35
42
|
const result = {
|