agenticros 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/robot-config.test.d.ts +2 -0
- package/dist/__tests__/robot-config.test.d.ts.map +1 -0
- package/dist/__tests__/robot-config.test.js +374 -0
- package/dist/__tests__/robot-config.test.js.map +1 -0
- package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
- package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
- package/dist/__tests__/transport-shorthand.test.js +88 -0
- package/dist/__tests__/transport-shorthand.test.js.map +1 -0
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +55 -3
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/robots.d.ts +57 -0
- package/dist/commands/robots.d.ts.map +1 -0
- package/dist/commands/robots.js +678 -0
- package/dist/commands/robots.js.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/util/mcp-discovery.d.ts +57 -0
- package/dist/util/mcp-discovery.d.ts.map +1 -0
- package/dist/util/mcp-discovery.js +153 -0
- package/dist/util/mcp-discovery.js.map +1 -0
- package/dist/util/robot-config.d.ts +167 -0
- package/dist/util/robot-config.d.ts.map +1 -0
- package/dist/util/robot-config.js +375 -0
- package/dist/util/robot-config.js.map +1 -0
- package/dist/util/transport-shorthand.d.ts +66 -0
- package/dist/util/transport-shorthand.d.ts.map +1 -0
- package/dist/util/transport-shorthand.js +102 -0
- package/dist/util/transport-shorthand.js.map +1 -0
- package/package.json +2 -1
- package/runtime/BUNDLE.json +1 -1
- package/runtime/README.md +4 -0
- package/runtime/package.json +6 -1
- package/runtime/packages/agenticros/openclaw.plugin.json +6 -0
- package/runtime/packages/agenticros/package.json +2 -1
- package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
- package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
- package/runtime/packages/agenticros/src/service.ts +120 -2
- package/runtime/packages/agenticros/src/skill-loader.ts +9 -1
- package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
- package/runtime/packages/agenticros/src/tools/index.ts +49 -9
- package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
- package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
- package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
- package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
- package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
- package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
- package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
- package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
- package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
- package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
- package/runtime/packages/agenticros-claude-code/package.json +2 -1
- package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +964 -0
- package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
- package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
- package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
- package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
- package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
- package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
- package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
- package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
- package/runtime/packages/agenticros-gemini/package.json +1 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
- package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
- package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
- package/runtime/packages/core/package.json +1 -1
- package/runtime/packages/core/src/__tests__/capabilities.test.ts +261 -0
- package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
- package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
- package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
- package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
- package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
- package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
- package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
- package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
- package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
- package/runtime/packages/core/src/capabilities.ts +340 -0
- package/runtime/packages/core/src/config.ts +220 -38
- package/runtime/packages/core/src/discovery.ts +193 -0
- package/runtime/packages/core/src/find-robots-for.ts +164 -0
- package/runtime/packages/core/src/index.ts +67 -1
- package/runtime/packages/core/src/mission-registry.ts +142 -0
- package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
- package/runtime/packages/core/src/mission.ts +574 -0
- package/runtime/packages/core/src/planner/index.ts +564 -0
- package/runtime/packages/core/src/robots.ts +282 -0
- package/runtime/packages/core/src/topic-utils.ts +47 -9
- package/runtime/packages/core/src/transport-pool.ts +171 -0
- package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
- package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
- package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
- package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
- package/runtime/scripts/refresh-skill-deps.mjs +381 -0
- package/runtime/scripts/setup_gateway_plugin.sh +32 -5
- package/runtime/scripts/start_demo.sh +26 -3
- package/runtime/scripts/sync-skill-tools.mjs +67 -10
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability registry — Phase 1.a/1.b of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* A capability is a named, declarable verb an LLM can plan against
|
|
5
|
+
* (`follow_person`, `find_object`, `take_snapshot`, `drive_base`).
|
|
6
|
+
* It's deliberately shaped to match inter-agent protocol agent cards
|
|
7
|
+
* (ACP / A2A) so a skill's capability today is readable as an agent
|
|
8
|
+
* capability when Phase 4 lands — no rewrite required.
|
|
9
|
+
*
|
|
10
|
+
* Two kinds of capabilities:
|
|
11
|
+
* - **Intrinsic** — built into every AgenticROS deployment (drive_base,
|
|
12
|
+
* take_snapshot, measure_depth, list_topics, publish, subscribe). Lives
|
|
13
|
+
* in this file as `BUILTIN_CAPABILITIES`.
|
|
14
|
+
* - **Skill-declared** — comes from a skill package's `package.json`
|
|
15
|
+
* under `agenticrosSkill.capabilities[]` (or a sibling
|
|
16
|
+
* `capabilities.json`). Read at adapter startup.
|
|
17
|
+
*
|
|
18
|
+
* See: docs/strategy-ai-agents-plus-ros.md §4 (Phase 1).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
23
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
24
|
+
import type { AgenticROSConfig } from "./config.js";
|
|
25
|
+
|
|
26
|
+
/** Where a capability came from. */
|
|
27
|
+
export type CapabilitySource =
|
|
28
|
+
| { kind: "builtin" }
|
|
29
|
+
| { kind: "skill"; skillId: string; package: string; path?: string };
|
|
30
|
+
|
|
31
|
+
/** Implementation hint — in-process Node.js skill vs external ROS node. */
|
|
32
|
+
export type CapabilityImplementation =
|
|
33
|
+
| { kind: "in_process" }
|
|
34
|
+
| {
|
|
35
|
+
kind: "external_ros_node";
|
|
36
|
+
package?: string;
|
|
37
|
+
launch?: string;
|
|
38
|
+
action?: string;
|
|
39
|
+
service?: string;
|
|
40
|
+
topic?: string;
|
|
41
|
+
msg_type?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Typed input/output schema — minimal, JSON-Schema-ish. */
|
|
45
|
+
export interface CapabilityField {
|
|
46
|
+
type: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
optional?: boolean;
|
|
49
|
+
default?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One capability the agent can plan against.
|
|
54
|
+
*
|
|
55
|
+
* Required: `id`, `verb`, `description`.
|
|
56
|
+
* Everything else is optional so a skill can adopt the manifest
|
|
57
|
+
* incrementally.
|
|
58
|
+
*/
|
|
59
|
+
export interface Capability {
|
|
60
|
+
/** Unique identifier within a robot (e.g. `follow_person`, `find_object`). */
|
|
61
|
+
id: string;
|
|
62
|
+
/** High-level verb (`follow`, `find`, `navigate`, `manipulate`, `map`, `detect`). */
|
|
63
|
+
verb: string;
|
|
64
|
+
/** One-line, human-readable description. */
|
|
65
|
+
description: string;
|
|
66
|
+
/** Optional structured inputs the agent can fill. */
|
|
67
|
+
inputs?: Record<string, CapabilityField>;
|
|
68
|
+
/** Optional structured outputs the skill emits. */
|
|
69
|
+
outputs?: Record<string, CapabilityField>;
|
|
70
|
+
/** Free-form preconditions ("depthTopic available", "person detected"). */
|
|
71
|
+
preconditions?: string[];
|
|
72
|
+
/** Can be canceled mid-execution? Default true for intrinsic, varies for skills. */
|
|
73
|
+
interruptible?: boolean;
|
|
74
|
+
/** Owns the robot base (cmd_vel); other base-owning skills should yield. */
|
|
75
|
+
blocks_base?: boolean;
|
|
76
|
+
/** How the capability is implemented. Defaults to `in_process` when unset. */
|
|
77
|
+
implementation?: CapabilityImplementation;
|
|
78
|
+
/** Set by the registry, not the skill author. */
|
|
79
|
+
source?: CapabilitySource;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Built-in robot verbs — wrap the raw MCP tools in agent-meaningful
|
|
84
|
+
* capability names. The intrinsic set is intentionally small and stable
|
|
85
|
+
* so the agent's planning surface stays understandable as skill catalogs
|
|
86
|
+
* grow.
|
|
87
|
+
*/
|
|
88
|
+
export const BUILTIN_CAPABILITIES: readonly Capability[] = [
|
|
89
|
+
{
|
|
90
|
+
id: "drive_base",
|
|
91
|
+
verb: "drive",
|
|
92
|
+
description:
|
|
93
|
+
"Drive the robot base by publishing geometry_msgs/Twist to the configured cmd_vel topic. " +
|
|
94
|
+
"Linear/angular velocities are clamped server-side by safety.maxLinearVelocity / maxAngularVelocity.",
|
|
95
|
+
inputs: {
|
|
96
|
+
linear_x: { type: "number", description: "Forward velocity (m/s).", optional: true },
|
|
97
|
+
angular_z: { type: "number", description: "Yaw velocity (rad/s).", optional: true },
|
|
98
|
+
},
|
|
99
|
+
interruptible: true,
|
|
100
|
+
blocks_base: true,
|
|
101
|
+
source: { kind: "builtin" },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "take_snapshot",
|
|
105
|
+
verb: "see",
|
|
106
|
+
description:
|
|
107
|
+
"Capture one frame from the configured camera topic. Returns base64-encoded JPEG/PNG.",
|
|
108
|
+
inputs: {
|
|
109
|
+
topic: { type: "string", description: "Camera topic (defaults to robot.cameraTopic).", optional: true },
|
|
110
|
+
},
|
|
111
|
+
outputs: {
|
|
112
|
+
format: { type: "string", description: "Image format (jpeg / png)." },
|
|
113
|
+
width: { type: "number", description: "Image width in pixels." },
|
|
114
|
+
height: { type: "number", description: "Image height in pixels." },
|
|
115
|
+
},
|
|
116
|
+
interruptible: true,
|
|
117
|
+
source: { kind: "builtin" },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "measure_depth",
|
|
121
|
+
verb: "measure",
|
|
122
|
+
description:
|
|
123
|
+
"Sample the center of a depth image and return a distance in meters. " +
|
|
124
|
+
"Use to answer 'how far am I from X' style questions.",
|
|
125
|
+
inputs: {
|
|
126
|
+
topic: { type: "string", description: "Depth topic.", optional: true },
|
|
127
|
+
},
|
|
128
|
+
outputs: {
|
|
129
|
+
distance_m: { type: "number", description: "Distance in meters at image center." },
|
|
130
|
+
median_m: { type: "number", description: "Median distance across sampled pixels." },
|
|
131
|
+
},
|
|
132
|
+
interruptible: true,
|
|
133
|
+
source: { kind: "builtin" },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "list_topics",
|
|
137
|
+
verb: "introspect",
|
|
138
|
+
description:
|
|
139
|
+
"List all reachable ROS 2 topics with their message types. Use for discovery when " +
|
|
140
|
+
"no other capability fits or to confirm what the robot currently publishes.",
|
|
141
|
+
interruptible: true,
|
|
142
|
+
source: { kind: "builtin" },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "publish_topic",
|
|
146
|
+
verb: "publish",
|
|
147
|
+
description:
|
|
148
|
+
"Generic ROS 2 publish escape hatch. Prefer a named capability when one exists; " +
|
|
149
|
+
"publishes are safety-checked but otherwise unmediated.",
|
|
150
|
+
inputs: {
|
|
151
|
+
topic: { type: "string", description: "ROS 2 topic name." },
|
|
152
|
+
type: { type: "string", description: "ROS 2 message type (e.g. geometry_msgs/msg/Twist)." },
|
|
153
|
+
message: { type: "object", description: "Message payload matching the type schema." },
|
|
154
|
+
},
|
|
155
|
+
interruptible: true,
|
|
156
|
+
source: { kind: "builtin" },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "subscribe_once",
|
|
160
|
+
verb: "read",
|
|
161
|
+
description: "Read one message from a ROS 2 topic. Use for one-shot sensor reads or state checks.",
|
|
162
|
+
inputs: {
|
|
163
|
+
topic: { type: "string", description: "ROS 2 topic name." },
|
|
164
|
+
type: { type: "string", description: "ROS 2 message type (optional, often inferred).", optional: true },
|
|
165
|
+
timeout: { type: "number", description: "Milliseconds to wait (default 5000).", optional: true },
|
|
166
|
+
},
|
|
167
|
+
interruptible: true,
|
|
168
|
+
source: { kind: "builtin" },
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const SKILL_PACKAGE_PREFIX = "agenticros-skill-";
|
|
173
|
+
|
|
174
|
+
function deriveSkillId(packageName: string): string {
|
|
175
|
+
const lower = packageName.toLowerCase();
|
|
176
|
+
if (lower.startsWith(SKILL_PACKAGE_PREFIX)) {
|
|
177
|
+
return lower.slice(SKILL_PACKAGE_PREFIX.length);
|
|
178
|
+
}
|
|
179
|
+
return lower.replace(/^@[^/]+\//, "").replace(/[^a-z0-9]/g, "");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isCapabilityLike(value: unknown): value is Partial<Capability> & { id: string } {
|
|
183
|
+
if (!value || typeof value !== "object") return false;
|
|
184
|
+
const v = value as Record<string, unknown>;
|
|
185
|
+
return typeof v.id === "string" && v.id.length > 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeCapability(
|
|
189
|
+
raw: Partial<Capability> & { id: string },
|
|
190
|
+
source: CapabilitySource,
|
|
191
|
+
): Capability {
|
|
192
|
+
return {
|
|
193
|
+
id: raw.id,
|
|
194
|
+
verb: typeof raw.verb === "string" && raw.verb ? raw.verb : raw.id,
|
|
195
|
+
description: typeof raw.description === "string" ? raw.description : "",
|
|
196
|
+
...(raw.inputs ? { inputs: raw.inputs } : {}),
|
|
197
|
+
...(raw.outputs ? { outputs: raw.outputs } : {}),
|
|
198
|
+
...(raw.preconditions ? { preconditions: raw.preconditions } : {}),
|
|
199
|
+
...(typeof raw.interruptible === "boolean" ? { interruptible: raw.interruptible } : {}),
|
|
200
|
+
...(typeof raw.blocks_base === "boolean" ? { blocks_base: raw.blocks_base } : {}),
|
|
201
|
+
...(raw.implementation ? { implementation: raw.implementation } : { implementation: { kind: "in_process" } }),
|
|
202
|
+
source,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Parse the `agenticrosSkill` field from a package.json + a sibling
|
|
208
|
+
* `capabilities.json` (if present). Returns the raw capability array or `null`
|
|
209
|
+
* if the package isn't an AgenticROS skill.
|
|
210
|
+
*/
|
|
211
|
+
function readSkillManifestFromDir(
|
|
212
|
+
packageDir: string,
|
|
213
|
+
): { packageName: string; rawCaps: Array<Partial<Capability> & { id: string }> } | null {
|
|
214
|
+
const pkgJsonPath = join(packageDir, "package.json");
|
|
215
|
+
if (!existsSync(pkgJsonPath)) return null;
|
|
216
|
+
let pkg: {
|
|
217
|
+
name?: string;
|
|
218
|
+
agenticrosSkill?: boolean | { capabilities?: unknown };
|
|
219
|
+
};
|
|
220
|
+
try {
|
|
221
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (pkg.agenticrosSkill !== true && (typeof pkg.agenticrosSkill !== "object" || pkg.agenticrosSkill === null)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const packageName = pkg.name ?? "unknown";
|
|
229
|
+
|
|
230
|
+
const rawCaps: Array<Partial<Capability> & { id: string }> = [];
|
|
231
|
+
|
|
232
|
+
if (typeof pkg.agenticrosSkill === "object" && pkg.agenticrosSkill !== null) {
|
|
233
|
+
const caps = (pkg.agenticrosSkill as { capabilities?: unknown }).capabilities;
|
|
234
|
+
if (Array.isArray(caps)) {
|
|
235
|
+
for (const c of caps) if (isCapabilityLike(c)) rawCaps.push(c);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const sidecarPath = join(packageDir, "capabilities.json");
|
|
240
|
+
if (existsSync(sidecarPath)) {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = JSON.parse(readFileSync(sidecarPath, "utf-8"));
|
|
243
|
+
const arr = Array.isArray(parsed)
|
|
244
|
+
? parsed
|
|
245
|
+
: Array.isArray((parsed as { capabilities?: unknown }).capabilities)
|
|
246
|
+
? (parsed as { capabilities: unknown[] }).capabilities
|
|
247
|
+
: [];
|
|
248
|
+
for (const c of arr) if (isCapabilityLike(c)) rawCaps.push(c);
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { packageName, rawCaps };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function resolvePackageDir(packageName: string, searchPaths: string[]): string | null {
|
|
257
|
+
const req = createRequire(import.meta.url);
|
|
258
|
+
for (const base of [...searchPaths, process.cwd()]) {
|
|
259
|
+
try {
|
|
260
|
+
const entry = req.resolve(`${packageName}/package.json`, { paths: [base] });
|
|
261
|
+
return entry.replace(/\/package\.json$/, "");
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const entry = req.resolve(`${packageName}/package.json`);
|
|
267
|
+
return entry.replace(/\/package\.json$/, "");
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Read every capability declared by every skill referenced in `config`.
|
|
275
|
+
* Sources both `skillPaths` (directories with a package.json) and
|
|
276
|
+
* `skillPackages` (resolvable npm names).
|
|
277
|
+
*
|
|
278
|
+
* Failures are silent — a missing skill is logged elsewhere by the
|
|
279
|
+
* skill loader; this function focuses on returning the capabilities
|
|
280
|
+
* it can read.
|
|
281
|
+
*/
|
|
282
|
+
export function readSkillCapabilities(config: AgenticROSConfig): Capability[] {
|
|
283
|
+
const out: Capability[] = [];
|
|
284
|
+
const seen = new Set<string>();
|
|
285
|
+
|
|
286
|
+
const skillPaths = config.skillPaths ?? [];
|
|
287
|
+
const skillPackages = config.skillPackages ?? [];
|
|
288
|
+
|
|
289
|
+
for (const dir of skillPaths) {
|
|
290
|
+
const absDir = resolvePath(dir);
|
|
291
|
+
const manifest = readSkillManifestFromDir(absDir);
|
|
292
|
+
if (!manifest) continue;
|
|
293
|
+
const skillId = deriveSkillId(manifest.packageName);
|
|
294
|
+
for (const raw of manifest.rawCaps) {
|
|
295
|
+
const key = `${skillId}:${raw.id}`;
|
|
296
|
+
if (seen.has(key)) continue;
|
|
297
|
+
seen.add(key);
|
|
298
|
+
out.push(
|
|
299
|
+
normalizeCapability(raw, {
|
|
300
|
+
kind: "skill",
|
|
301
|
+
skillId,
|
|
302
|
+
package: manifest.packageName,
|
|
303
|
+
path: absDir,
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const pkgName of skillPackages) {
|
|
310
|
+
const packageDir = resolvePackageDir(pkgName, skillPaths.map((p) => resolvePath(p)));
|
|
311
|
+
if (!packageDir) continue;
|
|
312
|
+
const manifest = readSkillManifestFromDir(packageDir);
|
|
313
|
+
if (!manifest) continue;
|
|
314
|
+
const skillId = deriveSkillId(manifest.packageName);
|
|
315
|
+
for (const raw of manifest.rawCaps) {
|
|
316
|
+
const key = `${skillId}:${raw.id}`;
|
|
317
|
+
if (seen.has(key)) continue;
|
|
318
|
+
seen.add(key);
|
|
319
|
+
out.push(
|
|
320
|
+
normalizeCapability(raw, {
|
|
321
|
+
kind: "skill",
|
|
322
|
+
skillId,
|
|
323
|
+
package: manifest.packageName,
|
|
324
|
+
path: packageDir,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Return the full capability list: built-in robot verbs first, then
|
|
335
|
+
* skill-declared capabilities. This is the shape returned by
|
|
336
|
+
* `ros2_list_capabilities` across every adapter.
|
|
337
|
+
*/
|
|
338
|
+
export function listAllCapabilities(config: AgenticROSConfig): Capability[] {
|
|
339
|
+
return [...BUILTIN_CAPABILITIES, ...readSkillCapabilities(config)];
|
|
340
|
+
}
|
|
@@ -7,6 +7,130 @@ const IceServerSchema = z.object({
|
|
|
7
7
|
credential: z.string().optional(),
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Transport sub-schemas. Extracted so both the top-level config and the
|
|
12
|
+
// per-robot override in `robots[i].transport` can share them without
|
|
13
|
+
// drifting. The shapes mirror packages/core/src/transport/types.ts.
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const ZenohSettingsSchema = z.object({
|
|
17
|
+
/** WebSocket URL for zenoh-ts (zenoh-plugin-remote-api). Not tcp/ — use e.g. ws://localhost:10000 */
|
|
18
|
+
routerEndpoint: z.string().default("ws://localhost:10000"),
|
|
19
|
+
domainId: z.number().default(0),
|
|
20
|
+
/** "ros2dds" = zenoh-bridge-ros2dds key format (slashes kept). "rmw_zenoh" = rmw_zenoh key format (domain + %). */
|
|
21
|
+
keyFormat: z.enum(["ros2dds", "rmw_zenoh"]).default("ros2dds"),
|
|
22
|
+
/**
|
|
23
|
+
* Must match zenoh-bridge-ros2dds `plugins.ros2dds.namespace` when it is not the default "/".
|
|
24
|
+
* Example: bridge has `namespace: "/bot1"` → set `bridgeNamespace` to "/bot1" or "bot1".
|
|
25
|
+
* Omit or use "/" when the bridge uses the default (only the ROS topic path is the zenoh key).
|
|
26
|
+
*/
|
|
27
|
+
bridgeNamespace: z.string().optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const RosbridgeSettingsSchema = z.object({
|
|
31
|
+
url: z.string().default("ws://localhost:9090"),
|
|
32
|
+
reconnect: z.boolean().default(true),
|
|
33
|
+
reconnectInterval: z.number().default(3000),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const LocalSettingsSchema = z.object({
|
|
37
|
+
domainId: z.number().default(0),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const WebrtcSettingsSchema = z.object({
|
|
41
|
+
signalingUrl: z.string().default(""),
|
|
42
|
+
apiUrl: z.string().default(""),
|
|
43
|
+
robotId: z.string().default(""),
|
|
44
|
+
robotKey: z.string().default(""),
|
|
45
|
+
iceServers: z
|
|
46
|
+
.array(IceServerSchema)
|
|
47
|
+
.default([{ urls: "stun:stun.l.google.com:19302" }]),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Per-robot transport override sub-schemas (Phase 1.d-resolve).
|
|
52
|
+
*
|
|
53
|
+
* These intentionally MIRROR the top-level shapes but drop every
|
|
54
|
+
* `.default(...)`. Why: when `getTransportConfigForRobot` merges an
|
|
55
|
+
* override over the global config it does a per-field merge
|
|
56
|
+
* (`{ ...global, ...override }`). If the override schema applied
|
|
57
|
+
* defaults (e.g. `domainId: 0`), those defaults would silently clobber
|
|
58
|
+
* the global values whenever the user omitted them — making the
|
|
59
|
+
* "merge only the fields I actually wrote" contract impossible.
|
|
60
|
+
*
|
|
61
|
+
* Every field is optional here. Validation still rejects wrong types
|
|
62
|
+
* (e.g. `routerEndpoint: 42`), which is what we want.
|
|
63
|
+
*/
|
|
64
|
+
const ZenohOverrideSettingsSchema = z.object({
|
|
65
|
+
routerEndpoint: z.string().optional(),
|
|
66
|
+
domainId: z.number().optional(),
|
|
67
|
+
keyFormat: z.enum(["ros2dds", "rmw_zenoh"]).optional(),
|
|
68
|
+
bridgeNamespace: z.string().optional(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const RosbridgeOverrideSettingsSchema = z.object({
|
|
72
|
+
url: z.string().optional(),
|
|
73
|
+
reconnect: z.boolean().optional(),
|
|
74
|
+
reconnectInterval: z.number().optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const LocalOverrideSettingsSchema = z.object({
|
|
78
|
+
domainId: z.number().optional(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const WebrtcOverrideSettingsSchema = z.object({
|
|
82
|
+
signalingUrl: z.string().optional(),
|
|
83
|
+
apiUrl: z.string().optional(),
|
|
84
|
+
robotId: z.string().optional(),
|
|
85
|
+
robotKey: z.string().optional(),
|
|
86
|
+
iceServers: z.array(IceServerSchema).optional(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Per-robot transport override schema (Phase 1.d-resolve).
|
|
91
|
+
*
|
|
92
|
+
* Discriminated union on `mode`, each variant `.strict()` so that a
|
|
93
|
+
* typo like `{ mode: "zenoh", local: {...} }` fails fast at parse time
|
|
94
|
+
* instead of being silently stripped. Sub-sections are optional —
|
|
95
|
+
* `{ mode: "zenoh" }` is a valid override that inherits everything
|
|
96
|
+
* from the top-level `config.zenoh`. A partial sub-section like
|
|
97
|
+
* `{ mode: "zenoh", zenoh: { routerEndpoint: "ws://field:10000" } }`
|
|
98
|
+
* is merged per-field over the global config — see the resolver.
|
|
99
|
+
*
|
|
100
|
+
* Why a discriminated union (vs the top-level shape of sibling fields):
|
|
101
|
+
* - Keeps the override self-contained at one JSON property
|
|
102
|
+
* - Matches TransportConfig's runtime discriminated union 1:1
|
|
103
|
+
* - Trivially validatable per-robot without leaking across siblings
|
|
104
|
+
*/
|
|
105
|
+
const RobotTransportOverrideSchema = z.discriminatedUnion("mode", [
|
|
106
|
+
z
|
|
107
|
+
.object({
|
|
108
|
+
mode: z.literal("rosbridge"),
|
|
109
|
+
rosbridge: RosbridgeOverrideSettingsSchema.optional(),
|
|
110
|
+
})
|
|
111
|
+
.strict(),
|
|
112
|
+
z
|
|
113
|
+
.object({
|
|
114
|
+
mode: z.literal("local"),
|
|
115
|
+
local: LocalOverrideSettingsSchema.optional(),
|
|
116
|
+
})
|
|
117
|
+
.strict(),
|
|
118
|
+
z
|
|
119
|
+
.object({
|
|
120
|
+
mode: z.literal("zenoh"),
|
|
121
|
+
zenoh: ZenohOverrideSettingsSchema.optional(),
|
|
122
|
+
})
|
|
123
|
+
.strict(),
|
|
124
|
+
z
|
|
125
|
+
.object({
|
|
126
|
+
mode: z.literal("webrtc"),
|
|
127
|
+
webrtc: WebrtcOverrideSettingsSchema.optional(),
|
|
128
|
+
})
|
|
129
|
+
.strict(),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
export type RobotTransportOverride = z.infer<typeof RobotTransportOverrideSchema>;
|
|
133
|
+
|
|
10
134
|
export const AgenticROSConfigSchema = z.object({
|
|
11
135
|
transport: z
|
|
12
136
|
.object({
|
|
@@ -19,47 +143,13 @@ export const AgenticROSConfigSchema = z.object({
|
|
|
19
143
|
})
|
|
20
144
|
.default({}),
|
|
21
145
|
|
|
22
|
-
zenoh:
|
|
23
|
-
.object({
|
|
24
|
-
/** WebSocket URL for zenoh-ts (zenoh-plugin-remote-api). Not tcp/ — use e.g. ws://localhost:10000 */
|
|
25
|
-
routerEndpoint: z.string().default("ws://localhost:10000"),
|
|
26
|
-
domainId: z.number().default(0),
|
|
27
|
-
/** "ros2dds" = zenoh-bridge-ros2dds key format (slashes kept). "rmw_zenoh" = rmw_zenoh key format (domain + %). */
|
|
28
|
-
keyFormat: z.enum(["ros2dds", "rmw_zenoh"]).default("ros2dds"),
|
|
29
|
-
/**
|
|
30
|
-
* Must match zenoh-bridge-ros2dds `plugins.ros2dds.namespace` when it is not the default "/".
|
|
31
|
-
* Example: bridge has `namespace: "/bot1"` → set `bridgeNamespace` to "/bot1" or "bot1".
|
|
32
|
-
* Omit or use "/" when the bridge uses the default (only the ROS topic path is the zenoh key).
|
|
33
|
-
*/
|
|
34
|
-
bridgeNamespace: z.string().optional(),
|
|
35
|
-
})
|
|
36
|
-
.default({}),
|
|
146
|
+
zenoh: ZenohSettingsSchema.default({}),
|
|
37
147
|
|
|
38
|
-
rosbridge:
|
|
39
|
-
.object({
|
|
40
|
-
url: z.string().default("ws://localhost:9090"),
|
|
41
|
-
reconnect: z.boolean().default(true),
|
|
42
|
-
reconnectInterval: z.number().default(3000),
|
|
43
|
-
})
|
|
44
|
-
.default({}),
|
|
148
|
+
rosbridge: RosbridgeSettingsSchema.default({}),
|
|
45
149
|
|
|
46
|
-
local:
|
|
47
|
-
.object({
|
|
48
|
-
domainId: z.number().default(0),
|
|
49
|
-
})
|
|
50
|
-
.default({}),
|
|
150
|
+
local: LocalSettingsSchema.default({}),
|
|
51
151
|
|
|
52
|
-
webrtc:
|
|
53
|
-
.object({
|
|
54
|
-
signalingUrl: z.string().default(""),
|
|
55
|
-
apiUrl: z.string().default(""),
|
|
56
|
-
robotId: z.string().default(""),
|
|
57
|
-
robotKey: z.string().default(""),
|
|
58
|
-
iceServers: z
|
|
59
|
-
.array(IceServerSchema)
|
|
60
|
-
.default([{ urls: "stun:stun.l.google.com:19302" }]),
|
|
61
|
-
})
|
|
62
|
-
.default({}),
|
|
152
|
+
webrtc: WebrtcSettingsSchema.default({}),
|
|
63
153
|
|
|
64
154
|
robot: z
|
|
65
155
|
.object({
|
|
@@ -70,6 +160,98 @@ export const AgenticROSConfigSchema = z.object({
|
|
|
70
160
|
})
|
|
71
161
|
.default({}),
|
|
72
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Phase 1.d multi-robot list. When non-empty, this is the source of
|
|
165
|
+
* truth for `ros2_list_robots` and (in a follow-up) the per-tool
|
|
166
|
+
* `robot_id` parameter. When empty, the legacy single-robot
|
|
167
|
+
* `config.robot` above is synthesised into a one-entry list at
|
|
168
|
+
* resolution time (see packages/core/src/robots.ts). Existing configs
|
|
169
|
+
* keep working unchanged; this field is opt-in.
|
|
170
|
+
*
|
|
171
|
+
* Mark one entry with `default: true` to make it the active robot
|
|
172
|
+
* even when it isn't the first in the list. Otherwise the first entry
|
|
173
|
+
* wins.
|
|
174
|
+
*/
|
|
175
|
+
robots: z
|
|
176
|
+
.array(
|
|
177
|
+
z.object({
|
|
178
|
+
/** Stable, human-readable identifier referenced by `robot_id` arguments. */
|
|
179
|
+
id: z.string(),
|
|
180
|
+
name: z.string().default("Robot"),
|
|
181
|
+
namespace: z.string().default(""),
|
|
182
|
+
cameraTopic: z.string().default(""),
|
|
183
|
+
default: z.boolean().optional(),
|
|
184
|
+
/**
|
|
185
|
+
* Phase 1.e robot kind ("amr" | "arm" | "drone" | "rover" | …).
|
|
186
|
+
*
|
|
187
|
+
* Free-form string so users can invent their own taxonomy, but the
|
|
188
|
+
* documented set is `amr` (autonomous mobile robot), `arm`,
|
|
189
|
+
* `drone`, and `rover`. Consumed by `ros2_find_robots_for(kind=…)`
|
|
190
|
+
* so an agent can ask "find me an AMR with a depth camera that
|
|
191
|
+
* can follow_person" and the platform filters the fleet.
|
|
192
|
+
*
|
|
193
|
+
* Defaults to "amr" (back-compat: every existing robot today is
|
|
194
|
+
* an AMR), so the field stays useful even when the user hasn't
|
|
195
|
+
* tagged their robots explicitly.
|
|
196
|
+
*/
|
|
197
|
+
kind: z.string().default("amr"),
|
|
198
|
+
/**
|
|
199
|
+
* Phase 1.e sensor/hardware tags.
|
|
200
|
+
*
|
|
201
|
+
* Lets `ros2_find_robots_for` answer questions like "which
|
|
202
|
+
* robots have a depth camera" without subscribing to topics or
|
|
203
|
+
* inspecting the URDF. All flags default to false — set them
|
|
204
|
+
* explicitly per robot via the CLI (`--sensors=has_realsense`)
|
|
205
|
+
* or by hand-editing `~/.agenticros/config.json`.
|
|
206
|
+
*
|
|
207
|
+
* The list is intentionally short for Phase 1; expand it as we
|
|
208
|
+
* accumulate seed-catalog skills that depend on a specific
|
|
209
|
+
* hardware capability (lidar SLAM, arm grasping, etc.).
|
|
210
|
+
*/
|
|
211
|
+
sensors: z
|
|
212
|
+
.object({
|
|
213
|
+
has_realsense: z.boolean().default(false),
|
|
214
|
+
has_lidar: z.boolean().default(false),
|
|
215
|
+
has_arm: z.boolean().default(false),
|
|
216
|
+
})
|
|
217
|
+
.default({}),
|
|
218
|
+
/**
|
|
219
|
+
* Phase 1.e optional per-robot capability allowlist.
|
|
220
|
+
*
|
|
221
|
+
* When set, restricts which capabilities `ros2_find_robots_for`
|
|
222
|
+
* considers this robot capable of. When unset (the common
|
|
223
|
+
* case), the robot inherits the gateway's global capability
|
|
224
|
+
* registry (the union of built-in verbs plus every loaded
|
|
225
|
+
* skill's capabilities). Useful in heterogeneous fleets where
|
|
226
|
+
* one robot has the arm skill loaded and another doesn't.
|
|
227
|
+
*/
|
|
228
|
+
capabilities: z.array(z.string()).optional(),
|
|
229
|
+
/**
|
|
230
|
+
* Phase 1.d-resolve per-robot transport override.
|
|
231
|
+
*
|
|
232
|
+
* When set, this robot uses its own transport instead of the
|
|
233
|
+
* top-level `config.transport.*`. Useful when one host drives
|
|
234
|
+
* BOTH a local sim (mode: "local") AND a real robot reached via
|
|
235
|
+
* an off-robot bridge (mode: "zenoh" / "rosbridge") — each
|
|
236
|
+
* robot connects through the right path with no global config
|
|
237
|
+
* juggling.
|
|
238
|
+
*
|
|
239
|
+
* Sub-sections (`zenoh`, `rosbridge`, `local`, `webrtc`) are
|
|
240
|
+
* optional inside the override: omit them to inherit the global
|
|
241
|
+
* `config.<section>` defaults; set them to override only the
|
|
242
|
+
* fields you need (e.g. a different router endpoint).
|
|
243
|
+
*
|
|
244
|
+
* Consumed by `getTransportConfigForRobot(config, robotId)` in
|
|
245
|
+
* robots.ts. Adapters opt in to multi-transport pools — when
|
|
246
|
+
* the override is absent they keep using the single global
|
|
247
|
+
* transport, so existing single-robot deployments are
|
|
248
|
+
* unaffected.
|
|
249
|
+
*/
|
|
250
|
+
transport: RobotTransportOverrideSchema.optional(),
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
.default([]),
|
|
254
|
+
|
|
73
255
|
/** Phase 3 teleop web app: camera + twist controls. */
|
|
74
256
|
teleop: z
|
|
75
257
|
.object({
|