agenticros 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +18 -3
  2. package/dist/util/skill-manifest.d.ts.map +1 -1
  3. package/dist/util/skill-manifest.js +22 -1
  4. package/dist/util/skill-manifest.js.map +1 -1
  5. package/package.json +1 -1
  6. package/runtime/BUNDLE.json +1 -1
  7. package/runtime/README.md +31 -6
  8. package/runtime/docs/cli.md +1 -1
  9. package/runtime/docs/robot-setup.md +1 -1
  10. package/runtime/packages/agenticros/openclaw.plugin.json +147 -1
  11. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +2 -0
  12. package/runtime/packages/agenticros/src/routes.ts +4 -3
  13. package/runtime/packages/agenticros/src/tools/index.ts +6 -5
  14. package/runtime/packages/agenticros/src/tools/mission-pause.ts +49 -0
  15. package/runtime/packages/agenticros/src/tools/mission-resume.ts +41 -0
  16. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +33 -84
  17. package/runtime/packages/agenticros-claude-code/README.md +2 -0
  18. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  19. package/runtime/packages/agenticros-claude-code/dist/tools.js +130 -111
  20. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  21. package/runtime/packages/agenticros-claude-code/src/tools.ts +140 -99
  22. package/runtime/packages/agenticros-gemini/package.json +2 -0
  23. package/runtime/packages/agenticros-gemini/src/find-object/coco-classes.ts +38 -0
  24. package/runtime/packages/agenticros-gemini/src/find-object/find-object.ts +191 -0
  25. package/runtime/packages/agenticros-gemini/src/follow-me/controller.ts +109 -0
  26. package/runtime/packages/agenticros-gemini/src/follow-me/depth-loop.ts +452 -0
  27. package/runtime/packages/agenticros-gemini/src/follow-me/detector.ts +303 -0
  28. package/runtime/packages/agenticros-gemini/src/follow-me/loop.ts +359 -0
  29. package/runtime/packages/agenticros-gemini/src/tools.ts +351 -90
  30. package/runtime/packages/core/README.md +1 -1
  31. package/runtime/packages/core/package.json +1 -1
  32. package/runtime/packages/core/src/__tests__/config-persistence.test.ts +26 -0
  33. package/runtime/packages/core/src/__tests__/external-capability.test.ts +72 -0
  34. package/runtime/packages/core/src/__tests__/heartbeat-fleet.test.ts +152 -0
  35. package/runtime/packages/core/src/__tests__/mission-bindings.test.ts +101 -0
  36. package/runtime/packages/core/src/__tests__/mission-pause.test.ts +104 -0
  37. package/runtime/packages/core/src/capabilities.ts +38 -3
  38. package/runtime/packages/core/src/capability-schema.ts +58 -0
  39. package/runtime/packages/core/src/config.ts +20 -0
  40. package/runtime/packages/core/src/discovery.ts +21 -3
  41. package/runtime/packages/core/src/external-capability.ts +219 -0
  42. package/runtime/packages/core/src/fleet-config.ts +91 -0
  43. package/runtime/packages/core/src/heartbeat.ts +167 -0
  44. package/runtime/packages/core/src/index.ts +55 -0
  45. package/runtime/packages/core/src/mission-bindings.ts +189 -0
  46. package/runtime/packages/core/src/mission-registry.ts +31 -3
  47. package/runtime/packages/core/src/mission.ts +64 -7
  48. package/runtime/packages/core/src/robots.ts +6 -2
  49. package/runtime/packages/core/src/transport/rosbridge/client.ts +7 -0
  50. package/runtime/pnpm-lock.yaml +6 -0
  51. package/templates/skills/camera/package.json +1 -1
  52. package/templates/skills/depth/package.json +1 -1
  53. package/templates/skills/robot/package.json +1 -1
@@ -22,7 +22,6 @@ import { Type } from "@sinclair/typebox";
22
22
  import type { AgentTool, OpenClawPluginApi } from "../plugin-api.js";
23
23
  import type {
24
24
  AgenticROSConfig,
25
- CapabilityToolBindings,
26
25
  Mission,
27
26
  MissionToolDispatcher,
28
27
  } from "@agenticros/core";
