agenticros 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +18 -3
  2. package/dist/util/skill-manifest.d.ts.map +1 -1
  3. package/dist/util/skill-manifest.js +22 -1
  4. package/dist/util/skill-manifest.js.map +1 -1
  5. package/package.json +1 -1
  6. package/runtime/BUNDLE.json +1 -1
  7. package/runtime/README.md +31 -6
  8. package/runtime/docs/cli.md +1 -1
  9. package/runtime/docs/robot-setup.md +1 -1
  10. package/runtime/packages/agenticros/openclaw.plugin.json +147 -1
  11. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +2 -0
  12. package/runtime/packages/agenticros/src/routes.ts +4 -3
  13. package/runtime/packages/agenticros/src/tools/index.ts +6 -5
  14. package/runtime/packages/agenticros/src/tools/mission-pause.ts +49 -0
  15. package/runtime/packages/agenticros/src/tools/mission-resume.ts +41 -0
  16. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +33 -84
  17. package/runtime/packages/agenticros-claude-code/README.md +2 -0
  18. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  19. package/runtime/packages/agenticros-claude-code/dist/tools.js +130 -111
  20. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  21. package/runtime/packages/agenticros-claude-code/src/tools.ts +140 -99
  22. package/runtime/packages/agenticros-gemini/package.json +2 -0
  23. package/runtime/packages/agenticros-gemini/src/find-object/coco-classes.ts +38 -0
  24. package/runtime/packages/agenticros-gemini/src/find-object/find-object.ts +191 -0
  25. package/runtime/packages/agenticros-gemini/src/follow-me/controller.ts +109 -0
  26. package/runtime/packages/agenticros-gemini/src/follow-me/depth-loop.ts +452 -0
  27. package/runtime/packages/agenticros-gemini/src/follow-me/detector.ts +303 -0
  28. package/runtime/packages/agenticros-gemini/src/follow-me/loop.ts +359 -0
  29. package/runtime/packages/agenticros-gemini/src/tools.ts +351 -90
  30. package/runtime/packages/core/README.md +1 -1
  31. package/runtime/packages/core/package.json +1 -1
  32. package/runtime/packages/core/src/__tests__/config-persistence.test.ts +26 -0
  33. package/runtime/packages/core/src/__tests__/external-capability.test.ts +72 -0
  34. package/runtime/packages/core/src/__tests__/heartbeat-fleet.test.ts +152 -0
  35. package/runtime/packages/core/src/__tests__/mission-bindings.test.ts +101 -0
  36. package/runtime/packages/core/src/__tests__/mission-pause.test.ts +104 -0
  37. package/runtime/packages/core/src/capabilities.ts +38 -3
  38. package/runtime/packages/core/src/capability-schema.ts +58 -0
  39. package/runtime/packages/core/src/config.ts +20 -0
  40. package/runtime/packages/core/src/discovery.ts +21 -3
  41. package/runtime/packages/core/src/external-capability.ts +219 -0
  42. package/runtime/packages/core/src/fleet-config.ts +91 -0
  43. package/runtime/packages/core/src/heartbeat.ts +167 -0
  44. package/runtime/packages/core/src/index.ts +55 -0
  45. package/runtime/packages/core/src/mission-bindings.ts +189 -0
  46. package/runtime/packages/core/src/mission-registry.ts +31 -3
  47. package/runtime/packages/core/src/mission.ts +64 -7
  48. package/runtime/packages/core/src/robots.ts +6 -2
  49. package/runtime/packages/core/src/transport/rosbridge/client.ts +7 -0
  50. package/runtime/pnpm-lock.yaml +6 -0
  51. package/templates/skills/camera/package.json +1 -1
  52. package/templates/skills/depth/package.json +1 -1
  53. package/templates/skills/robot/package.json +1 -1
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Capability → tool bindings for `run_mission`.
3
+ *
4
+ * Builtins live here once; adapters import `buildMissionBindings()` instead
5
+ * of triplicating MISSION_BINDINGS. Skill-declared capabilities get a
6
+ * default tool name (`ros2_<id>`, or `capability.tool` when set) and a
7
+ * passthrough `buildArgs` that forwards resolved inputs.
8
+ *
9
+ * External ROS-node capabilities map to the synthetic tool
10
+ * `external:<capability_id>` so adapters can dispatch via
11
+ * `executeExternalCapability` before looking up a real tool.
12
+ */
13
+
14
+ import type { Capability } from "./capabilities.js";
15
+ import type { CapabilityToolBinding, CapabilityToolBindings } from "./mission.js";
16
+
17
+ /** Optional explicit tool name on a capability (skill authors may set this). */
18
+ export type CapabilityWithTool = Capability & { tool?: string };
19
+
20
+ function copyNumber(out: Record<string, unknown>, inputs: Record<string, unknown>, key: string): void {
21
+ if (typeof inputs[key] === "number") out[key] = inputs[key];
22
+ }
23
+
24
+ function copyString(out: Record<string, unknown>, inputs: Record<string, unknown>, key: string): void {
25
+ if (typeof inputs[key] === "string") out[key] = inputs[key];
26
+ }
27
+
28
+ function copyBoolean(out: Record<string, unknown>, inputs: Record<string, unknown>, key: string): void {
29
+ if (typeof inputs[key] === "boolean") out[key] = inputs[key];
30
+ }
31
+
32
+ /** Built-in capability → tool mappings shared by all adapters. */
33
+ export const BUILTIN_MISSION_BINDINGS: CapabilityToolBindings = {
34
+ drive_base: {
35
+ tool: "ros2_publish",
36
+ buildArgs: (inputs) => {
37
+ const lx = Number(inputs.linear_x ?? 0) || 0;
38
+ const az = Number(inputs.angular_z ?? 0) || 0;
39
+ return {
40
+ topic: "/cmd_vel",
41
+ type: "geometry_msgs/msg/Twist",
42
+ message: {
43
+ linear: { x: lx, y: 0, z: 0 },
44
+ angular: { x: 0, y: 0, z: az },
45
+ },
46
+ };
47
+ },
48
+ },
49
+ take_snapshot: {
50
+ tool: "ros2_camera_snapshot",
51
+ buildArgs: (inputs) => {
52
+ const out: Record<string, unknown> = {};
53
+ copyString(out, inputs, "topic");
54
+ copyString(out, inputs, "message_type");
55
+ copyNumber(out, inputs, "timeout");
56
+ return out;
57
+ },
58
+ },
59
+ measure_depth: {
60
+ tool: "ros2_depth_distance",
61
+ buildArgs: (inputs) => {
62
+ const out: Record<string, unknown> = {};
63
+ copyString(out, inputs, "topic");
64
+ copyNumber(out, inputs, "timeout");
65
+ return out;
66
+ },
67
+ },
68
+ list_topics: {
69
+ tool: "ros2_list_topics",
70
+ buildArgs: () => ({}),
71
+ },
72
+ publish_topic: {
73
+ tool: "ros2_publish",
74
+ buildArgs: (inputs) => ({
75
+ topic: String(inputs.topic ?? ""),
76
+ type: String(inputs.type ?? inputs.msg_type ?? ""),
77
+ message: inputs.message ?? inputs.msg ?? {},
78
+ }),
79
+ },
80
+ subscribe_once: {
81
+ tool: "ros2_subscribe_once",
82
+ buildArgs: (inputs) => {
83
+ const out: Record<string, unknown> = { topic: String(inputs.topic ?? "") };
84
+ copyString(out, inputs, "type");
85
+ copyNumber(out, inputs, "timeout");
86
+ return out;
87
+ },
88
+ },
89
+ follow_person: {
90
+ tool: "ros2_follow_me_start",
91
+ buildArgs: (inputs) => {
92
+ const out: Record<string, unknown> = {};
93
+ copyNumber(out, inputs, "target_distance");
94
+ copyString(out, inputs, "mode");
95
+ return out;
96
+ },
97
+ },
98
+ find_object: {
99
+ tool: "ros2_find_object",
100
+ buildArgs: (inputs) => {
101
+ const out: Record<string, unknown> = { target: String(inputs.target ?? "") };
102
+ copyNumber(out, inputs, "angular_speed");
103
+ copyBoolean(out, inputs, "clockwise");
104
+ copyNumber(out, inputs, "timeout_seconds");
105
+ copyNumber(out, inputs, "min_confidence");
106
+ return out;
107
+ },
108
+ },
109
+ };
110
+
111
+ /** Synthetic tool prefix for external_ros_node capabilities. */
112
+ export const EXTERNAL_TOOL_PREFIX = "external:";
113
+
114
+ export function externalToolName(capabilityId: string): string {
115
+ return `${EXTERNAL_TOOL_PREFIX}${capabilityId}`;
116
+ }
117
+
118
+ export function isExternalToolName(toolName: string): boolean {
119
+ return toolName.startsWith(EXTERNAL_TOOL_PREFIX);
120
+ }
121
+
122
+ export function capabilityIdFromExternalTool(toolName: string): string {
123
+ return toolName.slice(EXTERNAL_TOOL_PREFIX.length);
124
+ }
125
+
126
+ /**
127
+ * Default tool name for a skill-declared capability.
128
+ * Prefer explicit `tool`, then external synthetic name, else `ros2_<id>`.
129
+ */
130
+ export function defaultToolForCapability(cap: CapabilityWithTool): string {
131
+ if (typeof cap.tool === "string" && cap.tool.trim().length > 0) {
132
+ return cap.tool.trim();
133
+ }
134
+ if (cap.implementation?.kind === "external_ros_node") {
135
+ return externalToolName(cap.id);
136
+ }
137
+ return `ros2_${cap.id}`;
138
+ }
139
+
140
+ /** Passthrough: forward resolved inputs as tool args (skill tools). */
141
+ export function passthroughBuildArgs(inputs: Record<string, unknown>): Record<string, unknown> {
142
+ return { ...inputs };
143
+ }
144
+
145
+ export interface BuildMissionBindingsOptions {
146
+ /**
147
+ * Override tool name resolution for a capability id.
148
+ * Useful when OpenClaw registers a skill tool under a non-default name.
149
+ */
150
+ toolNameResolver?: (cap: Capability) => string | undefined;
151
+ /**
152
+ * Extra bindings merged last (win over builtins and auto-derived).
153
+ */
154
+ extra?: CapabilityToolBindings;
155
+ }
156
+
157
+ /**
158
+ * Build the full capability → tool map for a mission run.
159
+ *
160
+ * Order: builtins → auto-derived from non-builtin capabilities → extra.
161
+ */
162
+ export function buildMissionBindings(
163
+ capabilities: readonly Capability[],
164
+ options: BuildMissionBindingsOptions = {},
165
+ ): CapabilityToolBindings {
166
+ const out: CapabilityToolBindings = { ...BUILTIN_MISSION_BINDINGS };
167
+
168
+ for (const cap of capabilities) {
169
+ if (cap.source?.kind === "builtin") continue;
170
+ // Skip if already covered by builtins (e.g. follow_person from a skill
171
+ // that duplicates the builtin id — keep the richer builtin binding).
172
+ if (out[cap.id] && BUILTIN_MISSION_BINDINGS[cap.id]) continue;
173
+
174
+ const resolved =
175
+ options.toolNameResolver?.(cap) ?? defaultToolForCapability(cap as CapabilityWithTool);
176
+
177
+ const binding: CapabilityToolBinding = {
178
+ tool: resolved,
179
+ buildArgs: passthroughBuildArgs,
180
+ };
181
+ out[cap.id] = binding;
182
+ }
183
+
184
+ if (options.extra) {
185
+ Object.assign(out, options.extra);
186
+ }
187
+
188
+ return out;
189
+ }
@@ -35,7 +35,7 @@ export interface MissionRegistryEntry {
35
35
  name?: string;
36
36
  /** ms since epoch when the mission was registered. */
37
37
  started_at: number;
38
- /** The token the mission runner reads each step. */
38
+ /** The token the mission runner reads each step (cancel + pause). */
39
39
  cancellation: MissionCancellationToken;
40
40
  }
