agenticros 0.1.18 → 0.2.0

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 (164) hide show
  1. package/dist/__tests__/robot-config.test.d.ts +2 -0
  2. package/dist/__tests__/robot-config.test.d.ts.map +1 -0
  3. package/dist/__tests__/robot-config.test.js +374 -0
  4. package/dist/__tests__/robot-config.test.js.map +1 -0
  5. package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
  6. package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
  7. package/dist/__tests__/transport-shorthand.test.js +88 -0
  8. package/dist/__tests__/transport-shorthand.test.js.map +1 -0
  9. package/dist/commands/robots.d.ts +57 -0
  10. package/dist/commands/robots.d.ts.map +1 -0
  11. package/dist/commands/robots.js +678 -0
  12. package/dist/commands/robots.js.map +1 -0
  13. package/dist/commands/skills.d.ts.map +1 -1
  14. package/dist/commands/skills.js +125 -2
  15. package/dist/commands/skills.js.map +1 -1
  16. package/dist/index.js +29 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/menu.js +22 -4
  19. package/dist/menu.js.map +1 -1
  20. package/dist/util/marketplace.d.ts +57 -0
  21. package/dist/util/marketplace.d.ts.map +1 -0
  22. package/dist/util/marketplace.js +144 -0
  23. package/dist/util/marketplace.js.map +1 -0
  24. package/dist/util/mcp-discovery.d.ts +57 -0
  25. package/dist/util/mcp-discovery.d.ts.map +1 -0
  26. package/dist/util/mcp-discovery.js +153 -0
  27. package/dist/util/mcp-discovery.js.map +1 -0
  28. package/dist/util/robot-config.d.ts +167 -0
  29. package/dist/util/robot-config.d.ts.map +1 -0
  30. package/dist/util/robot-config.js +375 -0
  31. package/dist/util/robot-config.js.map +1 -0
  32. package/dist/util/skills.d.ts +6 -6
  33. package/dist/util/skills.d.ts.map +1 -1
  34. package/dist/util/skills.js +14 -9
  35. package/dist/util/skills.js.map +1 -1
  36. package/dist/util/transport-shorthand.d.ts +66 -0
  37. package/dist/util/transport-shorthand.d.ts.map +1 -0
  38. package/dist/util/transport-shorthand.js +102 -0
  39. package/dist/util/transport-shorthand.js.map +1 -0
  40. package/package.json +12 -10
  41. package/runtime/BUNDLE.json +1 -1
  42. package/runtime/README.md +62 -1
  43. package/runtime/package.json +6 -1
  44. package/runtime/packages/agenticros/openclaw.plugin.json +7 -1
  45. package/runtime/packages/agenticros/package.json +2 -1
  46. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
  47. package/runtime/packages/agenticros/src/config-page.ts +1 -1
  48. package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
  49. package/runtime/packages/agenticros/src/service.ts +120 -2
  50. package/runtime/packages/agenticros/src/skill-loader.ts +109 -30
  51. package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
  52. package/runtime/packages/agenticros/src/tools/index.ts +49 -9
  53. package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
  54. package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
  55. package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
  56. package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
  57. package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
  58. package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
  59. package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
  60. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
  61. package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
  62. package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
  63. package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
  64. package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
  65. package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
  66. package/runtime/packages/agenticros-claude-code/README.md +52 -9
  67. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts +2 -0
  68. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts.map +1 -0
  69. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js +773 -0
  70. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js.map +1 -0
  71. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts +2 -0
  72. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts.map +1 -0
  73. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js +97 -0
  74. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js.map +1 -0
  75. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts +2 -2
  76. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts.map +1 -1
  77. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js +5 -5
  78. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js.map +1 -1
  79. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts +109 -0
  80. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts.map +1 -0
  81. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js +392 -0
  82. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js.map +1 -0
  83. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts +8 -3
  84. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts.map +1 -1
  85. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js +30 -12
  86. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js.map +1 -1
  87. package/runtime/packages/agenticros-claude-code/dist/index.js +5 -5
  88. package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
  89. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts +16 -0
  90. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts.map +1 -0
  91. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js +19 -0
  92. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js.map +1 -0
  93. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts +9 -0
  94. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  95. package/runtime/packages/agenticros-claude-code/dist/tools.js +700 -54
  96. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  97. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts +28 -8
  98. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts.map +1 -1
  99. package/runtime/packages/agenticros-claude-code/dist/transport.js +37 -34
  100. package/runtime/packages/agenticros-claude-code/dist/transport.js.map +1 -1
  101. package/runtime/packages/agenticros-claude-code/package.json +2 -1
  102. package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +965 -0
  103. package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
  104. package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
  105. package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
  106. package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
  107. package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
  108. package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
  109. package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
  110. package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
  111. package/runtime/packages/agenticros-gemini/package.json +1 -0
  112. package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
  113. package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
  114. package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
  115. package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
  116. package/runtime/packages/core/LICENSE +192 -0
  117. package/runtime/packages/core/README.md +88 -0
  118. package/runtime/packages/core/package.json +36 -3
  119. package/runtime/packages/core/src/__tests__/capabilities.test.ts +266 -0
  120. package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
  121. package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
  122. package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
  123. package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
  124. package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
  125. package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
  126. package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
  127. package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
  128. package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
  129. package/runtime/packages/core/src/capabilities.ts +314 -0
  130. package/runtime/packages/core/src/config.ts +221 -39
  131. package/runtime/packages/core/src/discovery.ts +193 -0
  132. package/runtime/packages/core/src/find-robots-for.ts +164 -0
  133. package/runtime/packages/core/src/index.ts +67 -1
  134. package/runtime/packages/core/src/mission-registry.ts +142 -0
  135. package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
  136. package/runtime/packages/core/src/mission.ts +574 -0
  137. package/runtime/packages/core/src/planner/index.ts +564 -0
  138. package/runtime/packages/core/src/robots.ts +282 -0
  139. package/runtime/packages/core/src/topic-utils.ts +47 -9
  140. package/runtime/packages/core/src/transport-pool.ts +171 -0
  141. package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
  142. package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
  143. package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
  144. package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
  145. package/runtime/scripts/configure_agenticros.sh +0 -0
  146. package/runtime/scripts/configure_for_sim.sh +0 -0
  147. package/runtime/scripts/fix-openclaw-control-ui-path.sh +0 -0
  148. package/runtime/scripts/install_cli.sh +0 -0
  149. package/runtime/scripts/install_rosbridge_from_source.sh +0 -0
  150. package/runtime/scripts/onboard_robot.sh +0 -0
  151. package/runtime/scripts/pack-runtime.mjs +0 -0
  152. package/runtime/scripts/refresh-skill-deps.mjs +381 -0
  153. package/runtime/scripts/run_demo_native.sh +0 -0
  154. package/runtime/scripts/run_nemoclaw_host_stack.sh +0 -0
  155. package/runtime/scripts/run_robot_rosbridge.sh +0 -0
  156. package/runtime/scripts/setup_gateway_plugin.sh +32 -5
  157. package/runtime/scripts/setup_robot.sh +0 -0
  158. package/runtime/scripts/setup_workspace.sh +0 -0
  159. package/runtime/scripts/sim/run_sim.sh +0 -0
  160. package/runtime/scripts/smoke_test_nemoclaw.sh +0 -0
  161. package/runtime/scripts/start_demo.sh +0 -0
  162. package/runtime/scripts/sync-skill-tools.mjs +69 -12
  163. package/runtime/scripts/use-openclaw-2026.2.26.sh +0 -0
  164. package/runtime/scripts/use-openclaw-2026.3.11.sh +0 -0
