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
@@ -0,0 +1,574 @@
1
+ /**
2
+ * Mission runner — Phase 1.c of the AgenticROS strategy.
3
+ *
4
+ * A mission is a declarative sequence of capability invocations. Each step
5
+ * names a capability (from the registry exposed by `ros2_list_capabilities`)
6
+ * with literal or templated inputs, where templates can reference outputs
7
+ * from earlier steps via `{{stepId.outputs.fieldName}}`.
8
+ *
9
+ * The runner here is transport-agnostic — it takes a `dispatcher` callback
10
+ * that knows how to invoke a single tool for the host adapter. Adapters
11
+ * pass their existing tool dispatch path (handleToolCall in claude-code,
12
+ * tool.execute() in OpenClaw, executeTool() in Gemini), and the runner
13
+ * stays the same across all three.
14
+ *
15
+ * Phase 1.d adds multi-robot routing: `mission.robot_id` (optional) and
16
+ * per-step `inputs.robot_id` are auto-injected into every dispatched
17
+ * tool call when the binding's tool args don't already specify one. The
18
+ * adapter-side tool handlers then resolve the robot via
19
+ * `resolveRobotFromArgs(config, args)` and route the underlying
20
+ * publish/subscribe/etc. through that robot's namespace.
21
+ *
22
+ * Precedence (highest wins):
23
+ * 1. Tool args produced by the binding (`binding.buildArgs(inputs)`)
24
+ * — if it sets `robot_id`, that's final.
25
+ * 2. Per-step `inputs.robot_id` (lets a mission mix robots step-by-step).
26
+ * 3. Mission-level `mission.robot_id` (the default for every step).
27
+ * 4. None — the adapter falls back to the active robot.
28
+ *
29
+ * Phase 1.f extends the runner with two additive capabilities:
30
+ * - `cancellation`: a token the runner checks BEFORE each step. The
31
+ * adapter holds the token in its `MissionRegistry` so a sibling
32
+ * tool call (`mission_cancel(mission_id)`) can flip
33
+ * `cancellation.cancelled` mid-run and stop the mission gracefully.
34
+ * Steps that have already completed keep their results; the current
35
+ * step finishes naturally (the runner doesn't preempt
36
+ * `await dispatcher(...)` because that requires per-tool cancel
37
+ * support — out of scope for Phase 1.f); subsequent steps are
38
+ * marked `"cancelled"` and skipped.
39
+ * - `transcript`: a callback invoked after every step with the
40
+ * step's `MissionStepResult`. Adapters wire this to the shared
41
+ * memory subsystem so a second agent can `recall(namespace=
42
+ * "mission:<id>")` and inspect what's run so far. The runner
43
+ * itself owns no I/O — the callback can be a no-op when memory
44
+ * is disabled.
45
+ *
46
+ * What's still deferred:
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).
50
+ * - Retry / backoff policies.
51
+ *
52
+ * See: docs/strategy-ai-agents-plus-ros.md §4 (Phase 1.c / 1.f).
53
+ */
54
+
55
+ import type { Capability } from "./capabilities.js";
56
+
57
+ /**
58
+ * Cancellation token consumed by `runMission`.
59
+ *
60
+ * Plain object (not AbortController) so it's easy to share across
61
+ * 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.
64
+ */
65
+ export interface MissionCancellationToken {
66
+ cancelled: boolean;
67
+ /** Optional free-text reason — bubbled up into the cancelled step results. */
68
+ reason?: string;
69
+ }
70
+
71
+ /**
72
+ * Per-step transcript sink. Called immediately after a step finishes
73
+ * (including cancelled / skipped steps) so an external store sees the
74
+ * whole timeline, not just the post-mortem `MissionResult`.
75
+ *
76
+ * The callback is best-effort: thrown errors are logged but never
77
+ * propagate up — losing a transcript entry must not abort an
78
+ * otherwise-healthy mission.
79
+ */
80
+ export type MissionTranscriptSink = (
81
+ entry: MissionTranscriptEntry,
82
+ ) => Promise<void> | void;
83
+
84
+ /** One transcript entry per executed step. */
85
+ export interface MissionTranscriptEntry {
86
+ /** Unique id of the running mission (assigned by the adapter, not the runner). */
87
+ mission_id: string;
88
+ /** Free-text mission label. */
89
+ mission_name?: string;
90
+ /** Adapter that ran the mission (e.g. "claude-code", "openclaw", "gemini"). */
91
+ adapter?: string;
92
+ /** When the step started (ms since epoch). */
93
+ started_at: number;
94
+ /** Mission-level robot id (empty when unset). */
95
+ robot_id?: string;
96
+ /** Index of this step in mission.steps (0-based). */
97
+ step_index: number;
98
+ /** Total steps in the mission. */
99
+ step_total: number;
100
+ /** Snapshot of the per-step result. */
101
+ result: MissionStepResult;
102
+ }
103
+
104
+ /** One step in a mission. */
105
+ export interface MissionStep {
106
+ /** Unique id within the mission, used by later steps for `{{id.outputs.x}}`. */
107
+ id: string;
108
+ /**
109
+ * Capability id (e.g. `drive_base`, `find_object`). The runner looks this
110
+ * up in the capability registry and uses a dispatch map to find the
111
+ * underlying tool to invoke.
112
+ */
113
+ capability: string;
114
+ /**
115
+ * Inputs to the capability. Values can be literals or `{{stepId.outputs.x}}`
116
+ * templates that resolve from prior steps' outputs.
117
+ */
118
+ inputs?: Record<string, unknown>;
119
+ /**
120
+ * Optional behaviour when this step fails. Defaults to "stop".
121
+ * - "stop": halt the mission, mark it failed
122
+ * - "continue": record the failure and run the next step anyway
123
+ */
124
+ on_fail?: "stop" | "continue";
125
+ }
126
+
127
+ /** A complete mission plan. */
128
+ export interface Mission {
129
+ /** Free-text label for logs / chat replies (e.g. "find chair and approach"). */
130
+ name?: string;
131
+ /**
132
+ * Optional planning notes from the agent — surfaced in the result for
133
+ * downstream summarisation. The runner ignores it.
134
+ */
135
+ goal?: string;
136
+ /**
137
+ * Optional default robot id for every step. Each step's tool args get
138
+ * `robot_id` auto-injected from this when the step doesn't supply its
139
+ * own and the binding's tool args don't already include one. Unknown
140
+ * robot ids surface as a tool error from the adapter, not from the
141
+ * runner. Empty/whitespace is ignored (uses the active robot).
142
+ */
143
+ robot_id?: string;
144
+ steps: MissionStep[];
145
+ }
146
+
147
+ /** Result of a single step. */
148
+ export interface MissionStepResult {
149
+ id: string;
150
+ capability: string;
151
+ /**
152
+ * Final step outcome:
153
+ * - "ok": tool returned cleanly, outputs (if any) parsed.
154
+ * - "error": tool returned an error or the binding/build threw.
155
+ * - "skipped": earlier step failed with on_fail=stop.
156
+ * - "cancelled": mission was cancelled (Phase 1.f) before this step ran.
157
+ */
158
+ status: "ok" | "error" | "skipped" | "cancelled";
159
+ /** Resolved inputs (with `{{...}}` templates substituted). */
160
+ inputs: Record<string, unknown>;
161
+ /** Outputs parsed from the tool response (may be `undefined` for fire-and-forget). */
162
+ outputs?: Record<string, unknown>;
163
+ /** Free-form text the tool returned (e.g. for human display). */
164
+ message?: string;
165
+ /** Error message when status is "error". */
166
+ error?: string;
167
+ /** Wall time in ms. */
168
+ duration_ms: number;
169
+ }
170
+
171
+ /** Result of a mission run. */
172
+ export interface MissionResult {
173
+ /**
174
+ * Overall mission outcome:
175
+ * - "ok": every step succeeded (or skipped on_fail=continue).
176
+ * - "error": one or more steps errored with on_fail=stop.
177
+ * - "cancelled": mission was cancelled mid-run (Phase 1.f). Some
178
+ * steps may have completed successfully — see steps[].
179
+ */
180
+ status: "ok" | "error" | "cancelled";
181
+ /** Number of steps that ran (skipped/cancelled steps are NOT counted as run). */
182
+ steps_run: number;
183
+ steps_total: number;
184
+ /** Per-step results, in declaration order. */
185
+ steps: MissionStepResult[];
186
+ /** Total wall time across all steps. */
187
+ duration_ms: number;
188
+ /** Brief one-line summary suitable for chat reply. */
189
+ summary: string;
190
+ /**
191
+ * Mission-level default robot id used during this run (surfaced for
192
+ * traceability — useful in chat replies and logs). Empty string when
193
+ * the mission didn't pin a robot.
194
+ */
195
+ robot_id?: string;
196
+ /**
197
+ * Phase 1.f — the mission id the adapter registered before running.
198
+ * Empty when the adapter didn't pass an id (e.g. ad-hoc dispatcher
199
+ * tests). Use this with `mission_cancel(mission_id)` to abort a
200
+ * mission mid-run, or `memory_recall(namespace="mission:<id>")` to
201
+ * read the transcript later.
202
+ */
203
+ mission_id?: string;
204
+ /**
205
+ * Phase 1.f — set when the mission was cancelled. Carries the free-text
206
+ * reason the canceller supplied (or "cancelled" when none was given).
207
+ */
208
+ cancellation_reason?: string;
209
+ }
210
+
211
+ /**
212
+ * A dispatcher invokes a single MCP tool by name and returns:
213
+ * - `text`: the tool's text response (free-form, for human display)
214
+ * - `outputs`: optional structured outputs (parsed JSON when the text
215
+ * payload is a JSON blob). The runner uses these for
216
+ * `{{...}}` template resolution by later steps.
217
+ *
218
+ * Adapters wrap their existing tool entry points to satisfy this contract.
219
+ */
220
+ export type MissionToolDispatcher = (
221
+ toolName: string,
222
+ args: Record<string, unknown>,
223
+ ) => Promise<{ text: string; outputs?: Record<string, unknown>; isError?: boolean }>;
224
+
225
+ /**
226
+ * Capability → MCP tool mapping. Each entry says "to satisfy capability X,
227
+ * call tool Y with these arguments". The transform receives the step's
228
+ * (already template-resolved) inputs and returns the args object the tool
229
+ * expects.
230
+ *
231
+ * Why a separate map (not a field on the capability itself)?
232
+ * - Some capabilities map onto different tools per adapter
233
+ * (claude-code's `ros2_find_object` is a single tool call; the OpenClaw
234
+ * adapter equivalent runs in a different process).
235
+ * - Skill-declared capabilities will eventually carry their own
236
+ * implementation hint (`implementation.kind = "in_process"|"external_ros_node"`),
237
+ * but until that wire-up is real, mapping happens here.
238
+ */
239
+ export interface CapabilityToolBinding {
240
+ /** The MCP tool name to invoke. */
241
+ tool: string;
242
+ /** Transform the step's resolved inputs into the tool's expected arguments. */
243
+ buildArgs: (inputs: Record<string, unknown>) => Record<string, unknown>;
244
+ /**
245
+ * Optional: extract structured outputs from the tool's text response
246
+ * for `{{...}}` template references by later steps. When omitted, the
247
+ * runner attempts to JSON-parse the text and use the result.
248
+ */
249
+ parseOutputs?: (text: string) => Record<string, unknown> | undefined;
250
+ }
251
+
252
+ export type CapabilityToolBindings = Record<string, CapabilityToolBinding>;
253
+
254
+ const TEMPLATE_RE = /\{\{\s*([a-zA-Z0-9_]+)\.outputs\.([a-zA-Z0-9_]+)\s*\}\}/g;
255
+
256
+ /**
257
+ * Substitute `{{stepId.outputs.field}}` references in `value` using the
258
+ * given step output map. Recurses into nested objects/arrays.
259
+ *
260
+ * String values containing exactly one template reference are replaced with
261
+ * the raw value (preserving type — number stays number, etc.). Strings with
262
+ * interpolated text get string-substituted.
263
+ */
264
+ function resolveTemplates(
265
+ value: unknown,
266
+ outputs: Record<string, Record<string, unknown> | undefined>,
267
+ ): unknown {
268
+ if (typeof value === "string") {
269
+ const matches = [...value.matchAll(TEMPLATE_RE)];
270
+ if (matches.length === 0) return value;
271
+ // Whole-string single template: preserve type.
272
+ if (matches.length === 1) {
273
+ const m = matches[0];
274
+ if (m[0] === value.trim()) {
275
+ const stepOuts = outputs[m[1]];
276
+ return stepOuts ? stepOuts[m[2]] : undefined;
277
+ }
278
+ }
279
+ return value.replace(TEMPLATE_RE, (_, stepId: string, field: string) => {
280
+ const stepOuts = outputs[stepId];
281
+ const v = stepOuts ? stepOuts[field] : undefined;
282
+ return v === undefined ? "" : String(v);
283
+ });
284
+ }
285
+ if (Array.isArray(value)) return value.map((v) => resolveTemplates(v, outputs));
286
+ if (value && typeof value === "object") {
287
+ const out: Record<string, unknown> = {};
288
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
289
+ out[k] = resolveTemplates(v, outputs);
290
+ }
291
+ return out;
292
+ }
293
+ return value;
294
+ }
295
+
296
+ function tryParseJsonOutputs(text: string): Record<string, unknown> | undefined {
297
+ // Tools often return "Summary line.\n{json}" — try parsing each line plus
298
+ // the whole payload, returning the first JSON object we find.
299
+ const candidates = [text, text.trim(), text.split("\n").pop() ?? ""];
300
+ for (const c of candidates) {
301
+ if (!c || !c.trim().startsWith("{")) continue;
302
+ try {
303
+ const parsed = JSON.parse(c);
304
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
305
+ return parsed as Record<string, unknown>;
306
+ }
307
+ } catch {
308
+ }
309
+ }
310
+ return undefined;
311
+ }
312
+
313
+ /** Phase 1.f optional behaviour bundle for `runMission`. */
314
+ export interface RunMissionOptions {
315
+ /**
316
+ * Stable identifier for this mission run. Adapters generate it and
317
+ * register a cancellation token under this key, so a sibling
318
+ * `mission_cancel(mission_id)` tool call can flip the token mid-run.
319
+ * Surfaced in the result + every transcript entry.
320
+ */
321
+ mission_id?: string;
322
+ /**
323
+ * Cancellation token the runner checks BEFORE each step. When
324
+ * `cancelled` is true at a step boundary, the remaining steps are
325
+ * marked "cancelled" and the mission returns with
326
+ * `status: "cancelled"`.
327
+ */
328
+ cancellation?: MissionCancellationToken;
329
+ /**
330
+ * Best-effort transcript sink — called after every step finishes
331
+ * (including skipped/cancelled). Adapters wire this to the memory
332
+ * subsystem so a downstream agent can read the timeline. Thrown
333
+ * errors are swallowed; transcripts are not gated by the mission's
334
+ * success.
335
+ */
336
+ transcript?: MissionTranscriptSink;
337
+ /** Adapter label (e.g. "claude-code", "openclaw", "gemini") — copied into transcripts. */
338
+ adapter?: string;
339
+ }
340
+
341
+ /**
342
+ * Execute a mission sequentially.
343
+ *
344
+ * @param mission The declarative plan.
345
+ * @param capabilities Capability registry — used to validate that each
346
+ * requested capability id is real.
347
+ * @param bindings Capability → tool mapping; entries missing here yield
348
+ * an "unsupported capability" error for that step.
349
+ * @param dispatcher Adapter-supplied function that invokes a tool by name.
350
+ * @param options Phase 1.f options (cancellation token, transcript sink, mission id).
351
+ * Omit for legacy callers.
352
+ */
353
+ export async function runMission(
354
+ mission: Mission,
355
+ capabilities: Capability[],
356
+ bindings: CapabilityToolBindings,
357
+ dispatcher: MissionToolDispatcher,
358
+ options?: RunMissionOptions,
359
+ ): Promise<MissionResult> {
360
+ const capIds = new Set(capabilities.map((c) => c.id));
361
+ const stepOutputs: Record<string, Record<string, unknown> | undefined> = {};
362
+ const results: MissionStepResult[] = [];
363
+ const t0Mission = Date.now();
364
+ let aborted = false;
365
+ let cancelled = false;
366
+
367
+ // Phase 1.f — best-effort transcript emit. Synchronous helper so the
368
+ // mission loop stays linear; the caller's sink may itself be async
369
+ // but we don't block the next step on it.
370
+ const emitTranscript = (idx: number, started_at: number, result: MissionStepResult): void => {
371
+ if (!options?.transcript) return;
372
+ try {
373
+ const entry: MissionTranscriptEntry = {
374
+ mission_id: options.mission_id ?? "",
375
+ mission_name: mission.name,
376
+ adapter: options.adapter,
377
+ started_at,
378
+ robot_id: typeof mission.robot_id === "string" ? mission.robot_id : "",
379
+ step_index: idx,
380
+ step_total: mission.steps.length,
381
+ result,
382
+ };
383
+ // Fire-and-forget — never throw out of the runner. We catch
384
+ // synchronous throws AND attach a no-op .catch on the promise so
385
+ // a rejected async sink doesn't surface as an unhandled rejection.
386
+ const ret = options.transcript(entry);
387
+ if (ret && typeof (ret as Promise<unknown>).catch === "function") {
388
+ (ret as Promise<unknown>).catch(() => {});
389
+ }
390
+ } catch {
391
+ // Transcript loss never breaks the mission.
392
+ }
393
+ };
394
+
395
+ for (let idx = 0; idx < mission.steps.length; idx++) {
396
+ const step = mission.steps[idx];
397
+ const t0 = Date.now();
398
+
399
+ // Phase 1.f — honour cancellation token at the step boundary. We
400
+ // check BEFORE the step runs so that a cancel arriving while
401
+ // step N is in-flight only affects step N+1 onward (per-tool
402
+ // preemption is out of scope; see the module header).
403
+ if (!cancelled && options?.cancellation?.cancelled) {
404
+ cancelled = true;
405
+ }
406
+
407
+ if (cancelled) {
408
+ const result: MissionStepResult = {
409
+ id: step.id,
410
+ capability: step.capability,
411
+ status: "cancelled",
412
+ inputs: {},
413
+ message: `Cancelled: ${options?.cancellation?.reason ?? "cancelled"}`,
414
+ duration_ms: 0,
415
+ };
416
+ results.push(result);
417
+ emitTranscript(idx, t0, result);
418
+ continue;
419
+ }
420
+
421
+ if (aborted) {
422
+ const result: MissionStepResult = {
423
+ id: step.id,
424
+ capability: step.capability,
425
+ status: "skipped",
426
+ inputs: {},
427
+ message: "Skipped: earlier step failed and on_fail=stop.",
428
+ duration_ms: 0,
429
+ };
430
+ results.push(result);
431
+ emitTranscript(idx, t0, result);
432
+ continue;
433
+ }
434
+
435
+ if (!capIds.has(step.capability)) {
436
+ const result: MissionStepResult = {
437
+ id: step.id,
438
+ capability: step.capability,
439
+ status: "error",
440
+ inputs: step.inputs ?? {},
441
+ error: `Capability "${step.capability}" not found in registry. Use ros2_list_capabilities to see what's available.`,
442
+ duration_ms: Date.now() - t0,
443
+ };
444
+ results.push(result);
445
+ emitTranscript(idx, t0, result);
446
+ if ((step.on_fail ?? "stop") === "stop") aborted = true;
447
+ continue;
448
+ }
449
+
450
+ const binding = bindings[step.capability];
451
+ if (!binding) {
452
+ const result: MissionStepResult = {
453
+ id: step.id,
454
+ capability: step.capability,
455
+ status: "error",
456
+ inputs: step.inputs ?? {},
457
+ error: `Capability "${step.capability}" is registered but has no mission-runner tool binding yet. See docs/strategy-ai-agents-plus-ros.md §4 Phase 1.c.`,
458
+ duration_ms: Date.now() - t0,
459
+ };
460
+ results.push(result);
461
+ emitTranscript(idx, t0, result);
462
+ if ((step.on_fail ?? "stop") === "stop") aborted = true;
463
+ continue;
464
+ }
465
+
466
+ const resolvedInputs = (resolveTemplates(step.inputs ?? {}, stepOutputs) as Record<string, unknown>) ?? {};
467
+
468
+ let toolArgs: Record<string, unknown>;
469
+ try {
470
+ toolArgs = binding.buildArgs(resolvedInputs);
471
+ } catch (err) {
472
+ const result: MissionStepResult = {
473
+ id: step.id,
474
+ capability: step.capability,
475
+ status: "error",
476
+ inputs: resolvedInputs,
477
+ error: `Failed to build args for tool ${binding.tool}: ${err instanceof Error ? err.message : String(err)}`,
478
+ duration_ms: Date.now() - t0,
479
+ };
480
+ results.push(result);
481
+ emitTranscript(idx, t0, result);
482
+ if ((step.on_fail ?? "stop") === "stop") aborted = true;
483
+ continue;
484
+ }
485
+
486
+ // Inject robot_id when the binding didn't set one. Per-step inputs
487
+ // take precedence over the mission-level default; empty strings
488
+ // mean "use the active robot" so we ignore them. This is what
489
+ // makes per-tool fleet routing work end-to-end from `run_mission`.
490
+ if (!("robot_id" in toolArgs) || typeof toolArgs.robot_id !== "string" || toolArgs.robot_id.trim().length === 0) {
491
+ const stepRid = typeof resolvedInputs.robot_id === "string" ? resolvedInputs.robot_id.trim() : "";
492
+ const missionRid = typeof mission.robot_id === "string" ? mission.robot_id.trim() : "";
493
+ const effective = stepRid || missionRid;
494
+ if (effective) toolArgs.robot_id = effective;
495
+ }
496
+
497
+ try {
498
+ const dispatched = await dispatcher(binding.tool, toolArgs);
499
+ const outputs =
500
+ dispatched.outputs ??
501
+ binding.parseOutputs?.(dispatched.text) ??
502
+ tryParseJsonOutputs(dispatched.text);
503
+ stepOutputs[step.id] = outputs;
504
+ const status: MissionStepResult["status"] = dispatched.isError ? "error" : "ok";
505
+ const result: MissionStepResult = {
506
+ id: step.id,
507
+ capability: step.capability,
508
+ status,
509
+ inputs: resolvedInputs,
510
+ outputs,
511
+ message: dispatched.text,
512
+ ...(dispatched.isError ? { error: dispatched.text } : {}),
513
+ duration_ms: Date.now() - t0,
514
+ };
515
+ results.push(result);
516
+ emitTranscript(idx, t0, result);
517
+ if (status === "error" && (step.on_fail ?? "stop") === "stop") aborted = true;
518
+ } catch (err) {
519
+ const msg = err instanceof Error ? err.message : String(err);
520
+ const result: MissionStepResult = {
521
+ id: step.id,
522
+ capability: step.capability,
523
+ status: "error",
524
+ inputs: resolvedInputs,
525
+ error: msg,
526
+ duration_ms: Date.now() - t0,
527
+ };
528
+ results.push(result);
529
+ emitTranscript(idx, t0, result);
530
+ if ((step.on_fail ?? "stop") === "stop") aborted = true;
531
+ }
532
+ }
533
+
534
+ // "Ran" counts steps that actually executed — skipped (on_fail=stop)
535
+ // and cancelled (Phase 1.f) are explicitly excluded.
536
+ const ran = results.filter((r) => r.status !== "skipped" && r.status !== "cancelled").length;
537
+ const failed = results.filter((r) => r.status === "error").length;
538
+ let overallStatus: MissionResult["status"] = failed === 0 ? "ok" : "error";
539
+ if (cancelled) overallStatus = "cancelled";
540
+ const summary = summariseMission(mission, results, overallStatus);
541
+
542
+ return {
543
+ status: overallStatus,
544
+ steps_run: ran,
545
+ steps_total: mission.steps.length,
546
+ steps: results,
547
+ duration_ms: Date.now() - t0Mission,
548
+ summary,
549
+ robot_id: typeof mission.robot_id === "string" ? mission.robot_id : "",
550
+ ...(options?.mission_id ? { mission_id: options.mission_id } : {}),
551
+ ...(cancelled
552
+ ? { cancellation_reason: options?.cancellation?.reason ?? "cancelled" }
553
+ : {}),
554
+ };
555
+ }
556
+
557
+ function summariseMission(
558
+ mission: Mission,
559
+ results: MissionStepResult[],
560
+ status: MissionResult["status"],
561
+ ): string {
562
+ const label = mission.name ?? "Mission";
563
+ const ran = results.filter((r) => r.status !== "skipped" && r.status !== "cancelled").length;
564
+ const cancelled = results.filter((r) => r.status === "cancelled").length;
565
+ const failed = results.filter((r) => r.status === "error");
566
+ if (status === "ok") {
567
+ return `${label}: completed ${ran}/${results.length} step(s) successfully.`;
568
+ }
569
+ if (status === "cancelled") {
570
+ return `${label}: cancelled after ${ran} step(s) — ${cancelled} remaining step(s) skipped.`;
571
+ }
572
+ const first = failed[0];
573
+ return `${label}: failed at step "${first?.id ?? "?"}" (${first?.capability ?? "?"}): ${first?.error ?? "unknown error"}.`;
574
+ }