41
41
 
@@ -45,7 +45,7 @@ export interface MissionRegistryEntry {
45
45
  * Uses `crypto.randomUUID()` when available (Node 14.17+, Bun, modern
46
46
  * browsers); falls back to a Math.random hex string with a `mns_` prefix.
47
47
  * Either way the id is opaque to downstream consumers — they just echo
48
- * it back to `mission_cancel` / `memory_recall`.
48
+ * it back to `mission_cancel` / `mission_pause` / `memory_recall`.
49
49
  */
50
50
  export function generateMissionId(): string {
51
51
  const g = globalThis as { crypto?: { randomUUID?: () => string } };
@@ -86,7 +86,7 @@ export class MissionRegistry {
86
86
  mission_id: missionId,
87
87
  name: opts?.name,
88
88
  started_at: Date.now(),
89
- cancellation: { cancelled: false },
89
+ cancellation: { cancelled: false, paused: false },
90
90
  };
91
91
  this.entries.set(missionId, entry);
92
92
  return {
@@ -112,10 +112,38 @@ export class MissionRegistry {
112
112
  if (!entry) return { found: false, alreadyCancelled: false };
113
113
  const alreadyCancelled = entry.cancellation.cancelled === true;
114
114
  entry.cancellation.cancelled = true;
115
+ entry.cancellation.paused = false;
115
116
  if (reason !== undefined) entry.cancellation.reason = reason;
116
117
  return { found: true, alreadyCancelled };
117
118
  }
118
119
 
120
+ /**
121
+ * Pause the named mission at the next step boundary.
122
+ * Idempotent — pausing an already-paused mission is a no-op.
123
+ */
124
+ pause(missionId: string, reason?: string): { found: boolean; alreadyPaused: boolean } {
125
+ const entry = this.entries.get(missionId);
126
+ if (!entry) return { found: false, alreadyPaused: false };
127
+ if (entry.cancellation.cancelled) {
128
+ return { found: true, alreadyPaused: entry.cancellation.paused === true };
129
+ }
130
+ const alreadyPaused = entry.cancellation.paused === true;
131
+ entry.cancellation.paused = true;
132
+ if (reason !== undefined) entry.cancellation.reason = reason;
133
+ return { found: true, alreadyPaused };
134
+ }
135
+
136
+ /**
137
+ * Resume a paused mission. Idempotent when not paused.
138
+ */
139
+ resume(missionId: string): { found: boolean; wasPaused: boolean } {
140
+ const entry = this.entries.get(missionId);
141
+ if (!entry) return { found: false, wasPaused: false };
142
+ const wasPaused = entry.cancellation.paused === true;
143
+ entry.cancellation.paused = false;
144
+ return { found: true, wasPaused };
145
+ }
146
+
119
147
  /** True when the named mission is currently registered. */
120
148
  has(missionId: string): boolean {
121
149
  return this.entries.has(missionId);
@@ -45,29 +45,40 @@
45
45
  *
46
46
  * What's still deferred:
47
47
  * - Parallel step execution (today: sequential only).
48
- * - Per-tool cancellation (cancel TIES to step boundaries today).
49
- * - Natural-language plan compilation (today: declarative only).
48
+ * - Per-tool cancellation (cancel TIES to step boundaries today;
49
+ * pause/resume also ties to step boundaries).
50
50
  * - Retry / backoff policies.
51
51
  *
52
+ * Shipped since the original Phase 1.f header:
53
+ * - Natural-language plan compilation (`compileGoalToMission` + goal arg).
54
+ * - Pause / resume via the same control token (`paused` flag).
55
+ *
52
56
  * See: docs/strategy-ai-agents-plus-ros.md §4 (Phase 1.c / 1.f).
53
57
  */
54
58
 
55
59
  import type { Capability } from "./capabilities.js";
56
60
 
57
61
  /**
58
- * Cancellation token consumed by `runMission`.
62
+ * Cancellation / pause token consumed by `runMission`.
59
63
  *
60
64
  * Plain object (not AbortController) so it's easy to share across
61
65
  * processes via a registry without pulling Web platform shims in. The
62
- * runner only reads `cancelled`; `reason` is surfaced in the result
63
- * for traceability.
66
+ * runner reads `cancelled` and `paused` at each step boundary.
64
67
  */
65
68
  export interface MissionCancellationToken {
66
69
  cancelled: boolean;
67
- /** Optional free-text reason — bubbled up into the cancelled step results. */
70
+ /**
71
+ * When true, the runner waits at the next step boundary until
72
+ * `paused` is cleared or `cancelled` is set. Phase 1 pause/resume.
73
+ */
74
+ paused?: boolean;
75
+ /** Optional free-text reason — bubbled up into cancelled / paused results. */
68
76
  reason?: string;
69
77
  }
70
78
 
79
+ /** Alias — control token is the same object as the cancellation token. */
80
+ export type MissionControlToken = MissionCancellationToken;
81
+
71
82
  /**
72
83
  * Per-step transcript sink. Called immediately after a step finishes
73
84
  * (including cancelled / skipped steps) so an external store sees the
@@ -154,8 +165,11 @@ export interface MissionStepResult {
154
165
  * - "error": tool returned an error or the binding/build threw.
155
166
  * - "skipped": earlier step failed with on_fail=stop.
156
167
  * - "cancelled": mission was cancelled (Phase 1.f) before this step ran.
168
+ * - "paused": transcript-only marker emitted when the runner entered
169
+ * a pause wait before this step (not a final step outcome
170
+ * in `MissionResult.steps` — those use ok/error/skipped/cancelled).
157
171
  */
158
- status: "ok" | "error" | "skipped" | "cancelled";
172
+ status: "ok" | "error" | "skipped" | "cancelled" | "paused";
159
173
  /** Resolved inputs (with `{{...}}` templates substituted). */
160
174
  inputs: Record<string, unknown>;
161
175
  /** Outputs parsed from the tool response (may be `undefined` for fire-and-forget). */
@@ -253,6 +267,31 @@ export type CapabilityToolBindings = Record<string, CapabilityToolBinding>;
253
267
 
254
268
  const TEMPLATE_RE = /\{\{\s*([a-zA-Z0-9_]+)\.outputs\.([a-zA-Z0-9_]+)\s*\}\}/g;
255
269
 
270
+ function sleep(ms: number): Promise<void> {
271
+ return new Promise((resolve) => setTimeout(resolve, ms));
272
+ }
273
+
274
+ const PAUSE_POLL_MS = 100;
275
+
276
+ /**
277
+ * Wait while `token.paused` is true. Returns when resumed or cancelled.
278
+ * Emits one transcript entry with status "paused" the first time we enter
279
+ * the wait (so a second agent can see the mission is held).
280
+ */
281
+ async function waitWhilePaused(
282
+ token: MissionCancellationToken | undefined,
283
+ emitPaused: () => void,
284
+ ): Promise<"resumed" | "cancelled"> {
285
+ if (!token?.paused || token.cancelled) {
286
+ return token?.cancelled ? "cancelled" : "resumed";
287
+ }
288
+ emitPaused();
289
+ while (token.paused && !token.cancelled) {
290
+ await sleep(PAUSE_POLL_MS);
291
+ }
292
+ return token.cancelled ? "cancelled" : "resumed";
293
+ }
294
+
256
295
  /**
257
296
  * Substitute `{{stepId.outputs.field}}` references in `value` using the
258
297
  * given step output map. Recurses into nested objects/arrays.
@@ -404,6 +443,24 @@ export async function runMission(
404
443
  cancelled = true;
405
444
  }
406
445
 
446
+ // Pause: wait at the step boundary until resume or cancel.
447
+ if (!cancelled && options?.cancellation?.paused) {
448
+ const pauseOutcome = await waitWhilePaused(options.cancellation, () => {
449
+ const pausedMarker: MissionStepResult = {
450
+ id: step.id,
451
+ capability: step.capability,
452
+ status: "paused",
453
+ inputs: {},
454
+ message: `Paused: ${options.cancellation?.reason ?? "paused"}`,
455
+ duration_ms: 0,
456
+ };
457
+ emitTranscript(idx, t0, pausedMarker);
458
+ });
459
+ if (pauseOutcome === "cancelled" || options.cancellation?.cancelled) {
460
+ cancelled = true;
461
+ }
462
+ }
463
+
407
464
  if (cancelled) {
408
465
  const result: MissionStepResult = {
409
466
  id: step.id,
@@ -25,6 +25,7 @@
25
25
  import type { AgenticROSConfig } from "./config.js";
26
26
  import { getTransportConfig } from "./config.js";
27
27
  import type { TransportConfig } from "./transport/types.js";
28
+ import { applyFleetOverride } from "./fleet-config.js";
28
29
 
29
30
  /** Sensor/hardware tags on a robot (Phase 1.e). */
30
31
  export interface RobotSensors {
@@ -85,7 +86,10 @@ const DEFAULT_SENSORS: RobotSensors = {
85
86
  };
86
87
 
87
88
  export function listRobots(config: AgenticROSConfig): ResolvedRobot[] {
88
- const explicit = Array.isArray(config.robots) ? config.robots : [];
89
+ // Phase 1.d ~/.agenticros/fleet.json (or AGENTICROS_FLEET_PATH) wins
90
+ // over config.robots when present and non-empty.
91
+ const effective = applyFleetOverride(config);
92
+ const explicit = Array.isArray(effective.robots) ? effective.robots : [];
89
93
  if (explicit.length > 0) {
90
94
  return explicit.map((r) => ({
91
95
  id: String(r.id),
@@ -107,7 +111,7 @@ export function listRobots(config: AgenticROSConfig): ResolvedRobot[] {
107
111
  // ros2_find_robots_for). Users on multi-robot deployments will have
108
112
  // promoted into config.robots[] anyway, where the fields are
109
113
  // explicit.
110
- const legacy = config.robot ?? { name: "Robot", namespace: "", cameraTopic: "" };
114
+ const legacy = effective.robot ?? { name: "Robot", namespace: "", cameraTopic: "" };
111
115
  const id = (legacy.namespace?.trim() || "default");
112
116
  return [
113
117
  {
@@ -46,6 +46,13 @@ export interface PendingRequest {
46
46
  * fresh CONNECT tunnel.
47
47
  */
48
48
  function buildProxyAgentForUrl(wsUrl: string): HttpAgent | null {
49
+ // When NODE_USE_ENV_PROXY=1 (Node 22+ / NemoClaw sandbox), the runtime's
50
+ // built-in EnvHttpProxyAgent handles ws:// tunneling natively and adds the
51
+ // authentication headers the OpenShell proxy requires. A manual CONNECT
52
+ // request (our TunnelAgent below) omits those headers and gets 403 from
53
+ // the proxy. Skip our agent and let Node handle it.
54
+ if (process.env.NODE_USE_ENV_PROXY === "1") return null;
55
+
49
56
  let parsed: URL;
50
57
  try {
51
58
  parsed = new URL(wsUrl);
@@ -99,6 +99,12 @@ importers:
99
99
  '@google/genai':
100
100
  specifier: ^1.0.0
101
101
  version: 1.46.0(@modelcontextprotocol/sdk@1.27.1(zod@3.25.76))
102
+ onnxruntime-node:
103
+ specifier: ^1.20.0
104
+ version: 1.26.0
105
+ sharp:
106
+ specifier: ^0.33.0
107
+ version: 0.33.5
102
108
  zod:
103
109
  specifier: ^3.24.0
104
110
  version: 3.25.76
@@ -26,7 +26,7 @@
26
26
  ]
27
27
  },
28
28
  "dependencies": {
29
- "@agenticros/core": "^0.5.0",
29
+ "@agenticros/core": "^0.6.0",
30
30
  "@sinclair/typebox": "^0.34.0"
31
31
  },
32
32
  "devDependencies": {
@@ -26,7 +26,7 @@
26
26
  ]
27
27
  },
28
28
  "dependencies": {
29
- "@agenticros/core": "^0.5.0",
29
+ "@agenticros/core": "^0.6.0",
30
30
  "@sinclair/typebox": "^0.34.0"
31
31
  },
32
32
  "devDependencies": {
@@ -28,7 +28,7 @@
28
28
  ]
29
29
  },
30
30
  "dependencies": {
31
- "@agenticros/core": "^0.5.0",
31
+ "@agenticros/core": "^0.6.0",
32
32
  "@sinclair/typebox": "^0.34.0"
33
33
  },
34
34
  "devDependencies": {