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,191 @@
1
+ /**
2
+ * Rotate the robot in place until a target COCO class is detected in the
3
+ * camera feed, then stop. Used by the ros2_find_object MCP tool.
4
+ */
5
+
6
+ import type { AgenticROSConfig, ResolvedRobot, RosTransport } from "@agenticros/core";
7
+ import { resolveCameraSubscribeTopic, toNamespacedTopic } from "@agenticros/core";
8
+ import {
9
+ ROS_MSG_COMPRESSED_IMAGE,
10
+ cameraSnapshotFromPlainMessage,
11
+ } from "@agenticros/ros-camera";
12
+ import { PersonDetector } from "../follow-me/detector.js";
13
+ import { resolveCocoClassId, COCO_CLASSES } from "./coco-classes.js";
14
+
15
+ const DEFAULT_COLOR_TOPIC = "/camera/camera/color/image_raw/compressed";
16
+ const DEFAULT_ANGULAR_SPEED = 0.3; // rad/s
17
+ const DEFAULT_TIMEOUT_SEC = 30;
18
+ const DEFAULT_MIN_CONFIDENCE = 0.5;
19
+ const POLL_INTERVAL_MS = 500;
20
+ const SNAPSHOT_TIMEOUT_MS = 3000;
21
+
22
+ export interface FindObjectOptions {
23
+ target: string;
24
+ angularSpeed?: number;
25
+ timeoutSeconds?: number;
26
+ minConfidence?: number;
27
+ clockwise?: boolean;
28
+ }
29
+
30
+ export interface FindObjectResult {
31
+ found: boolean;
32
+ target: string;
33
+ classId: number;
34
+ elapsedSeconds: number;
35
+ rotationDirection: "clockwise" | "counterclockwise";
36
+ angularSpeed: number;
37
+ detection?: {
38
+ confidence: number;
39
+ cx: number;
40
+ cy: number;
41
+ width: number;
42
+ height: number;
43
+ imageWidth: number;
44
+ imageHeight: number;
45
+ horizontalOffset: number;
46
+ };
47
+ error?: string;
48
+ }
49
+
50
+ export async function findObject(
51
+ robot: ResolvedRobot,
52
+ config: AgenticROSConfig,
53
+ transport: RosTransport,
54
+ opts: FindObjectOptions,
55
+ ): Promise<FindObjectResult> {
56
+ const classId = resolveCocoClassId(opts.target);
57
+ if (classId === null) {
58
+ return {
59
+ found: false,
60
+ target: opts.target,
61
+ classId: -1,
62
+ elapsedSeconds: 0,
63
+ rotationDirection: "clockwise",
64
+ angularSpeed: 0,
65
+ error:
66
+ `Unknown target "${opts.target}". Must be a COCO class name (e.g., "cell phone", "chair", "bottle"). ` +
67
+ `Supported: ${COCO_CLASSES.join(", ")}.`,
68
+ };
69
+ }
70
+
71
+ const safety = config.safety ?? {};
72
+ const maxAngular = safety.maxAngularVelocity ?? 1.5;
73
+ const requestedSpeed = Math.max(0.05, Math.min(maxAngular, opts.angularSpeed ?? DEFAULT_ANGULAR_SPEED));
74
+ const clockwise = opts.clockwise ?? true;
75
+ const angularZ = clockwise ? -requestedSpeed : requestedSpeed;
76
+ const timeoutMs = Math.max(1000, (opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SEC) * 1000);
77
+ const minConfidence = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
78
+
79
+ const detector = new PersonDetector({ scoreThreshold: minConfidence });
80
+ await detector.load();
81
+
82
+ const cmdVelTopic = resolveCmdVelTopic(config, robot);
83
+ const colorTopic = resolveCameraSubscribeTopic(
84
+ robot.namespace,
85
+ robot.cameraTopic.trim() || DEFAULT_COLOR_TOPIC,
86
+ );
87
+
88
+ const startedAt = Date.now();
89
+ let rotating = false;
90
+ let result: FindObjectResult["detection"] | undefined;
91
+
92
+ const publishTwist = async (linearX: number, angZ: number) => {
93
+ try {
94
+ await transport.publish({
95
+ topic: cmdVelTopic,
96
+ type: "geometry_msgs/msg/Twist",
97
+ msg: { linear: { x: linearX, y: 0, z: 0 }, angular: { x: 0, y: 0, z: angZ } },
98
+ });
99
+ } catch {
100
+ // best-effort; loop will retry
101
+ }
102
+ };
103
+
104
+ try {
105
+ await publishTwist(0, angularZ);
106
+ rotating = true;
107
+
108
+ const deadline = startedAt + timeoutMs;
109
+ while (Date.now() < deadline && !result) {
110
+ // Keep the rotation alive in case the robot times out cmd_vel commands.
111
+ await publishTwist(0, angularZ);
112
+
113
+ const frame = await snapshotOnce(transport, colorTopic).catch(() => null);
114
+ if (frame) {
115
+ const det = await detector.detectClass(frame.buffer, classId);
116
+ if (det.detections.length > 0) {
117
+ const best = det.detections.reduce((a, b) => (a.confidence > b.confidence ? a : b));
118
+ result = {
119
+ confidence: best.confidence,
120
+ cx: best.cx,
121
+ cy: best.cy,
122
+ width: best.width,
123
+ height: best.height,
124
+ imageWidth: det.width,
125
+ imageHeight: det.height,
126
+ horizontalOffset: (best.cx - det.width / 2) / (det.width / 2),
127
+ };
128
+ break;
129
+ }
130
+ }
131
+
132
+ const remaining = deadline - Date.now();
133
+ if (remaining <= 0) break;
134
+ await sleep(Math.min(POLL_INTERVAL_MS, remaining));
135
+ }
136
+ } finally {
137
+ if (rotating) await publishTwist(0, 0);
138
+ await detector.dispose().catch(() => {});
139
+ }
140
+
141
+ const elapsedSeconds = (Date.now() - startedAt) / 1000;
142
+ return {
143
+ found: !!result,
144
+ target: opts.target,
145
+ classId,
146
+ elapsedSeconds,
147
+ rotationDirection: clockwise ? "clockwise" : "counterclockwise",
148
+ angularSpeed: requestedSpeed,
149
+ detection: result,
150
+ };
151
+ }
152
+
153
+ async function snapshotOnce(
154
+ transport: RosTransport,
155
+ topic: string,
156
+ ): Promise<{ buffer: Buffer } | null> {
157
+ return new Promise((resolve) => {
158
+ const sub = transport.subscribe(
159
+ { topic, type: ROS_MSG_COMPRESSED_IMAGE },
160
+ (msg: Record<string, unknown>) => {
161
+ clearTimeout(timer);
162
+ sub.unsubscribe();
163
+ try {
164
+ const payload = cameraSnapshotFromPlainMessage("CompressedImage", msg);
165
+ resolve({ buffer: Buffer.from(payload.dataBase64, "base64") });
166
+ } catch {
167
+ resolve(null);
168
+ }
169
+ },
170
+ );
171
+ const timer = setTimeout(() => {
172
+ sub.unsubscribe();
173
+ resolve(null);
174
+ }, SNAPSHOT_TIMEOUT_MS);
175
+ });
176
+ }
177
+
178
+ function sleep(ms: number): Promise<void> {
179
+ return new Promise((resolve) => setTimeout(resolve, ms));
180
+ }
181
+
182
+ function resolveCmdVelTopic(config: AgenticROSConfig, robot: ResolvedRobot): string {
183
+ const raw = (config.teleop?.cmdVelTopic ?? "").trim() || "/cmd_vel";
184
+ const namespaced = toNamespacedTopic(robot.namespace, raw);
185
+ const match = namespaced.match(/^\/([^/]+)\/cmd_vel$/i);
186
+ const segment = match?.[1] ?? "";
187
+ if (match && !segment.toLowerCase().startsWith("robot")) {
188
+ return `/robot${segment.replace(/-/g, "")}/cmd_vel`;
189
+ }
190
+ return namespaced;
191
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Follower P-controller ported from agenticros_follow_me/follower_controller.py.
3
+ *
4
+ * Given a target person's 3D position (x = lateral metres, z = depth metres),
5
+ * compute a smoothed, deadzoned, clamped Twist command.
6
+ */
7
+
8
+ export interface ControllerConfig {
9
+ targetDistance: number;
10
+ maxLinearVel: number;
11
+ maxAngularVel: number;
12
+ kpDistance: number;
13
+ kpAngular: number;
14
+ distanceDeadzone: number;
15
+ angularDeadzone: number;
16
+ smoothingFactor: number;
17
+ watchdogTimeoutMs: number;
18
+ }
19
+
20
+ export const DEFAULT_CONTROLLER_CONFIG: ControllerConfig = {
21
+ targetDistance: 1.0,
22
+ maxLinearVel: 0.5,
23
+ maxAngularVel: 1.0,
24
+ kpDistance: 0.5,
25
+ kpAngular: 1.5,
26
+ distanceDeadzone: 0.05,
27
+ angularDeadzone: 0.05,
28
+ smoothingFactor: 0.3,
29
+ watchdogTimeoutMs: 500,
30
+ };
31
+
32
+ export interface Twist {
33
+ linearX: number;
34
+ angularZ: number;
35
+ }
36
+
37
+ export interface TargetSample {
38
+ /** Lateral offset in metres (positive = right of camera centre). */
39
+ x: number;
40
+ /** Forward depth in metres. */
41
+ z: number;
42
+ confidence: number;
43
+ }
44
+
45
+ export class FollowerController {
46
+ readonly config: ControllerConfig;
47
+ private smoothedLinear = 0;
48
+ private smoothedAngular = 0;
49
+ private lastDetectionMs = 0;
50
+ private lastTwist: Twist = { linearX: 0, angularZ: 0 };
51
+
52
+ constructor(config: Partial<ControllerConfig> = {}) {
53
+ this.config = { ...DEFAULT_CONTROLLER_CONFIG, ...config };
54
+ }
55
+
56
+ setTargetDistance(d: number): void {
57
+ this.config.targetDistance = Math.max(0.3, Math.min(5.0, d));
58
+ }
59
+
60
+ reset(): void {
61
+ this.smoothedLinear = 0;
62
+ this.smoothedAngular = 0;
63
+ this.lastTwist = { linearX: 0, angularZ: 0 };
64
+ this.lastDetectionMs = Date.now();
65
+ }
66
+
67
+ /** Compute next twist. Pass null when no person is detected this tick. */
68
+ update(target: TargetSample | null): Twist {
69
+ const now = Date.now();
70
+ if (!target) {
71
+ if (now - this.lastDetectionMs > this.config.watchdogTimeoutMs) {
72
+ this.smoothedLinear = 0;
73
+ this.smoothedAngular = 0;
74
+ this.lastTwist = { linearX: 0, angularZ: 0 };
75
+ return this.lastTwist;
76
+ }
77
+ return this.lastTwist;
78
+ }
79
+ this.lastDetectionMs = now;
80
+
81
+ let distErr = target.z - this.config.targetDistance;
82
+ // angular error: pointing right (positive x) should turn right (negative angular_z)
83
+ let angErr = -Math.atan2(target.x, Math.max(target.z, 0.1));
84
+
85
+ if (Math.abs(distErr) < this.config.distanceDeadzone) distErr = 0;
86
+ if (Math.abs(angErr) < this.config.angularDeadzone) angErr = 0;
87
+
88
+ let linearCmd = this.config.kpDistance * distErr;
89
+ let angularCmd = this.config.kpAngular * angErr;
90
+
91
+ linearCmd = clamp(linearCmd, -this.config.maxLinearVel, this.config.maxLinearVel);
92
+ angularCmd = clamp(angularCmd, -this.config.maxAngularVel, this.config.maxAngularVel);
93
+
94
+ const a = this.config.smoothingFactor;
95
+ this.smoothedLinear = a * linearCmd + (1 - a) * this.smoothedLinear;
96
+ this.smoothedAngular = a * angularCmd + (1 - a) * this.smoothedAngular;
97
+
98
+ this.lastTwist = { linearX: this.smoothedLinear, angularZ: this.smoothedAngular };
99
+ return this.lastTwist;
100
+ }
101
+
102
+ getLastTwist(): Twist {
103
+ return this.lastTwist;
104
+ }
105
+ }
106
+
107
+ function clamp(v: number, lo: number, hi: number): number {
108
+ return Math.max(lo, Math.min(hi, v));
109
+ }