@@ -1,58 +1,58 @@
1
- import type { RosTransport } from "@agenticros/core";
2
- import { createTransport, getTransportConfig } from "@agenticros/core";
3
- import type { AgenticROSConfig } from "@agenticros/core";
1
+ /**
2
+ * Transport lifecycle for the AgenticROS MCP server (Claude Code adapter).
3
+ *
4
+ * Thin wrapper around `@agenticros/core`'s shared `TransportPool`. The
5
+ * pool owns the per-robot routing logic — see its docstring for the
6
+ * full contract. This module just exposes a module-level singleton so
7
+ * the rest of the adapter doesn't need to thread a pool reference
8
+ * through every call site.
9
+ *
10
+ * Why a module-level singleton: the MCP server is one process. Two
11
+ * pools would mean two transports — exactly what the pool is designed
12
+ * to prevent. Tests inject their own pool via `_swapPoolForTests`.
13
+ */
14
+
15
+ import type { AgenticROSConfig, ResolvedRobot, RosTransport } from "@agenticros/core";
16
+ import { TransportPool } from "@agenticros/core";
4
17
 
5
- let transport: RosTransport | null = null;
18
+ let pool = new TransportPool();
6
19
 
7
20
  /**
8
- * Get the active transport. Throws if not connected.
21
+ * Async per-robot transport accessor — the path every tool call takes
22
+ * once it has resolved a target robot. Lazy-connects on first use.
9
23
  */