@@ -33,10 +32,15 @@ import {
33
32
  createMemoryTranscriptSink,
34
33
  missionTranscriptNamespace,
35
34
  compileGoalToMission,
35
+ buildMissionBindings,
36
+ isExternalToolName,
37
+ capabilityIdFromExternalTool,
38
+ executeExternalCapability,
36
39
  } from "@agenticros/core";
37
40
  import { resolveRobotForTool } from "./_robot-helpers.js";
38
41
  import { getMissionRegistry } from "../mission-registry.js";
39
42
  import { getMemory } from "../memory.js";
43
+ import { getTransportForRobot } from "../service.js";
40
44
 
41
45
  /**
42
46
  * Snapshot of the registered tools, keyed by tool name. Built in
@@ -45,85 +49,6 @@ import { getMemory } from "../memory.js";
45
49
  */
46
50
  export type ToolRegistry = Map<string, AgentTool>;
47
51
 
48
- const MISSION_BINDINGS: CapabilityToolBindings = {
49
- drive_base: {
50
- tool: "ros2_publish",
51
- buildArgs: (inputs) => {
52
- const lx = Number(inputs.linear_x ?? 0) || 0;
53
- const az = Number(inputs.angular_z ?? 0) || 0;
54
- return {
55
- topic: "/cmd_vel",
56
- type: "geometry_msgs/msg/Twist",
57
- message: {
58
- linear: { x: lx, y: 0, z: 0 },
59
- angular: { x: 0, y: 0, z: az },
60
- },
61
- };
62
- },
63
- },
64
- take_snapshot: {
65
- tool: "ros2_camera_snapshot",
66
- buildArgs: (inputs) => {
67
- const out: Record<string, unknown> = {};
68
- if (typeof inputs.topic === "string") out.topic = inputs.topic;
69
- if (typeof inputs.message_type === "string") out.message_type = inputs.message_type;
70
- if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
71
- return out;
72
- },
73
- },
74
- measure_depth: {
75
- tool: "ros2_depth_distance",
76
- buildArgs: (inputs) => {
77
- const out: Record<string, unknown> = {};
78
- if (typeof inputs.topic === "string") out.topic = inputs.topic;
79
- if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
80
- return out;
81
- },
82
- },
83
- list_topics: {
84
- tool: "ros2_list_topics",
85
- buildArgs: () => ({}),
86
- },
87
- publish_topic: {
88
- tool: "ros2_publish",
89
- buildArgs: (inputs) => ({
90
- topic: String(inputs.topic ?? ""),
91
- type: String(inputs.type ?? inputs.msg_type ?? ""),
92
- message: inputs.message ?? inputs.msg ?? {},
93
- }),
94
- },
95
- subscribe_once: {
96
- tool: "ros2_subscribe_once",
97
- buildArgs: (inputs) => {
98
- const out: Record<string, unknown> = { topic: String(inputs.topic ?? "") };
99
- if (typeof inputs.type === "string") out.type = inputs.type;
100
- if (typeof inputs.timeout === "number") out.timeout = inputs.timeout;
101
- return out;
102
- },
103
- },
104
- follow_person: {
105
- tool: "ros2_follow_me_start",
106
- buildArgs: (inputs) => {
107
- const out: Record<string, unknown> = {};
108
- if (typeof inputs.target_distance === "number") out.target_distance = inputs.target_distance;
109
- if (typeof inputs.mode === "string") out.mode = inputs.mode;
110
- return out;
111
- },
112
- },
113
- find_object: {
114
- tool: "ros2_find_object",
115
- buildArgs: (inputs) => {
116
- const target = String(inputs.target ?? "");
117
- const out: Record<string, unknown> = { target };
118
- if (typeof inputs.angular_speed === "number") out.angular_speed = inputs.angular_speed;
119
- if (typeof inputs.clockwise === "boolean") out.clockwise = inputs.clockwise;
120
- if (typeof inputs.timeout_seconds === "number") out.timeout_seconds = inputs.timeout_seconds;
121
- if (typeof inputs.min_confidence === "number") out.min_confidence = inputs.min_confidence;
122
- return out;
123
- },
124
- },
125
- };
126
-
127
52
  export function registerMissionTool(
128
53
  api: OpenClawPluginApi,
129
54
  config: AgenticROSConfig,
@@ -143,9 +68,9 @@ export function registerMissionTool(
143
68
  "provided) the compiled plan + candidate match list so you can see what the planner did. " +
144
69
  "When memory is enabled, every step is also written to the shared memory under namespace " +
145
70
  "mission:<mission_id> so a second agent can recall the timeline via memory_recall. " +
146
- "Today the runner supports: drive_base, take_snapshot, measure_depth, list_topics, " +
147
- "publish_topic, subscribe_once, follow_person, find_object. " +
148
- "Pass mission.robot_id (or top-level robot_id with goal) to target every step at one robot.",
71
+ "Bindings are built from the capability registry (builtins + skill-declared). " +
72
+ "Pass mission.robot_id (or top-level robot_id with goal) to target every step at one robot. " +
73
+ "Use mission_pause / mission_resume / mission_cancel with the returned mission_id.",
149
74
  parameters: Type.Object({
150
75
  goal: Type.Optional(
151
76
  Type.String({
@@ -186,6 +111,13 @@ export function registerMissionTool(
186
111
 
187
112
  async execute(toolCallId, params, signal) {
188
113
  const caps = listAllCapabilities(config);
114
+ const missionBindings = buildMissionBindings(caps, {
115
+ toolNameResolver: (cap) => {
116
+ const preferred = `ros2_${cap.id}`;
117
+ if (registry.has(preferred)) return preferred;
118
+ return undefined;
119
+ },
120
+ });
189
121
  const missionRaw = params["mission"];
190
122
  const goalRaw = params["goal"];
191
123
  const topLevelRobotId = typeof params["robot_id"] === "string" ? (params["robot_id"] as string) : undefined;
@@ -236,6 +168,23 @@ export function registerMissionTool(
236
168
  }
237
169
 
238
170
  const dispatcher: MissionToolDispatcher = async (toolName, toolArgs) => {
171
+ if (isExternalToolName(toolName)) {
172
+ const capId = capabilityIdFromExternalTool(toolName);
173
+ const cap = caps.find((c) => c.id === capId);
174
+ if (!cap) {
175
+ return { text: `Unknown external capability "${capId}".`, isError: true };
176
+ }
177
+ const resolved = resolveRobotForTool(config, toolArgs);
178
+ if ("error" in resolved) {
179
+ const errText = resolved.error.content.map((c) => ("text" in c ? c.text : "")).join("\n");
180
+ return { text: errText, isError: true };
181
+ }
182
+ const transport = await getTransportForRobot(config, resolved.robot);
183
+ const ext = await executeExternalCapability(cap, toolArgs, transport, {
184
+ namespace: resolved.robot.namespace,
185
+ });
186
+ return { text: ext.text, outputs: ext.outputs, isError: ext.isError };
187
+ }
239
188
  const tool = registry.get(toolName);
240
189
  if (!tool) {
241
190
  return {
@@ -273,7 +222,7 @@ export function registerMissionTool(
273
222
 
274
223
  let result;
275
224
  try {
276
- result = await runMission(mission, caps, MISSION_BINDINGS, dispatcher, {
225
+ result = await runMission(mission, caps, missionBindings, dispatcher, {
277
226
  mission_id: missionId,
278
227
  cancellation: regEntry.cancellation,
279
228
  transcript,
@@ -4,6 +4,8 @@ MCP (Model Context Protocol) server that exposes AgenticROS ROS2 tools to **Clau
4
4
 
5
5
  This adapter does **not** provide the config or teleop web UI; use the [OpenClaw plugin](../../packages/agenticros) for that, or run the gateway for the browser-based teleop page.
6
6
 
7
+ **Local models (no cloud API):** Claude and Codex still use cloud LLMs. For **fully local** VLM inference with Ollama, use the [OpenClaw plugin](../../packages/agenticros) or [Hermes Agent](../../docs/hermes-setup.md) — see **[docs/local-vlm.md](../../docs/local-vlm.md)**.
8
+
7
9
  ## Prerequisites
8
10
 
9
11
  - Node.js 20+
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAMjB,MAAM,kBAAkB,CAAC;AAqC1B;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAK5B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,aAmBlC,CAAC;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;QACvF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,KAAK,EAAE,OAAO,EAgX1B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AA6W7G,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAomBxD"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAKjB,MAAM,kBAAkB,CAAC;AAyC1B;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAK5B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,aAsBlC,CAAC;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;QACvF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,KAAK,EAAE,OAAO,EAkZ1B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AA4W7G,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAsmBxD"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MCP tool definitions and handler. Mirrors OpenClaw adapter tools.
3
3
  */
4
- import { resolveCameraSubscribeTopic, toNamespacedTopic, toNamespacedTopicFull, listAllCapabilities, runMission, listRobots, getActiveRobotId, resolveRobotFromArgs, discoverRobots, findRobotsFor, generateMissionId, createMemoryTranscriptSink, missionTranscriptNamespace, compileGoalToMission, } from "@agenticros/core";
4
+ import { resolveCameraSubscribeTopic, toNamespacedTopic, toNamespacedTopicFull, listAllCapabilities, runMission, listRobots, getActiveRobotId, resolveRobotFromArgs, discoverRobots, findRobotsFor, generateMissionId, createMemoryTranscriptSink, missionTranscriptNamespace, compileGoalToMission, buildMissionBindings, isExternalToolName, capabilityIdFromExternalTool, executeExternalCapability, } from "@agenticros/core";
5
5
  import { getMissionRegistry } from "./mission-registry.js";
6
6
  import { ROS_MSG_COMPRESSED_IMAGE, ROS_MSG_IMAGE, cameraSnapshotFromPlainMessage, mimeTypeForSnapshotBase64, rosNumericField, } from "@agenticros/ros-camera";
7
7
  import { resolveMemoryNamespace } from "@agenticros/core";
@@ -39,9 +39,12 @@ export const NO_TRANSPORT_TOOL_NAMES = new Set([
39
39
  // tool falls back to a transport-driven discovery only when the caller
40
40
  // passes online=true. See the handler for the precise branch.
41
41
  "ros2_find_robots_for",
42
- // mission_cancel only mutates the in-process MissionRegistry it
43
- // never publishes to ROS, so it must work even when zenohd is down.
42
+ // mission_cancel / mission_pause / mission_resume only mutate the
43
+ // in-process MissionRegistry — they never publish to ROS, so they
44
+ // must work even when zenohd is down.
44
45
  "mission_cancel",
46
+ "mission_pause",
47
+ "mission_resume",
45
48
  // run_mission's *outer* call doesn't need transport — its handler
46
49
  // first compiles the goal (Phase 1.g, no transport) or validates
47
50
  // the mission shape, then per-step calls go through the transport
@@ -392,6 +395,38 @@ export const TOOLS = [
392
395
  required: ["mission_id"],
393
396
  },
394
397
  },
398
+ {
399
+ name: "mission_pause",
400
+ description: "Pause a mission that's currently running in this MCP server. Pass the mission_id returned by run_mission. The runner waits at the next step boundary until mission_resume (or mission_cancel). Idempotent if already paused.",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ mission_id: {
405
+ type: "string",
406
+ description: "The mission_id echoed by run_mission. Required.",
407
+ },
408
+ reason: {
409
+ type: "string",
410
+ description: "Optional free-text reason — surfaced in the paused transcript entry.",
411
+ },
412
+ },
413
+ required: ["mission_id"],
414
+ },
415
+ },
416
+ {
417
+ name: "mission_resume",
418
+ description: "Resume a mission previously paused with mission_pause. Pass the mission_id returned by run_mission. Idempotent if the mission is not paused.",
419
+ inputSchema: {
420
+ type: "object",
421
+ properties: {
422
+ mission_id: {
423
+ type: "string",
424
+ description: "The mission_id echoed by run_mission. Required.",
425
+ },
426
+ },
427
+ required: ["mission_id"],
428
+ },
429
+ },
395
430
  ];
396
431
  function followMeMode(args) {
397
432
  const raw = String(args["mode"] ?? "depth").toLowerCase().trim();
@@ -485,107 +520,6 @@ function formatFindRobotsForResponse(result) {
485
520
  })),
486
521
  });
487
522
  }
488
- /**
489
- * Capability → MCP tool dispatch table used by `run_mission`.
490
- *
491
- * Every capability returned by `ros2_list_capabilities` that the mission
492
- * runner can actually execute must appear here. If you add a new
493
- * intrinsic capability to BUILTIN_CAPABILITIES, mirror it here. For new
494
- * skill-declared capabilities, this is where you map them to the
495
- * corresponding MCP tool. Steps that name a capability without an entry
496
- * fail with an actionable "no mission-runner tool binding" error.
497
- */
498
- const MISSION_BINDINGS = {
499
- drive_base: {
500
- tool: "ros2_publish",
501
- buildArgs: (inputs) => {
502
- const lx = Number(inputs.linear_x ?? 0) || 0;
503
- const az = Number(inputs.angular_z ?? 0) || 0;
504
- return {
505
- topic: "/cmd_vel",
506
- type: "geometry_msgs/msg/Twist",
507
- message: {
508
- linear: { x: lx, y: 0, z: 0 },
509
- angular: { x: 0, y: 0, z: az },
510
- },
511
- };
512
- },
513
- },
514
- take_snapshot: {
515
- tool: "ros2_camera_snapshot",
516
- buildArgs: (inputs) => {
517
- const out = {};
518
- if (typeof inputs.topic === "string")
519
- out.topic = inputs.topic;
520
- if (typeof inputs.message_type === "string")
521
- out.message_type = inputs.message_type;
522
- if (typeof inputs.timeout === "number")
523
- out.timeout = inputs.timeout;
524
- return out;
525
- },
526
- },
527
- measure_depth: {
528
- tool: "ros2_depth_distance",
529
- buildArgs: (inputs) => {
530
- const out = {};
531
- if (typeof inputs.topic === "string")
532
- out.topic = inputs.topic;
533
- if (typeof inputs.timeout === "number")
534
- out.timeout = inputs.timeout;
535
- return out;
536
- },
537
- },
538
- list_topics: {
539
- tool: "ros2_list_topics",
540
- buildArgs: () => ({}),
541
- },
542
- publish_topic: {
543
- tool: "ros2_publish",
544
- buildArgs: (inputs) => ({
545
- topic: String(inputs.topic ?? ""),
546
- type: String(inputs.type ?? inputs.msg_type ?? ""),
547
- message: inputs.message ?? inputs.msg ?? {},
548
- }),
549
- },
550
- subscribe_once: {
551
- tool: "ros2_subscribe_once",
552
- buildArgs: (inputs) => {
553
- const out = { topic: String(inputs.topic ?? "") };
554
- if (typeof inputs.type === "string")
555
- out.type = inputs.type;
556
- if (typeof inputs.timeout === "number")
557
- out.timeout = inputs.timeout;
558
- return out;
559
- },
560
- },
561
- follow_person: {
562
- tool: "ros2_follow_me_start",
563
- buildArgs: (inputs) => {
564
- const out = {};
565
- if (typeof inputs.target_distance === "number")
566
- out.target_distance = inputs.target_distance;
567
- if (typeof inputs.mode === "string")
568
- out.mode = inputs.mode;
569
- return out;
570
- },
571
- },
572
- find_object: {
573
- tool: "ros2_find_object",
574
- buildArgs: (inputs) => {
575
- const target = String(inputs.target ?? "");
576
- const out = { target };
577
- if (typeof inputs.angular_speed === "number")
578
- out.angular_speed = inputs.angular_speed;
579
- if (typeof inputs.clockwise === "boolean")
580
- out.clockwise = inputs.clockwise;
581
- if (typeof inputs.timeout_seconds === "number")
582
- out.timeout_seconds = inputs.timeout_seconds;
583
- if (typeof inputs.min_confidence === "number")
584
- out.min_confidence = inputs.min_confidence;
585
- return out;
586
- },
587
- },
588
- };
589
523
  /**
590
524
  * Phase 1.f — `mission_cancel` handler. Lives at module scope so the
591
525
  * outer `handleToolCall` can invoke it WITHOUT first resolving a
@@ -622,6 +556,64 @@ function handleMissionCancel(args) {
622
556
  ],
623
557
  };
624
558
  }
559
+ function handleMissionPause(args) {
560
+ const missionId = typeof args["mission_id"] === "string" ? args["mission_id"].trim() : "";
561
+ if (!missionId) {
562
+ return {
563
+ content: [
564
+ {
565
+ type: "text",
566
+ text: "mission_pause requires 'mission_id' (a non-empty string returned by run_mission).",
567
+ },
568
+ ],
569
+ isError: true,
570
+ };
571
+ }
572
+ const reason = typeof args["reason"] === "string" ? args["reason"] : undefined;
573
+ const outcome = getMissionRegistry().pause(missionId, reason);
574
+ return {
575
+ content: [
576
+ {
577
+ type: "text",
578
+ text: JSON.stringify({
579
+ success: true,
580
+ mission_id: missionId,
581
+ found: outcome.found,
582
+ already_paused: outcome.alreadyPaused,
583
+ reason: reason ?? null,
584
+ }),
585
+ },
586
+ ],
587
+ };
588
+ }
589
+ function handleMissionResume(args) {
590
+ const missionId = typeof args["mission_id"] === "string" ? args["mission_id"].trim() : "";
591
+ if (!missionId) {
592
+ return {
593
+ content: [
594
+ {
595
+ type: "text",
596
+ text: "mission_resume requires 'mission_id' (a non-empty string returned by run_mission).",
597
+ },
598
+ ],
599
+ isError: true,
600
+ };
601
+ }
602
+ const outcome = getMissionRegistry().resume(missionId);
603
+ return {
604
+ content: [
605
+ {
606
+ type: "text",
607
+ text: JSON.stringify({
608
+ success: true,
609
+ mission_id: missionId,
610
+ found: outcome.found,
611
+ was_paused: outcome.wasPaused,
612
+ }),
613
+ },
614
+ ],
615
+ };
616
+ }
625
617
  /**
626
618
  * Phase 1.c + 1.f + 1.g — `run_mission` handler. Lives at module scope
627
619
  * so the outer `handleToolCall` can short-circuit BEFORE
@@ -696,6 +688,31 @@ async function handleRunMission(args, config) {
696
688
  }
697
689
  }
698
690
  const dispatcher = async (toolName, toolArgs) => {
691
+ if (isExternalToolName(toolName)) {
692
+ const capId = capabilityIdFromExternalTool(toolName);
693
+ const cap = caps.find((c) => c.id === capId);
694
+ if (!cap) {
695
+ return {
696
+ text: `Unknown external capability "${capId}".`,
697
+ isError: true,
698
+ };
699
+ }
700
+ let robot;
701
+ try {
702
+ robot = resolveRobotFromArgs(config, toolArgs);
703
+ }
704
+ catch (err) {
705
+ return {
706
+ text: err instanceof Error ? err.message : String(err),
707
+ isError: true,
708
+ };
709
+ }
710
+ const transport = await getTransportForRobot(config, robot);
711
+ const ext = await executeExternalCapability(cap, toolArgs, transport, {
712
+ namespace: robot.namespace,
713
+ });
714
+ return { text: ext.text, outputs: ext.outputs, isError: ext.isError };
715
+ }
699
716
  const res = await handleToolCall(toolName, toolArgs, config);
700
717
  const text = res.content
701
718
  .map((c) => (c.type === "text" ? c.text : `[image: ${c.mimeType}]`))
@@ -711,7 +728,7 @@ async function handleRunMission(args, config) {
711
728
  const transcript = memory ? createMemoryTranscriptSink(memory, missionId) : undefined;
712
729
  let result;
713
730
  try {
714
- result = await runMission(mission, caps, MISSION_BINDINGS, dispatcher, {
731
+ result = await runMission(mission, caps, buildMissionBindings(caps), dispatcher, {
715
732
  mission_id: missionId,
716
733
  cancellation: regEntry.cancellation,
717
734
  transcript,
@@ -828,15 +845,17 @@ export async function handleToolCall(name, args, config) {
828
845
  };
829
846
  }
830
847
  }
831
- // mission_cancel + run_mission must NOT trigger a transport connect
832
- // at this outer layer: mission_cancel only mutates the in-process
833
- // MissionRegistry, and run_mission's handler does its OWN per-step
834
- // dispatch (each step's inner `handleToolCall` connects as needed).
835
- // Routing the outer call through `getTransportForRobot()` would
836
- // hang or fail when an agent only wanted to compile / cancel.
848
+ // mission_cancel / pause / resume + run_mission must NOT trigger a
849
+ // transport connect at this outer layer.
837
850
  if (name === "mission_cancel") {
838
851
  return handleMissionCancel(args);
839
852
  }
853
+ if (name === "mission_pause") {
854
+ return handleMissionPause(args);
855
+ }
856
+ if (name === "mission_resume") {
857
+ return handleMissionResume(args);
858
+ }
840
859
  if (name === "run_mission") {
841
860
  return handleRunMission(args, config);
842
861
  }