agenticros 0.3.3 → 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.
- package/README.md +25 -7
- package/dist/__tests__/claude-config.test.d.ts +2 -0
- package/dist/__tests__/claude-config.test.d.ts.map +1 -0
- package/dist/__tests__/claude-config.test.js +77 -0
- package/dist/__tests__/claude-config.test.js.map +1 -0
- package/dist/__tests__/mcp-setup.test.d.ts +2 -0
- package/dist/__tests__/mcp-setup.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-setup.test.js +23 -0
- package/dist/__tests__/mcp-setup.test.js.map +1 -0
- package/dist/commands/claude.d.ts +16 -0
- package/dist/commands/claude.d.ts.map +1 -0
- package/dist/commands/claude.js +30 -0
- package/dist/commands/claude.js.map +1 -0
- package/dist/commands/codex.d.ts +4 -4
- package/dist/commands/codex.d.ts.map +1 -1
- package/dist/commands/codex.js +21 -90
- package/dist/commands/codex.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -27
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hermes.d.ts +3 -3
- package/dist/commands/hermes.d.ts.map +1 -1
- package/dist/commands/hermes.js +13 -76
- package/dist/commands/hermes.js.map +1 -1
- package/dist/commands/init.d.ts +2 -3
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -38
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp.d.ts +20 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +31 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -1
- package/dist/util/claude-config.d.ts +47 -0
- package/dist/util/claude-config.d.ts.map +1 -0
- package/dist/util/claude-config.js +227 -0
- package/dist/util/claude-config.js.map +1 -0
- package/dist/util/mcp-setup.d.ts +55 -0
- package/dist/util/mcp-setup.d.ts.map +1 -0
- package/dist/util/mcp-setup.js +235 -0
- package/dist/util/mcp-setup.js.map +1 -0
- package/dist/util/skill-manifest.d.ts.map +1 -1
- package/dist/util/skill-manifest.js +22 -1
- package/dist/util/skill-manifest.js.map +1 -1
- package/package.json +1 -1
- package/runtime/BUNDLE.json +1 -1
- package/runtime/README.md +31 -6
- package/runtime/docs/cli.md +54 -14
- package/runtime/docs/robot-setup.md +1 -1
- package/runtime/packages/agenticros/openclaw.plugin.json +147 -1
- package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +2 -0
- package/runtime/packages/agenticros/src/routes.ts +4 -3
- package/runtime/packages/agenticros/src/tools/index.ts +6 -5
- package/runtime/packages/agenticros/src/tools/mission-pause.ts +49 -0
- package/runtime/packages/agenticros/src/tools/mission-resume.ts +41 -0
- package/runtime/packages/agenticros/src/tools/ros2-mission.ts +33 -84
- package/runtime/packages/agenticros-claude-code/README.md +2 -0
- package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/tools.js +130 -111
- package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/src/tools.ts +140 -99
- package/runtime/packages/agenticros-gemini/package.json +2 -0
- package/runtime/packages/agenticros-gemini/src/find-object/coco-classes.ts +38 -0
- package/runtime/packages/agenticros-gemini/src/find-object/find-object.ts +191 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/controller.ts +109 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/depth-loop.ts +452 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/detector.ts +303 -0
- package/runtime/packages/agenticros-gemini/src/follow-me/loop.ts +359 -0
- package/runtime/packages/agenticros-gemini/src/tools.ts +351 -90
- package/runtime/packages/core/README.md +1 -1
- package/runtime/packages/core/package.json +1 -1
- package/runtime/packages/core/src/__tests__/config-persistence.test.ts +26 -0
- package/runtime/packages/core/src/__tests__/external-capability.test.ts +72 -0
- package/runtime/packages/core/src/__tests__/heartbeat-fleet.test.ts +152 -0
- package/runtime/packages/core/src/__tests__/mission-bindings.test.ts +101 -0
- package/runtime/packages/core/src/__tests__/mission-pause.test.ts +104 -0
- package/runtime/packages/core/src/capabilities.ts +38 -3
- package/runtime/packages/core/src/capability-schema.ts +58 -0
- package/runtime/packages/core/src/config.ts +20 -0
- package/runtime/packages/core/src/discovery.ts +21 -3
- package/runtime/packages/core/src/external-capability.ts +219 -0
- package/runtime/packages/core/src/fleet-config.ts +91 -0
- package/runtime/packages/core/src/heartbeat.ts +167 -0
- package/runtime/packages/core/src/index.ts +55 -0
- package/runtime/packages/core/src/mission-bindings.ts +189 -0
- package/runtime/packages/core/src/mission-registry.ts +31 -3
- package/runtime/packages/core/src/mission.ts +64 -7
- package/runtime/packages/core/src/robots.ts +6 -2
- package/runtime/packages/core/src/transport/rosbridge/client.ts +7 -0
- package/runtime/pnpm-lock.yaml +6 -0
- package/templates/skills/camera/package.json +1 -1
- package/templates/skills/depth/package.json +1 -1
- package/templates/skills/robot/package.json +1 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External ROS-node capability executor — Phase 1 completion.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches `implementation.kind: "external_ros_node"` capabilities via
|
|
5
|
+
* the existing RosTransport action / service / topic APIs. The manifest
|
|
6
|
+
* `launch` field is metadata only (operator-owned bringup) — the gateway
|
|
7
|
+
* never shells out to `ros2 launch` in v1.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Capability, CapabilityImplementation } from "./capabilities.js";
|
|
11
|
+
import type { RosTransport } from "./transport/transport.js";
|
|
12
|
+
import { toNamespacedTopic } from "./topic-utils.js";
|
|
13
|
+
|
|
14
|
+
export interface ExecuteExternalOptions {
|
|
15
|
+
/** Robot namespace for topic/action/service prefixing. */
|
|
16
|
+
namespace?: string;
|
|
17
|
+
/** Optional timeout for subscribe_once (ms). */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ExecuteExternalResult {
|
|
22
|
+
text: string;
|
|
23
|
+
outputs?: Record<string, unknown>;
|
|
24
|
+
isError?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isExternalImpl(
|
|
28
|
+
impl: CapabilityImplementation | undefined,
|
|
29
|
+
): impl is Extract<CapabilityImplementation, { kind: "external_ros_node" }> {
|
|
30
|
+
return impl?.kind === "external_ros_node";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveName(name: string, namespace?: string): string {
|
|
34
|
+
const trimmed = name.trim();
|
|
35
|
+
if (!trimmed) return trimmed;
|
|
36
|
+
if (!namespace?.trim()) return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
37
|
+
return toNamespacedTopic(namespace, trimmed);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Map navigate_to-style inputs into a Nav2 NavigateToPose goal when the
|
|
42
|
+
* action type looks like NavigateToPose and inputs have x/y.
|
|
43
|
+
*/
|
|
44
|
+
export function buildExternalGoal(
|
|
45
|
+
impl: Extract<CapabilityImplementation, { kind: "external_ros_node" }>,
|
|
46
|
+
inputs: Record<string, unknown>,
|
|
47
|
+
): Record<string, unknown> {
|
|
48
|
+
// Explicit goal / request / message wins.
|
|
49
|
+
if (inputs.goal && typeof inputs.goal === "object") {
|
|
50
|
+
return inputs.goal as Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
if (inputs.request && typeof inputs.request === "object") {
|
|
53
|
+
return inputs.request as Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
if (inputs.message && typeof inputs.message === "object") {
|
|
56
|
+
return inputs.message as Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const msgType = (impl.msg_type ?? "").toLowerCase();
|
|
60
|
+
if (msgType.includes("navigatetopose") || impl.action?.includes("navigate_to_pose")) {
|
|
61
|
+
const x = Number(inputs.x ?? 0) || 0;
|
|
62
|
+
const y = Number(inputs.y ?? 0) || 0;
|
|
63
|
+
const yaw = Number(inputs.yaw ?? 0) || 0;
|
|
64
|
+
const qz = Math.sin(yaw / 2);
|
|
65
|
+
const qw = Math.cos(yaw / 2);
|
|
66
|
+
const frame = typeof inputs.frame_id === "string" ? inputs.frame_id : "map";
|
|
67
|
+
return {
|
|
68
|
+
pose: {
|
|
69
|
+
header: { frame_id: frame },
|
|
70
|
+
pose: {
|
|
71
|
+
position: { x, y, z: 0 },
|
|
72
|
+
orientation: { x: 0, y: 0, z: qz, w: qw },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Passthrough remaining inputs (minus robot_id).
|
|
79
|
+
const { robot_id: _rid, ...rest } = inputs;
|
|
80
|
+
return rest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Execute an external_ros_node capability against a connected transport.
|
|
85
|
+
*/
|
|
86
|
+
export async function executeExternalCapability(
|
|
87
|
+
capability: Capability,
|
|
88
|
+
inputs: Record<string, unknown>,
|
|
89
|
+
transport: RosTransport,
|
|
90
|
+
options: ExecuteExternalOptions = {},
|
|
91
|
+
): Promise<ExecuteExternalResult> {
|
|
92
|
+
const impl = capability.implementation;
|
|
93
|
+
if (!isExternalImpl(impl)) {
|
|
94
|
+
return {
|
|
95
|
+
text: `Capability "${capability.id}" is not an external_ros_node implementation.`,
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ns = options.namespace;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
if (impl.action) {
|
|
104
|
+
const action = resolveName(impl.action, ns);
|
|
105
|
+
const actionType = impl.msg_type ?? "";
|
|
106
|
+
if (!actionType) {
|
|
107
|
+
return {
|
|
108
|
+
text: `Capability "${capability.id}" external action requires implementation.msg_type.`,
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const args = buildExternalGoal(impl, inputs);
|
|
113
|
+
const result = await transport.sendActionGoal({
|
|
114
|
+
action,
|
|
115
|
+
actionType,
|
|
116
|
+
args,
|
|
117
|
+
});
|
|
118
|
+
const outputs = {
|
|
119
|
+
success: result.result,
|
|
120
|
+
...(result.values ?? {}),
|
|
121
|
+
launch_hint: impl.launch ?? null,
|
|
122
|
+
package_hint: impl.package ?? null,
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
text: JSON.stringify(outputs),
|
|
126
|
+
outputs,
|
|
127
|
+
isError: !result.result,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (impl.service) {
|
|
132
|
+
const service = resolveName(impl.service, ns);
|
|
133
|
+
const serviceType = impl.msg_type ?? "";
|
|
134
|
+
if (!serviceType) {
|
|
135
|
+
return {
|
|
136
|
+
text: `Capability "${capability.id}" external service requires implementation.msg_type.`,
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const args = buildExternalGoal(impl, inputs);
|
|
141
|
+
const result = await transport.callService({
|
|
142
|
+
service,
|
|
143
|
+
type: serviceType,
|
|
144
|
+
args,
|
|
145
|
+
});
|
|
146
|
+
const outputs = {
|
|
147
|
+
success: result.result,
|
|
148
|
+
...(result.values ?? {}),
|
|
149
|
+
launch_hint: impl.launch ?? null,
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
text: JSON.stringify(outputs),
|
|
153
|
+
outputs,
|
|
154
|
+
isError: !result.result,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (impl.topic) {
|
|
159
|
+
const topic = resolveName(impl.topic, ns);
|
|
160
|
+
const msgType = impl.msg_type ?? "";
|
|
161
|
+
const mode = typeof inputs.mode === "string" ? inputs.mode : "publish";
|
|
162
|
+
if (mode === "subscribe" || mode === "subscribe_once") {
|
|
163
|
+
const timeout =
|
|
164
|
+
options.timeoutMs ?? (typeof inputs.timeout === "number" ? inputs.timeout : 5000);
|
|
165
|
+
const msg = await new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
166
|
+
let settled = false;
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
if (settled) return;
|
|
169
|
+
settled = true;
|
|
170
|
+
try {
|
|
171
|
+
sub.unsubscribe();
|
|
172
|
+
} catch {
|
|
173
|
+
/* ignore */
|
|
174
|
+
}
|
|
175
|
+
reject(new Error(`Timed out waiting for message on ${topic} after ${timeout}ms`));
|
|
176
|
+
}, timeout);
|
|
177
|
+
const sub = transport.subscribe(
|
|
178
|
+
{ topic, type: msgType || "std_msgs/msg/String" },
|
|
179
|
+
(message) => {
|
|
180
|
+
if (settled) return;
|
|
181
|
+
settled = true;
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
try {
|
|
184
|
+
sub.unsubscribe();
|
|
185
|
+
} catch {
|
|
186
|
+
/* ignore */
|
|
187
|
+
}
|
|
188
|
+
resolve(message as Record<string, unknown>);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
const outputs = { message: msg, topic };
|
|
193
|
+
return { text: JSON.stringify(outputs), outputs };
|
|
194
|
+
}
|
|
195
|
+
if (!msgType) {
|
|
196
|
+
return {
|
|
197
|
+
text: `Capability "${capability.id}" external topic publish requires implementation.msg_type.`,
|
|
198
|
+
isError: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const message = buildExternalGoal(impl, inputs);
|
|
202
|
+
await transport.publish({ topic, type: msgType, msg: message });
|
|
203
|
+
const outputs = { published: true, topic, launch_hint: impl.launch ?? null };
|
|
204
|
+
return { text: JSON.stringify(outputs), outputs };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
text:
|
|
209
|
+
`Capability "${capability.id}" external_ros_node has no action, service, or topic. ` +
|
|
210
|
+
`Bringup hint: ${impl.launch ?? impl.package ?? "(none)"}.`,
|
|
211
|
+
isError: true,
|
|
212
|
+
};
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return {
|
|
215
|
+
text: err instanceof Error ? err.message : String(err),
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static fleet file — Phase 1.d hybrid discovery.
|
|
3
|
+
*
|
|
4
|
+
* Precedence: `AGENTICROS_FLEET_PATH` → `~/.agenticros/fleet.json` →
|
|
5
|
+
* `config.robots[]` (via listRobots). When a fleet file exists and
|
|
6
|
+
* parses as a non-empty robot array (or `{ robots: [...] }`), it wins.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { AgenticROSConfig } from "./config.js";
|
|
13
|
+
import { AgenticROSConfigSchema } from "./config.js";
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_FLEET_FILENAME = "fleet.json";
|
|
16
|
+
|
|
17
|
+
/** Resolve the fleet file path (env override or default under ~/.agenticros). */
|
|
18
|
+
export function resolveFleetPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
19
|
+
const override = env.AGENTICROS_FLEET_PATH?.trim();
|
|
20
|
+
if (override) return override;
|
|
21
|
+
return join(homedir(), ".agenticros", DEFAULT_FLEET_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FleetFileResult {
|
|
25
|
+
/** Absolute path that was checked. */
|
|
26
|
+
path: string;
|
|
27
|
+
/** True when the file exists and was used. */
|
|
28
|
+
used: boolean;
|
|
29
|
+
/** Parsed robot entries (empty when unused / invalid). */
|
|
30
|
+
robots: NonNullable<AgenticROSConfig["robots"]>;
|
|
31
|
+
/** Human-readable error when the file exists but failed to parse. */
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load `fleet.json` if present. Does not throw on missing file.
|
|
37
|
+
* Invalid JSON / schema → `used: false` with `error` set so callers can warn.
|
|
38
|
+
*/
|
|
39
|
+
export function loadFleetFile(path?: string): FleetFileResult {
|
|
40
|
+
const fleetPath = path ?? resolveFleetPath();
|
|
41
|
+
if (!existsSync(fleetPath)) {
|
|
42
|
+
return { path: fleetPath, used: false, robots: [] };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const raw = JSON.parse(readFileSync(fleetPath, "utf8")) as unknown;
|
|
46
|
+
let robotsRaw: unknown;
|
|
47
|
+
if (Array.isArray(raw)) {
|
|
48
|
+
robotsRaw = raw;
|
|
49
|
+
} else if (raw && typeof raw === "object" && Array.isArray((raw as { robots?: unknown }).robots)) {
|
|
50
|
+
robotsRaw = (raw as { robots: unknown[] }).robots;
|
|
51
|
+
} else {
|
|
52
|
+
return {
|
|
53
|
+
path: fleetPath,
|
|
54
|
+
used: false,
|
|
55
|
+
robots: [],
|
|
56
|
+
error: "fleet.json must be an array of robots or { robots: [...] }",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const parsed = AgenticROSConfigSchema.pick({ robots: true }).safeParse({ robots: robotsRaw });
|
|
60
|
+
if (!parsed.success) {
|
|
61
|
+
return {
|
|
62
|
+
path: fleetPath,
|
|
63
|
+
used: false,
|
|
64
|
+
robots: [],
|
|
65
|
+
error: parsed.error.message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const robots = parsed.data.robots ?? [];
|
|
69
|
+
if (robots.length === 0) {
|
|
70
|
+
return { path: fleetPath, used: false, robots: [] };
|
|
71
|
+
}
|
|
72
|
+
return { path: fleetPath, used: true, robots };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return {
|
|
75
|
+
path: fleetPath,
|
|
76
|
+
used: false,
|
|
77
|
+
robots: [],
|
|
78
|
+
error: err instanceof Error ? err.message : String(err),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Return a config view where `robots` comes from fleet.json when present.
|
|
85
|
+
* Does not mutate the input config object.
|
|
86
|
+
*/
|
|
87
|
+
export function applyFleetOverride(config: AgenticROSConfig, fleetPath?: string): AgenticROSConfig {
|
|
88
|
+
const fleet = loadFleetFile(fleetPath);
|
|
89
|
+
if (!fleet.used) return config;
|
|
90
|
+
return { ...config, robots: fleet.robots };
|
|
91
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RobotInfo heartbeat helpers — Phase 1.d completion.
|
|
3
|
+
*
|
|
4
|
+
* `agenticros_discovery` publishes `<ns>/agenticros/robot_info` at 1 Hz.
|
|
5
|
+
* Consumers treat a robot as online when the last stamp is within
|
|
6
|
+
* {@link DEFAULT_HEARTBEAT_STALENESS_MS} (5 s per strategy memo).
|
|
7
|
+
*
|
|
8
|
+
* Topic-scan path: when adapters only have `listTopics()`, presence of
|
|
9
|
+
* `/<ns>/agenticros/robot_info` on the graph is a weak online signal
|
|
10
|
+
* (same as cmd_vel). Prefer {@link mergeRobotHeartbeats} when message
|
|
11
|
+
* payloads with stamps are available.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { effectiveCmdVelNamespace } from "./discovery.js";
|
|
15
|
+
import type { TopicInfo } from "./transport/types.js";
|
|
16
|
+
import type { RobotSensors } from "./robots.js";
|
|
17
|
+
|
|
18
|
+
/** Default staleness window — strategy §4(d): 1 Hz + 5 s. */
|
|
19
|
+
export const DEFAULT_HEARTBEAT_STALENESS_MS = 5_000;
|
|
20
|
+
|
|
21
|
+
/** Parsed robot_info payload (subset of agenticros_msgs/RobotInfo). */
|
|
22
|
+
export interface RobotHeartbeat {
|
|
23
|
+
/** Stable id (falls back to namespace). */
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
/** ROS namespace without leading slash. */
|
|
28
|
+
robot_namespace: string;
|
|
29
|
+
capability_ids: string[];
|
|
30
|
+
sensors: RobotSensors;
|
|
31
|
+
/** Epoch ms of the heartbeat stamp (0 when unknown). */
|
|
32
|
+
stamp_ms: number;
|
|
33
|
+
/** Topic the heartbeat was observed on. */
|
|
34
|
+
topic: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface HeartbeatOnlineOptions {
|
|
38
|
+
/** Wall clock now (ms). Defaults to Date.now(). */
|
|
39
|
+
nowMs?: number;
|
|
40
|
+
/** Staleness window in ms. Defaults to 5000. */
|
|
41
|
+
stalenessMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract namespace from `/<ns>/agenticros/robot_info` (or without leading slash).
|
|
46
|
+
* Returns "" for unnamespaced `/agenticros/robot_info`.
|
|
47
|
+
*/
|
|
48
|
+
export function namespaceFromRobotInfoTopic(topic: string): string {
|
|
49
|
+
const m = topic.match(/^\/?([^/]+)\/agenticros\/robot_info$/);
|
|
50
|
+
if (m) return m[1]!;
|
|
51
|
+
if (topic === "/agenticros/robot_info" || topic === "agenticros/robot_info") return "";
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True when the topic name is a robot_info heartbeat topic. */
|
|
56
|
+
export function isRobotInfoTopic(topic: string): boolean {
|
|
57
|
+
return /(?:^|\/)agenticros\/robot_info$/.test(topic);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect namespaces that advertise `agenticros/robot_info` on the topic graph
|
|
62
|
+
* (no stamp available — treat as online for discovery fallback).
|
|
63
|
+
*/
|
|
64
|
+
export function detectHeartbeatNamespacesFromTopics(topics: TopicInfo[]): string[] {
|
|
65
|
+
const out = new Set<string>();
|
|
66
|
+
for (const t of topics) {
|
|
67
|
+
if (!isRobotInfoTopic(t.name)) continue;
|
|
68
|
+
out.add(namespaceFromRobotInfoTopic(t.name));
|
|
69
|
+
}
|
|
70
|
+
return [...out];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function rosTimeToMs(stamp: unknown): number {
|
|
74
|
+
if (!stamp || typeof stamp !== "object") return 0;
|
|
75
|
+
const s = stamp as Record<string, unknown>;
|
|
76
|
+
const sec = Number(s.sec ?? s.secs ?? 0) || 0;
|
|
77
|
+
const nanosec = Number(s.nanosec ?? s.nsecs ?? 0) || 0;
|
|
78
|
+
return sec * 1000 + Math.floor(nanosec / 1e6);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a plain RobotInfo-like message into {@link RobotHeartbeat}.
|
|
83
|
+
* Tolerates missing optional fields.
|
|
84
|
+
*/
|
|
85
|
+
export function parseRobotInfoMessage(
|
|
86
|
+
topic: string,
|
|
87
|
+
msg: Record<string, unknown>,
|
|
88
|
+
): RobotHeartbeat {
|
|
89
|
+
const nsFromTopic = namespaceFromRobotInfoTopic(topic);
|
|
90
|
+
const robot_namespace = String(msg.robot_namespace ?? msg.namespace ?? nsFromTopic ?? "");
|
|
91
|
+
const id = String(msg.id ?? (robot_namespace || "default"));
|
|
92
|
+
const capability_ids = Array.isArray(msg.capability_ids)
|
|
93
|
+
? msg.capability_ids.map((c) => String(c))
|
|
94
|
+
: Array.isArray(msg.capabilities)
|
|
95
|
+
? (msg.capabilities as unknown[]).map((c) => String(c))
|
|
96
|
+
: [];
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
name: String(msg.name ?? "Robot"),
|
|
100
|
+
kind: String(msg.kind ?? "amr"),
|
|
101
|
+
robot_namespace,
|
|
102
|
+
capability_ids,
|
|
103
|
+
sensors: {
|
|
104
|
+
has_realsense: Boolean(msg.has_realsense),
|
|
105
|
+
has_lidar: Boolean(msg.has_lidar),
|
|
106
|
+
has_arm: Boolean(msg.has_arm),
|
|
107
|
+
},
|
|
108
|
+
stamp_ms: rosTimeToMs(msg.stamp),
|
|
109
|
+
topic,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** True when the heartbeat stamp is within the staleness window. */
|
|
114
|
+
export function isHeartbeatFresh(
|
|
115
|
+
heartbeat: RobotHeartbeat,
|
|
116
|
+
options: HeartbeatOnlineOptions = {},
|
|
117
|
+
): boolean {
|
|
118
|
+
const now = options.nowMs ?? Date.now();
|
|
119
|
+
const window = options.stalenessMs ?? DEFAULT_HEARTBEAT_STALENESS_MS;
|
|
120
|
+
if (!heartbeat.stamp_ms) {
|
|
121
|
+
// No stamp — treat as fresh if we just received the message (caller
|
|
122
|
+
// should only pass live subscribe results here).
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return now - heartbeat.stamp_ms <= window;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Merge heartbeats into an online-id set keyed by configured robot id
|
|
130
|
+
* when `configuredIdByNamespace` is provided, else by heartbeat id /
|
|
131
|
+
* effective namespace.
|
|
132
|
+
*/
|
|
133
|
+
export function onlineIdsFromHeartbeats(
|
|
134
|
+
heartbeats: readonly RobotHeartbeat[],
|
|
135
|
+
options: HeartbeatOnlineOptions & {
|
|
136
|
+
/** Map effective cmd_vel namespace → configured robot id. */
|
|
137
|
+
configuredIdByNamespace?: ReadonlyMap<string, string>;
|
|
138
|
+
} = {},
|
|
139
|
+
): Set<string> {
|
|
140
|
+
const online = new Set<string>();
|
|
141
|
+
for (const hb of heartbeats) {
|
|
142
|
+
if (!isHeartbeatFresh(hb, options)) continue;
|
|
143
|
+
const eff = effectiveCmdVelNamespace(hb.robot_namespace);
|
|
144
|
+
const cfgId = options.configuredIdByNamespace?.get(eff);
|
|
145
|
+
online.add(cfgId ?? hb.id);
|
|
146
|
+
}
|
|
147
|
+
return online;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Build a map of effective-namespace → latest fresh heartbeat.
|
|
152
|
+
*/
|
|
153
|
+
export function mergeRobotHeartbeats(
|
|
154
|
+
heartbeats: readonly RobotHeartbeat[],
|
|
155
|
+
options: HeartbeatOnlineOptions = {},
|
|
156
|
+
): Map<string, RobotHeartbeat> {
|
|
157
|
+
const byNs = new Map<string, RobotHeartbeat>();
|
|
158
|
+
for (const hb of heartbeats) {
|
|
159
|
+
if (!isHeartbeatFresh(hb, options)) continue;
|
|
160
|
+
const key = effectiveCmdVelNamespace(hb.robot_namespace);
|
|
161
|
+
const prev = byNs.get(key);
|
|
162
|
+
if (!prev || hb.stamp_ms >= prev.stamp_ms) {
|
|
163
|
+
byNs.set(key, hb);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return byNs;
|
|
167
|
+
}
|
|
@@ -6,6 +6,7 @@ export type { AgenticROSConfig, RobotTransportOverride } from "./config.js";
|
|
|
6
6
|
export {
|
|
7
7
|
AgenticROSConfigSchema,
|
|
8
8
|
parseConfig,
|
|
9
|
+
prepareConfigForPersistence,
|
|
9
10
|
getTransportConfig,
|
|
10
11
|
} from "./config.js";
|
|
11
12
|
|
|
@@ -65,6 +66,24 @@ export type {
|
|
|
65
66
|
CapabilitySource,
|
|
66
67
|
} from "./capabilities.js";
|
|
67
68
|
|
|
69
|
+
export {
|
|
70
|
+
CapabilitySchema,
|
|
71
|
+
CapabilityFieldSchema,
|
|
72
|
+
CapabilityImplementationSchema,
|
|
73
|
+
parseCapability,
|
|
74
|
+
safeParseCapability,
|
|
75
|
+
} from "./capability-schema.js";
|
|
76
|
+
export type { ParsedCapability } from "./capability-schema.js";
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
buildExternalGoal,
|
|
80
|
+
executeExternalCapability,
|
|
81
|
+
} from "./external-capability.js";
|
|
82
|
+
export type {
|
|
83
|
+
ExecuteExternalOptions,
|
|
84
|
+
ExecuteExternalResult,
|
|
85
|
+
} from "./external-capability.js";
|
|
86
|
+
|
|
68
87
|
export { runMission } from "./mission.js";
|
|
69
88
|
export type {
|
|
70
89
|
Mission,
|
|
@@ -75,11 +94,27 @@ export type {
|
|
|
75
94
|
CapabilityToolBinding,
|
|
76
95
|
CapabilityToolBindings,
|
|
77
96
|
MissionCancellationToken,
|
|
97
|
+
MissionControlToken,
|
|
78
98
|
MissionTranscriptEntry,
|
|
79
99
|
MissionTranscriptSink,
|
|
80
100
|
RunMissionOptions,
|
|
81
101
|
} from "./mission.js";
|
|
82
102
|
|
|
103
|
+
export {
|
|
104
|
+
BUILTIN_MISSION_BINDINGS,
|
|
105
|
+
EXTERNAL_TOOL_PREFIX,
|
|
106
|
+
buildMissionBindings,
|
|
107
|
+
capabilityIdFromExternalTool,
|
|
108
|
+
defaultToolForCapability,
|
|
109
|
+
externalToolName,
|
|
110
|
+
isExternalToolName,
|
|
111
|
+
passthroughBuildArgs,
|
|
112
|
+
} from "./mission-bindings.js";
|
|
113
|
+
export type {
|
|
114
|
+
BuildMissionBindingsOptions,
|
|
115
|
+
CapabilityWithTool,
|
|
116
|
+
} from "./mission-bindings.js";
|
|
117
|
+
|
|
83
118
|
export {
|
|
84
119
|
MissionRegistry,
|
|
85
120
|
generateMissionId,
|
|
@@ -109,6 +144,26 @@ export {
|
|
|
109
144
|
} from "./discovery.js";
|
|
110
145
|
export type { DetectedRobot, RobotDiscoveryResult } from "./discovery.js";
|
|
111
146
|
|
|
147
|
+
export {
|
|
148
|
+
DEFAULT_HEARTBEAT_STALENESS_MS,
|
|
149
|
+
detectHeartbeatNamespacesFromTopics,
|
|
150
|
+
isHeartbeatFresh,
|
|
151
|
+
isRobotInfoTopic,
|
|
152
|
+
mergeRobotHeartbeats,
|
|
153
|
+
namespaceFromRobotInfoTopic,
|
|
154
|
+
onlineIdsFromHeartbeats,
|
|
155
|
+
parseRobotInfoMessage,
|
|
156
|
+
} from "./heartbeat.js";
|
|
157
|
+
export type { HeartbeatOnlineOptions, RobotHeartbeat } from "./heartbeat.js";
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
DEFAULT_FLEET_FILENAME,
|
|
161
|
+
applyFleetOverride,
|
|
162
|
+
loadFleetFile,
|
|
163
|
+
resolveFleetPath,
|
|
164
|
+
} from "./fleet-config.js";
|
|
165
|
+
export type { FleetFileResult } from "./fleet-config.js";
|
|
166
|
+
|
|
112
167
|
export { findRobotsFor } from "./find-robots-for.js";
|
|
113
168
|
export type {
|
|
114
169
|
FindRobotsForQuery,
|