10
- export function getTransport(): RosTransport {
11
- if (!transport) {
12
- throw new Error("Transport not initialized. Ensure config is loaded and connect() has been called.");
13
- }
14
- return transport;
15
- }
16
-
17
- export function getTransportOrNull(): RosTransport | null {
18
- return transport;
24
+ export async function getTransportForRobot(
25
+ config: AgenticROSConfig,
26
+ robot: ResolvedRobot,
27
+ ): Promise<RosTransport> {
28
+ return pool.acquire(config, robot);
19
29
  }
20
30
 
21
- const CONNECT_TIMEOUT_MS = 15_000;
22
-
23
31
  /**
24
- * Create and connect the transport. Idempotent.
25
- * Fails after CONNECT_TIMEOUT_MS if Zenoh/router is unreachable.
32
+ * Pre-warm the active robot's transport at server start. Called from
33
+ * `index.ts` before the first tool dispatch — keeps the legacy
34
+ * "connect at startup" UX while routing through the pool internally.
26
35
  */
27
36
  export async function connect(config: AgenticROSConfig): Promise<void> {
28
- if (transport && transport.getStatus() === "connected") {
29
- return;
30
- }
31
- if (transport) {
32
- await transport.disconnect();
33
- transport = null;
34
- }
35
- const transportCfg = getTransportConfig(config);
36
- const newTransport = await createTransport(transportCfg);
37
- const timeoutPromise = new Promise<never>((_, reject) => {
38
- setTimeout(
39
- () => reject(new Error(
40
- `Transport connection timed out after ${CONNECT_TIMEOUT_MS / 1000}s. ` +
41
- "Is zenohd running? (e.g. ws://localhost:10000). Check config and MCP log.",
42
- )),
43
- CONNECT_TIMEOUT_MS,
44
- );
45
- });
46
- await Promise.race([newTransport.connect(), timeoutPromise]);
47
- transport = newTransport;
37
+ await pool.connectActive(config);
48
38
  }
49
39
 
50
40
  /**
51
- * Disconnect and clear the transport. Call on process exit.
41
+ * Drain every connection. Called on SIGINT/SIGTERM.
52
42
  */
53
43
  export async function disconnect(): Promise<void> {
54
- if (transport) {
55
- await transport.disconnect();
56
- transport = null;
57
- }
44
+ await pool.disconnectAll();
45
+ }
46
+
47
+ /**
48
+ * For tests only — replace the module singleton with a custom pool
49
+ * (typically one constructed with a fake factory). Restores via the
50
+ * returned undo function.
51
+ */
52
+ export function _swapPoolForTests(replacement: TransportPool): () => void {
53
+ const previous = pool;
54
+ pool = replacement;
55
+ return () => {
56
+ pool = previous;
57
+ };
58
58
  }
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "typecheck": "tsc --noEmit",
13
+ "test": "node --test --test-force-exit \"dist/__tests__/*.test.js\"",
13
14
  "smoke-api": "node scripts/smoke-api.mjs",
14
15
  "chat": "node dist/index.js"
15
16
  },
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Phase 1.f — Gemini adapter tests for `mission_cancel` and the
3
+ * mission_id round-trip from `run_mission`.
4
+ *
5
+ * Drives the `executeTool` entry point directly (no network) and
6
+ * verifies:
7
+ * 1. `mission_cancel` returns success / found:false on unknown ids
8
+ * (idempotent, never errors out — safe to spam from the agent).
9
+ * 2. `mission_cancel` rejects empty / missing mission_id with
10
+ * success:false and a useful error message.
11
+ * 3. `run_mission` echoes a mission_id in its compact JSON payload
12
+ * so a sibling `mission_cancel` call can target it.
13
+ * 4. `mission_cancel` is in the function-declaration list.
14
+ *
15
+ * Mirrors the Claude Code + OpenClaw tests; together they pin the
16
+ * cross-adapter contract for Phase 1.f.
17
+ */
18
+
19
+ import { test } from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { executeTool, GEMINI_FUNCTION_DECLARATIONS } from "../tools.js";
22
+ import type { AgenticROSConfig } from "@agenticros/core";
23
+
24
+ function makeHermeticConfig(): AgenticROSConfig {
25
+ // Minimal config that satisfies the Zod schema with defaults. We
26
+ // never reach the transport because mission_cancel + the empty
27
+ // run_mission paths short-circuit before getTransport(). The
28
+ // rosbridge mode + port 1 ensures Phase 1.g goal tests fail FAST
29
+ // (ECONNREFUSED) when a goal compiles to a transport-bound step.
30
+ return {
31
+ transport: { mode: "rosbridge" },
32
+ zenoh: {
33
+ routerEndpoint: "ws://localhost:10000",
34
+ domainId: 0,
35
+ keyFormat: "ros2dds",
36
+ },
37
+ rosbridge: { url: "ws://127.0.0.1:1", reconnect: false, reconnectInterval: 3000 },
38
+ local: { domainId: 0 },
39
+ webrtc: { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] },
40
+ robot: { name: "Test", namespace: "test_robot", cameraTopic: "" },
41
+ safety: {
42
+ maxLinearVelocity: 1,
43
+ maxAngularVelocity: 1.5,
44
+ workspaceLimits: { xMin: -10, xMax: 10, yMin: -10, yMax: 10 },
45
+ },
46
+ teleop: {
47
+ cameraTopic: "",
48
+ cameraTopics: [],
49
+ cmdVelTopic: "",
50
+ speedDefault: 0.3,
51
+ cameraPollMs: 150,
52
+ },
53
+ describer: {
54
+ enabled: false,
55
+ url: "http://localhost:11435/v1/chat/completions",
56
+ model: "qwen2.5vl:7b",
57
+ maxTokens: 400,
58
+ timeoutMs: 60000,
59
+ maxImageDimension: 896,
60
+ },
61
+ memory: {
62
+ enabled: false,
63
+ backend: "local",
64
+ local: { storePath: "~/.agenticros/memory.json" },
65
+ mem0: {
66
+ inferOnWrite: false,
67
+ historyDbPath: "~/.agenticros/memory-history.db",
68
+ },
69
+ },
70
+ skills: {},
71
+ skillPaths: [],
72
+ skillPackages: [],
73
+ robots: [],
74
+ } as unknown as AgenticROSConfig;
75
+ }
76
+
77
+ test("gemini: GEMINI_FUNCTION_DECLARATIONS includes mission_cancel with required mission_id", () => {
78
+ const decl = GEMINI_FUNCTION_DECLARATIONS.find((d) => d.name === "mission_cancel");
79
+ assert.ok(decl, "mission_cancel must be in the function declaration set");
80
+ const schema = decl!.parametersJsonSchema as {
81
+ properties?: Record<string, unknown>;
82
+ required?: string[];
83
+ };
84
+ assert.ok(schema?.properties?.["mission_id"], "must advertise 'mission_id'");
85
+ assert.ok(schema?.properties?.["reason"], "must advertise optional 'reason'");
86
+ assert.deepEqual(schema?.required, ["mission_id"]);
87
+ });
88
+
89
+ test("gemini: mission_cancel on unknown id returns success+found:false (no error, idempotent)", async () => {
90
+ const config = makeHermeticConfig();
91
+ const result = await executeTool(
92
+ "mission_cancel",
93
+ { mission_id: "mn_unknown_xyz" },
94
+ config,
95
+ );
96
+ const payload = JSON.parse(result.output) as {
97
+ success: boolean;
98
+ mission_id: string;
99
+ found: boolean;
100
+ already_cancelled: boolean;
101
+ reason: string | null;
102
+ };
103
+ assert.equal(payload.success, true);
104
+ assert.equal(payload.found, false);
105
+ assert.equal(payload.already_cancelled, false);
106
+ assert.equal(payload.mission_id, "mn_unknown_xyz");
107
+ assert.equal(payload.reason, null);
108
+ });
109
+
110
+ test("gemini: mission_cancel rejects empty mission_id with success:false + useful error", async () => {
111
+ const config = makeHermeticConfig();
112
+ const result = await executeTool("mission_cancel", { mission_id: " " }, config);
113
+ const payload = JSON.parse(result.output) as { success: boolean; error?: string };
114
+ assert.equal(payload.success, false);
115
+ assert.ok(payload.error?.toLowerCase().includes("mission_id"));
116
+ });
117
+
118
+ test("gemini: run_mission echoes a mission_id so a sibling mission_cancel can target it", async () => {
119
+ const config = makeHermeticConfig();
120
+ // Empty steps means we don't dispatch any sub-tool — no transport needed.
121
+ const result = await executeTool(
122
+ "run_mission",
123
+ { mission: { name: "yield mission_id", steps: [] } },
124
+ config,
125
+ );
126
+ // The text payload is "<summary>\n<json>" — pull the JSON line.
127
+ const lines = result.output.split("\n");
128
+ const jsonLine = lines.find((l) => l.trim().startsWith("{")) ?? "";
129
+ const payload = JSON.parse(jsonLine) as {
130
+ mission_id?: string;
131
+ status: string;
132
+ steps: unknown[];
133
+ };
134
+ assert.ok(
135
+ typeof payload.mission_id === "string" && payload.mission_id.startsWith("mn_"),
136
+ `run_mission compact JSON must include a mission_id; got: ${jsonLine}`,
137
+ );
138
+ });
139
+
140
+ // --- Phase 1.g: run_mission { goal } natural-language compile path ---
141
+
142
+ test("gemini: GEMINI_FUNCTION_DECLARATIONS run_mission advertises both 'mission' AND 'goal' (Phase 1.g)", () => {
143
+ const decl = GEMINI_FUNCTION_DECLARATIONS.find((d) => d.name === "run_mission");
144
+ assert.ok(decl);
145
+ const schema = decl!.parametersJsonSchema as {
146
+ properties?: Record<string, unknown>;
147
+ required?: string[];
148
+ };
149
+ assert.ok(schema?.properties?.["mission"], "must still accept 'mission'");
150
+ assert.ok(schema?.properties?.["goal"], "must also accept natural-language 'goal'");
151
+ assert.ok(schema?.properties?.["robot_id"], "must accept top-level robot_id when goal is used");
152
+ // Neither field is individually required — the handler enforces
153
+ // "at least one of mission/goal" at runtime.
154
+ assert.deepEqual(schema?.required ?? [], []);
155
+ });
156
+
157
+ test("gemini: run_mission { goal: 'take a picture' } compiles + echoes planner info (Phase 1.g)", async () => {
158
+ const config = makeHermeticConfig();
159
+ // The compiled mission step (take_snapshot) WILL try to dispatch
160
+ // ros2_camera_snapshot which fails fast against the bogus zenoh
161
+ // endpoint — but the run_mission response still includes the
162
+ // planner info + mission_id, which is what we assert here.
163
+ const result = await executeTool("run_mission", { goal: "take a picture" }, config);
164
+ const lines = result.output.split("\n");
165
+ const jsonLine = lines.find((l) => l.trim().startsWith("{")) ?? "";
166
+ const payload = JSON.parse(jsonLine) as {
167
+ mission_id?: string;
168
+ planner?: {
169
+ compiled_from_goal: string;
170
+ candidates: Array<{ capability_id: string }>;
171
+ };
172
+ steps?: Array<{ capability: string }>;
173
+ };
174
+ assert.equal(payload.planner?.compiled_from_goal, "take a picture");
175
+ assert.equal(payload.planner?.candidates[0].capability_id, "take_snapshot");
176
+ assert.ok(typeof payload.mission_id === "string" && payload.mission_id.startsWith("mn_"));
177
+ });
178
+
179
+ test("gemini: run_mission { goal: 'paint the wall' } surfaces a clean compile error (Phase 1.g)", async () => {
180
+ const config = makeHermeticConfig();
181
+ const result = await executeTool("run_mission", { goal: "paint the wall blue" }, config);
182
+ const payload = JSON.parse(result.output) as {
183
+ success: boolean;
184
+ error?: string;
185
+ suggestions?: string[];
186
+ };
187
+ assert.equal(payload.success, false);
188
+ assert.ok(payload.error);
189
+ assert.ok(Array.isArray(payload.suggestions) && payload.suggestions.length > 0);
190
+ });
191
+
192
+ test("gemini: run_mission with NEITHER mission nor goal returns a clear error (Phase 1.g)", async () => {
193
+ const config = makeHermeticConfig();
194
+ const result = await executeTool("run_mission", {}, config);
195
+ assert.ok(result.output.toLowerCase().includes("mission"));
196
+ assert.ok(result.output.toLowerCase().includes("goal"));
197
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Phase 1.f — Gemini adapter test for mission transcripts.
3
+ *
4
+ * Pins the end-to-end wiring: when `memory.enabled` is true and we
5
+ * `executeTool("run_mission", ...)`, every step the runner executes
6
+ * MUST land in the configured memory provider under the canonical
7
+ * `mission:<id>` namespace. A second agent (or the same agent
8
+ * later) can then call `memory_recall({ namespace: "mission:<id>"
9
+ * })` to read the full timeline.
10
+ *
11
+ * The test:
12
+ * 1. Spins up a hermetic config with the local-JSON memory
13
+ * backend pointing at a tmp file.
14
+ * 2. Runs a mission with one step whose capability isn't in the
15
+ * registry — the runner records an "error" step (no transport
16
+ * needed) AND emits a transcript entry for it.
17
+ * 3. Asserts the transcript landed in memory under
18
+ * `mission:<id>`, with the expected tags + content.
19
+ *
20
+ * Mirrors the cross-adapter contract; the Claude Code and OpenClaw
21
+ * adapters share the same wiring (see ros2-mission.ts / tools.ts).
22
+ */
23
+
24
+ import { test } from "node:test";
25
+ import assert from "node:assert/strict";
26
+ import { mkdtemp, rm } from "node:fs/promises";
27
+ import { tmpdir } from "node:os";
28
+ import path from "node:path";
29
+ import { executeTool } from "../tools.js";
30
+ import { createMemory } from "@agenticros/core";
31
+ import type { AgenticROSConfig } from "@agenticros/core";
32
+
33
+ function makeMemoryConfig(storePath: string): AgenticROSConfig {
34
+ return {
35
+ transport: { mode: "zenoh" },
36
+ zenoh: {
37
+ routerEndpoint: "ws://localhost:10000",
38
+ domainId: 0,
39
+ keyFormat: "ros2dds",
40
+ },
41
+ rosbridge: { url: "ws://localhost:9090", reconnect: true, reconnectInterval: 3000 },
42
+ local: { domainId: 0 },
43
+ webrtc: { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] },
44
+ robot: { name: "Test", namespace: "test_robot", cameraTopic: "" },
45
+ safety: {
46
+ maxLinearVelocity: 1,
47
+ maxAngularVelocity: 1.5,
48
+ workspaceLimits: { xMin: -10, xMax: 10, yMin: -10, yMax: 10 },
49
+ },
50
+ teleop: {
51
+ cameraTopic: "",
52
+ cameraTopics: [],
53
+ cmdVelTopic: "",
54
+ speedDefault: 0.3,
55
+ cameraPollMs: 150,
56
+ },
57
+ describer: {
58
+ enabled: false,
59
+ url: "http://localhost:11435/v1/chat/completions",
60
+ model: "qwen2.5vl:7b",
61
+ maxTokens: 400,
62
+ timeoutMs: 60000,
63
+ maxImageDimension: 896,
64
+ },
65
+ memory: {
66
+ enabled: true,
67
+ backend: "local",
68
+ local: { storePath },
69
+ mem0: {
70
+ inferOnWrite: false,
71
+ historyDbPath: "~/.agenticros/memory-history.db",
72
+ },
73
+ },
74
+ skills: {},
75
+ skillPaths: [],
76
+ skillPackages: [],
77
+ robots: [],
78
+ } as unknown as AgenticROSConfig;
79
+ }
80
+
81
+ test("gemini transcripts: run_mission writes per-step transcripts under mission:<id> namespace", async () => {
82
+ const dir = await mkdtemp(path.join(tmpdir(), "agenticros-gemini-transcripts-"));
83
+ const storePath = path.join(dir, "memory.json");
84
+ try {
85
+ const config = makeMemoryConfig(storePath);
86
+
87
+ // Sanity check: the local provider initialises cleanly so we can
88
+ // attribute any later failure to the adapter wiring rather than
89
+ // the backend itself.
90
+ const probe = await createMemory(config);
91
+ assert.ok(probe, "local memory provider must initialise from hermetic config");
92
+
93
+ // Run a mission with one step whose capability isn't in the
94
+ // registry — the runner emits an "error" step result + transcript
95
+ // entry for it. No transport / no sub-tool dispatch needed.
96
+ const result = await executeTool(
97
+ "run_mission",
98
+ {
99
+ mission: {
100
+ name: "transcript wiring smoke test",
101
+ steps: [
102
+ { id: "noop", capability: "this_capability_does_not_exist" },
103
+ ],
104
+ },
105
+ },
106
+ config,
107
+ );
108
+
109
+ const jsonLine = result.output.split("\n").find((l) => l.trim().startsWith("{")) ?? "";
110
+ const payload = JSON.parse(jsonLine) as {
111
+ mission_id?: string;
112
+ transcript_namespace?: string;
113
+ steps: Array<{ status: string }>;
114
+ };
115
+ assert.ok(payload.mission_id, "run_mission must echo mission_id when memory is enabled");
116
+ assert.equal(
117
+ payload.transcript_namespace,
118
+ `mission:${payload.mission_id}`,
119
+ "compact result must surface the transcript namespace so the agent can recall",
120
+ );
121
+
122
+ // The transcript sink is fire-and-forget; runMission returns
123
+ // before the on-disk write necessarily settles. Poll the file
124
+ // directly (the local backend's atomic-rename guarantees a
125
+ // self-consistent snapshot once memory.json exists), so we don't
126
+ // race the writer. Creating a fresh MemoryProvider per poll
127
+ // sidesteps the per-instance load() cache the local backend
128
+ // keeps around (it never reloads after the first read).
129
+ const ns = `mission:${payload.mission_id}`;
130
+ const deadline = Date.now() + 2000;
131
+ let recordCount = 0;
132
+ let reader = null as Awaited<ReturnType<typeof createMemory>>;
133
+ while (Date.now() < deadline) {
134
+ reader = await createMemory(config);
135
+ const status = await reader!.status(ns);
136
+ recordCount = status.recordCount;
137
+ if (recordCount >= 1) break;
138
+ await new Promise((r) => setTimeout(r, 25));
139
+ }
140
+ assert.ok(recordCount >= 1, `expected >=1 transcript entry under ${ns}, got ${recordCount}`);
141
+
142
+ // Use a meaningful query token (the step id "noop" appears
143
+ // verbatim inside the serialized transcript JSON) so the
144
+ // tokenized scorer in the local backend returns hits — not the
145
+ // hit-everything empty-query path.
146
+ const hits = await reader!.recall({ query: "noop", namespace: ns, limit: 10 });
147
+ assert.ok(hits.length >= 1, `recall by content should return >=1 hit, got ${hits.length}`);
148
+
149
+ // Every recalled entry should carry the transcript tags so a
150
+ // future filter-by-tag UI can locate them efficiently.
151
+ const tagsSeen = new Set<string>();
152
+ for (const h of hits) {
153
+ for (const t of h.tags ?? []) tagsSeen.add(t);
154
+ }
155
+ assert.ok(tagsSeen.has("mission_transcript"), `tags should include 'mission_transcript'; got: ${[...tagsSeen].join(", ")}`);
156
+
157
+ // And the content should be JSON-parseable, carrying the
158
+ // mission_id back out the other side (round-trip). The sink
159
+ // packs the per-step snapshot under `step` (not `result`) — see
160
+ // packages/core/src/mission-transcript-sink.ts.
161
+ const firstContent = JSON.parse(hits[0].content) as {
162
+ mission_id: string;
163
+ adapter?: string;
164
+ step: { id: string; status: string; capability: string };
165
+ };
166
+ assert.equal(firstContent.mission_id, payload.mission_id);
167
+ assert.equal(firstContent.adapter, "gemini", "transcript should record the originating adapter");
168
+ assert.equal(firstContent.step.id, "noop");
169
+ assert.equal(firstContent.step.status, "error");
170
+ assert.equal(firstContent.step.capability, "this_capability_does_not_exist");
171
+ } finally {
172
+ // The transcript sink writes are fire-and-forget — give any
173
+ // still-in-flight atomic rename (tmp → final) a moment to settle
174
+ // before we rmdir, otherwise ENOTEMPTY trips the cleanup.
175
+ await new Promise((r) => setTimeout(r, 250));
176
+ await rm(dir, { recursive: true, force: true });
177
+ }
178
+ });