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.
- package/dist/__tests__/robot-config.test.d.ts +2 -0
- package/dist/__tests__/robot-config.test.d.ts.map +1 -0
- package/dist/__tests__/robot-config.test.js +374 -0
- package/dist/__tests__/robot-config.test.js.map +1 -0
- package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
- package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
- package/dist/__tests__/transport-shorthand.test.js +88 -0
- package/dist/__tests__/transport-shorthand.test.js.map +1 -0
- package/dist/commands/robots.d.ts +57 -0
- package/dist/commands/robots.d.ts.map +1 -0
- package/dist/commands/robots.js +678 -0
- package/dist/commands/robots.js.map +1 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +125 -2
- package/dist/commands/skills.js.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/menu.js +22 -4
- package/dist/menu.js.map +1 -1
- package/dist/util/marketplace.d.ts +57 -0
- package/dist/util/marketplace.d.ts.map +1 -0
- package/dist/util/marketplace.js +144 -0
- package/dist/util/marketplace.js.map +1 -0
- package/dist/util/mcp-discovery.d.ts +57 -0
- package/dist/util/mcp-discovery.d.ts.map +1 -0
- package/dist/util/mcp-discovery.js +153 -0
- package/dist/util/mcp-discovery.js.map +1 -0
- package/dist/util/robot-config.d.ts +167 -0
- package/dist/util/robot-config.d.ts.map +1 -0
- package/dist/util/robot-config.js +375 -0
- package/dist/util/robot-config.js.map +1 -0
- package/dist/util/skills.d.ts +6 -6
- package/dist/util/skills.d.ts.map +1 -1
- package/dist/util/skills.js +14 -9
- package/dist/util/skills.js.map +1 -1
- package/dist/util/transport-shorthand.d.ts +66 -0
- package/dist/util/transport-shorthand.d.ts.map +1 -0
- package/dist/util/transport-shorthand.js +102 -0
- package/dist/util/transport-shorthand.js.map +1 -0
- package/package.json +12 -10
- package/runtime/BUNDLE.json +1 -1
- package/runtime/README.md +62 -1
- package/runtime/package.json +6 -1
- package/runtime/packages/agenticros/openclaw.plugin.json +7 -1
- package/runtime/packages/agenticros/package.json +2 -1
- package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
- package/runtime/packages/agenticros/src/config-page.ts +1 -1
- package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
- package/runtime/packages/agenticros/src/service.ts +120 -2
- package/runtime/packages/agenticros/src/skill-loader.ts +109 -30
- package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
- package/runtime/packages/agenticros/src/tools/index.ts +49 -9
- package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
- package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
- package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
- package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
- package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
- package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
- package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
- package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
- package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
- package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
- package/runtime/packages/agenticros-claude-code/README.md +52 -9
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts +2 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js +773 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts +2 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js +97 -0
- package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts +2 -2
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js +5 -5
- package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts +109 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js +392 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts +8 -3
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js +30 -12
- package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/index.js +5 -5
- package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts +16 -0
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.js +19 -0
- package/runtime/packages/agenticros-claude-code/dist/mission-registry.js.map +1 -0
- package/runtime/packages/agenticros-claude-code/dist/tools.d.ts +9 -0
- package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/tools.js +700 -54
- package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/transport.d.ts +28 -8
- package/runtime/packages/agenticros-claude-code/dist/transport.d.ts.map +1 -1
- package/runtime/packages/agenticros-claude-code/dist/transport.js +37 -34
- package/runtime/packages/agenticros-claude-code/dist/transport.js.map +1 -1
- package/runtime/packages/agenticros-claude-code/package.json +2 -1
- package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +965 -0
- package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
- package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
- package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
- package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
- package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
- package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
- package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
- package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
- package/runtime/packages/agenticros-gemini/package.json +1 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
- package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
- package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
- package/runtime/packages/core/LICENSE +192 -0
- package/runtime/packages/core/README.md +88 -0
- package/runtime/packages/core/package.json +36 -3
- package/runtime/packages/core/src/__tests__/capabilities.test.ts +266 -0
- package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
- package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
- package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
- package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
- package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
- package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
- package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
- package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
- package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
- package/runtime/packages/core/src/capabilities.ts +314 -0
- package/runtime/packages/core/src/config.ts +221 -39
- package/runtime/packages/core/src/discovery.ts +193 -0
- package/runtime/packages/core/src/find-robots-for.ts +164 -0
- package/runtime/packages/core/src/index.ts +67 -1
- package/runtime/packages/core/src/mission-registry.ts +142 -0
- package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
- package/runtime/packages/core/src/mission.ts +574 -0
- package/runtime/packages/core/src/planner/index.ts +564 -0
- package/runtime/packages/core/src/robots.ts +282 -0
- package/runtime/packages/core/src/topic-utils.ts +47 -9
- package/runtime/packages/core/src/transport-pool.ts +171 -0
- package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
- package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
- package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
- package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
- package/runtime/scripts/configure_agenticros.sh +0 -0
- package/runtime/scripts/configure_for_sim.sh +0 -0
- package/runtime/scripts/fix-openclaw-control-ui-path.sh +0 -0
- package/runtime/scripts/install_cli.sh +0 -0
- package/runtime/scripts/install_rosbridge_from_source.sh +0 -0
- package/runtime/scripts/onboard_robot.sh +0 -0
- package/runtime/scripts/pack-runtime.mjs +0 -0
- package/runtime/scripts/refresh-skill-deps.mjs +381 -0
- package/runtime/scripts/run_demo_native.sh +0 -0
- package/runtime/scripts/run_nemoclaw_host_stack.sh +0 -0
- package/runtime/scripts/run_robot_rosbridge.sh +0 -0
- package/runtime/scripts/setup_gateway_plugin.sh +32 -5
- package/runtime/scripts/setup_robot.sh +0 -0
- package/runtime/scripts/setup_workspace.sh +0 -0
- package/runtime/scripts/sim/run_sim.sh +0 -0
- package/runtime/scripts/smoke_test_nemoclaw.sh +0 -0
- package/runtime/scripts/start_demo.sh +0 -0
- package/runtime/scripts/sync-skill-tools.mjs +69 -12
- package/runtime/scripts/use-openclaw-2026.2.26.sh +0 -0
- package/runtime/scripts/use-openclaw-2026.3.11.sh +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: ros2_list_capabilities — Phase 1.b of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* Returns the merged list of intrinsic robot verbs + every capability
|
|
5
|
+
* declared by installed AgenticROS skills, as read by
|
|
6
|
+
* @agenticros/core. Prefer this over `ros2_list_topics` for high-level
|
|
7
|
+
* planning: capabilities are agent-meaningful verbs with typed
|
|
8
|
+
* inputs/outputs, not raw topic names.
|
|
9
|
+
*
|
|
10
|
+
* Read-only and transport-free: this tool reads skill manifests from
|
|
11
|
+
* local config + filesystem, so it works even when the robot is
|
|
12
|
+
* offline. Mirrored across all three adapters (OpenClaw, Claude Code,
|
|
13
|
+
* Gemini) — see docs/strategy-ai-agents-plus-ros.md §4.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Type } from "@sinclair/typebox";
|
|
17
|
+
import type { OpenClawPluginApi } from "../plugin-api.js";
|
|
18
|
+
import type { AgenticROSConfig, Capability } from "@agenticros/core";
|
|
19
|
+
import { listAllCapabilities } from "@agenticros/core";
|
|
20
|
+
import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
|
|
21
|
+
|
|
22
|
+
export function registerCapabilitiesTool(
|
|
23
|
+
api: OpenClawPluginApi,
|
|
24
|
+
config: AgenticROSConfig,
|
|
25
|
+
): void {
|
|
26
|
+
api.registerTool({
|
|
27
|
+
name: "ros2_list_capabilities",
|
|
28
|
+
label: "ROS2 List Capabilities",
|
|
29
|
+
description:
|
|
30
|
+
"List the high-level capabilities (named verbs) this robot can perform — built-in verbs like " +
|
|
31
|
+
"drive_base / take_snapshot / measure_depth plus every capability declared by installed " +
|
|
32
|
+
"AgenticROS skills (e.g. follow_person, find_object). PREFER this over ros2_list_topics for " +
|
|
33
|
+
"high-level planning: capabilities are agent-meaningful verbs with typed inputs/outputs, " +
|
|
34
|
+
"not raw topic names. Returns one structured response listing every capability the robot " +
|
|
35
|
+
"supports right now. Pass robot_id (from ros2_list_robots) to scope to a specific robot; " +
|
|
36
|
+
"today every robot exposes the same capabilities, but the API is in place for per-robot " +
|
|
37
|
+
"capability declarations.",
|
|
38
|
+
parameters: Type.Object({ ...ROBOT_ID_SCHEMA }),
|
|
39
|
+
|
|
40
|
+
async execute(_toolCallId, params) {
|
|
41
|
+
// robot_id is validated even though the response doesn't depend on
|
|
42
|
+
// it yet — unknown ids surface as a tool error, matching every
|
|
43
|
+
// other tool in the suite.
|
|
44
|
+
const resolved = resolveRobotForTool(config, params);
|
|
45
|
+
if ("error" in resolved) return resolved.error;
|
|
46
|
+
|
|
47
|
+
const caps: Capability[] = listAllCapabilities(config);
|
|
48
|
+
const intrinsic = caps.filter((c) => c.source?.kind === "builtin").length;
|
|
49
|
+
const skill = caps.filter((c) => c.source?.kind === "skill").length;
|
|
50
|
+
const result = {
|
|
51
|
+
success: true,
|
|
52
|
+
total: caps.length,
|
|
53
|
+
intrinsic_count: intrinsic,
|
|
54
|
+
skill_count: skill,
|
|
55
|
+
capabilities: caps,
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
59
|
+
details: result,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -7,9 +7,10 @@ import { Type } from "@sinclair/typebox";
|
|
|
7
7
|
import type { OpenClawPluginApi } from "../plugin-api.js";
|
|
8
8
|
import type { AgenticROSConfig } from "@agenticros/core";
|
|
9
9
|
import { toNamespacedTopic } from "@agenticros/core";
|
|
10
|
-
import {
|
|
10
|
+
import { getTransportForRobot } from "../service.js";
|
|
11
11
|
import { getDepthDistance } from "../depth.js";
|
|
12
12
|
import { REALSENSE_CAMERA_TOPICS } from "./ros2-camera.js";
|
|
13
|
+
import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_DEPTH_TOPIC = REALSENSE_CAMERA_TOPICS.depth_raw;
|
|
15
16
|
|
|
@@ -20,7 +21,8 @@ export function registerDepthDistanceTool(api: OpenClawPluginApi, config: Agenti
|
|
|
20
21
|
description:
|
|
21
22
|
"Get distance in meters from the robot's depth camera (e.g. RealSense). " +
|
|
22
23
|
"Samples the center of the depth image and returns distance biased toward nearer pixels (~12th percentile), not median (median often tracks walls when the person only covers part of the patch). " +
|
|
23
|
-
"Use when the user asks how far they are from the robot, or distance to/from the robot."
|
|
24
|
+
"Use when the user asks how far they are from the robot, or distance to/from the robot. " +
|
|
25
|
+
"Pass robot_id (from ros2_list_robots) to sample a specific robot's depth camera.",
|
|
24
26
|
|
|
25
27
|
parameters: Type.Object({
|
|
26
28
|
topic: Type.Optional(
|
|
@@ -29,15 +31,20 @@ export function registerDepthDistanceTool(api: OpenClawPluginApi, config: Agenti
|
|
|
29
31
|
}),
|
|
30
32
|
),
|
|
31
33
|
timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 5000)" })),
|
|
34
|
+
...ROBOT_ID_SCHEMA,
|
|
32
35
|
}),
|
|
33
36
|
|
|
34
37
|
async execute(_toolCallId, params) {
|
|
38
|
+
const resolved = resolveRobotForTool(config, params);
|
|
39
|
+
if ("error" in resolved) return resolved.error;
|
|
40
|
+
const { robot } = resolved;
|
|
41
|
+
|
|
35
42
|
const rawTopic = (params["topic"] as string | undefined)?.trim() || DEFAULT_DEPTH_TOPIC;
|
|
36
|
-
const topic = toNamespacedTopic(
|
|
43
|
+
const topic = toNamespacedTopic(robot.namespace, rawTopic);
|
|
37
44
|
const timeout = (params["timeout"] as number | undefined) ?? 5000;
|
|
38
45
|
|
|
39
46
|
try {
|
|
40
|
-
const transport =
|
|
47
|
+
const transport = await getTransportForRobot(config, robot);
|
|
41
48
|
const result = await getDepthDistance(transport, topic, timeout);
|
|
42
49
|
const text = result.valid
|
|
43
50
|
? `Distance at center (~12th percentile, nearer surfaces): **${result.distance_m} m** (median in same patch: ${result.median_m} m; range ${result.min_m}–${result.max_m} m; ${result.sample_count} pixels). Topic: ${result.topic}.`
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: ros2_discover_robots — Phase 1.d "fleet awareness on the wire".
|
|
3
|
+
*
|
|
4
|
+
* Scans the ROS2 topic graph via `transport.listTopics()`, infers robot
|
|
5
|
+
* namespaces from `<ns>/cmd_vel` patterns, and cross-references them
|
|
6
|
+
* against the gateway's configured robot list. Returns four buckets so
|
|
7
|
+
* the agent can answer "which robots are online right now?" without
|
|
8
|
+
* forcing the user to hand-edit config:
|
|
9
|
+
*
|
|
10
|
+
* - detected — every robot inferred from the topic graph
|
|
11
|
+
* - configured_online — configured AND on the wire
|
|
12
|
+
* - configured_offline — configured but the wire is silent
|
|
13
|
+
* - unknown_detected — on the wire but NOT in config (candidates)
|
|
14
|
+
*
|
|
15
|
+
* Read-only — nothing is written to config from this tool. Persistence
|
|
16
|
+
* is a separate UX decision handled by the CLI / OpenClaw config UI.
|
|
17
|
+
*
|
|
18
|
+
* Pure-function detection + classification lives in @agenticros/core
|
|
19
|
+
* (`discoverRobots()`) so all three adapters share the same semantics.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Type } from "@sinclair/typebox";
|
|
23
|
+
import type { OpenClawPluginApi } from "../plugin-api.js";
|
|
24
|
+
import type { AgenticROSConfig } from "@agenticros/core";
|
|
25
|
+
import { discoverRobots } from "@agenticros/core";
|
|
26
|
+
import { getTransport } from "../service.js";
|
|
27
|
+
|
|
28
|
+
export function registerDiscoverRobotsTool(
|
|
29
|
+
api: OpenClawPluginApi,
|
|
30
|
+
config: AgenticROSConfig,
|
|
31
|
+
): void {
|
|
32
|
+
api.registerTool({
|
|
33
|
+
name: "ros2_discover_robots",
|
|
34
|
+
label: "ROS2 Discover Robots",
|
|
35
|
+
description:
|
|
36
|
+
"Scan the ROS2 topic graph and report which robots are actually on the wire right now, " +
|
|
37
|
+
"classified against the gateway's configured robot list. Returns: (1) every namespace " +
|
|
38
|
+
"inferred from `<ns>/cmd_vel` topics, with a topicCount that says how many corroborating " +
|
|
39
|
+
"topics live under that namespace, (2) configured_online — configured robots currently " +
|
|
40
|
+
"publishing, (3) configured_offline — configured robots that are silent, (4) " +
|
|
41
|
+
"unknown_detected — robots on the wire that aren't in config yet (candidates to add via " +
|
|
42
|
+
"the CLI / config UI). Use this when the user asks 'which robots are online right now', " +
|
|
43
|
+
"'is my robot connected', or wants to find a robot that isn't in ros2_list_robots. " +
|
|
44
|
+
"Requires the ROS transport to be connected.",
|
|
45
|
+
parameters: Type.Object({}),
|
|
46
|
+
|
|
47
|
+
async execute(_toolCallId, _params) {
|
|
48
|
+
const transport = getTransport();
|
|
49
|
+
const topics = await transport.listTopics();
|
|
50
|
+
const result = discoverRobots(topics, config);
|
|
51
|
+
const payload = {
|
|
52
|
+
success: true,
|
|
53
|
+
total_topics: result.total_topics,
|
|
54
|
+
detected: result.detected,
|
|
55
|
+
configured_online: result.configured_online,
|
|
56
|
+
configured_offline: result.configured_offline,
|
|
57
|
+
unknown_detected: result.unknown_detected,
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
61
|
+
details: payload,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: ros2_find_robots_for — Phase 1.e of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* Capability-aware fleet filter: given a (capability, kind, online)
|
|
5
|
+
* query, returns the configured robots that match, ranked best-first.
|
|
6
|
+
* This is the tool that lets an LLM ask "which robot can find a chair"
|
|
7
|
+
* or "is there an AMR online that can follow a person" and get a
|
|
8
|
+
* structured answer it can plan against — without scanning topic lists.
|
|
9
|
+
*
|
|
10
|
+
* Config-only by default (no transport touched). When the caller sets
|
|
11
|
+
* `online: true|false`, we list topics via the global transport and
|
|
12
|
+
* cross-reference against `<ns>/cmd_vel` patterns to compute the live
|
|
13
|
+
* set — same heuristic as ros2_discover_robots, sharing
|
|
14
|
+
* `discoverRobots()` from @agenticros/core. The transport is only
|
|
15
|
+
* acquired in that branch; offline-mode planning still works.
|
|
16
|
+
*
|
|
17
|
+
* Mirrored across all three adapters (Claude Code, OpenClaw, Gemini)
|
|
18
|
+
* with identical request/response shapes — see
|
|
19
|
+
* `packages/agenticros-claude-code/src/tools.ts` for the canonical one.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Type } from "@sinclair/typebox";
|
|
23
|
+
import type { OpenClawPluginApi } from "../plugin-api.js";
|
|
24
|
+
import type { AgenticROSConfig } from "@agenticros/core";
|
|
25
|
+
import { discoverRobots, findRobotsFor } from "@agenticros/core";
|
|
26
|
+
import { getTransport } from "../service.js";
|
|
27
|
+
|
|
28
|
+
export function registerFindRobotsForTool(
|
|
29
|
+
api: OpenClawPluginApi,
|
|
30
|
+
config: AgenticROSConfig,
|
|
31
|
+
): void {
|
|
32
|
+
api.registerTool({
|
|
33
|
+
name: "ros2_find_robots_for",
|
|
34
|
+
label: "ROS2 Find Robots For",
|
|
35
|
+
description:
|
|
36
|
+
"Find the robots in the configured fleet that match a capability + kind + online filter, " +
|
|
37
|
+
"ranked best-first. PREFER this over ros2_list_robots whenever the user names a verb " +
|
|
38
|
+
"('which robot can find a chair', 'do I have an arm robot that can grasp', 'is there an " +
|
|
39
|
+
"AMR online that can follow a person'). Capability matches the verbs from " +
|
|
40
|
+
"ros2_list_capabilities — by default robots inherit the gateway-wide registry; declaring " +
|
|
41
|
+
"per-robot capabilities in config narrows it. Kind matches robot.kind exactly ('amr' | " +
|
|
42
|
+
"'arm' | 'drone' | 'rover'). When online=true, only currently-reachable robots are " +
|
|
43
|
+
"returned (uses the same `<ns>/cmd_vel` heuristic as ros2_discover_robots and requires " +
|
|
44
|
+
"the transport). The result lists matched robots with id/name/namespace/kind/sensors/" +
|
|
45
|
+
"online flag, ranked so explicit capability declarations + online robots come first.",
|
|
46
|
+
parameters: Type.Object({
|
|
47
|
+
capability: Type.Optional(
|
|
48
|
+
Type.String({
|
|
49
|
+
description:
|
|
50
|
+
"Capability id to match (e.g. 'follow_person', 'find_object', 'drive_base'). " +
|
|
51
|
+
"Case-sensitive — use ros2_list_capabilities to get the exact list.",
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
kind: Type.Optional(
|
|
55
|
+
Type.String({
|
|
56
|
+
description:
|
|
57
|
+
"Robot kind filter (case-insensitive exact match). Common values: 'amr', 'arm', " +
|
|
58
|
+
"'drone', 'rover'.",
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
online: Type.Optional(
|
|
62
|
+
Type.Boolean({
|
|
63
|
+
description:
|
|
64
|
+
"When true, only return robots currently reachable on the ROS2 graph (requires the " +
|
|
65
|
+
"transport to be connected). When false, only return robots NOT reachable. When " +
|
|
66
|
+
"omitted, online status is annotated on every match but doesn't filter the list.",
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
async execute(_toolCallId, params) {
|
|
72
|
+
const p = (params ?? {}) as { capability?: string; kind?: string; online?: boolean };
|
|
73
|
+
let onlineIds: Set<string> | undefined;
|
|
74
|
+
if (p.online !== undefined) {
|
|
75
|
+
const transport = getTransport();
|
|
76
|
+
if (transport.getStatus() !== "connected") {
|
|
77
|
+
const fail = {
|
|
78
|
+
success: false,
|
|
79
|
+
error:
|
|
80
|
+
"online filter requires the ROS transport to be connected. Drop the 'online' " +
|
|
81
|
+
"arg to run config-only, or check that zenohd / rosbridge is up.",
|
|
82
|
+
};
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(fail) }], details: fail };
|
|
84
|
+
}
|
|
85
|
+
const topics = await transport.listTopics();
|
|
86
|
+
const disc = discoverRobots(topics, config);
|
|
87
|
+
onlineIds = new Set(disc.configured_online.map((r) => r.id));
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const result = findRobotsFor(
|
|
91
|
+
config,
|
|
92
|
+
{ capability: p.capability, kind: p.kind, online: p.online },
|
|
93
|
+
onlineIds,
|
|
94
|
+
);
|
|
95
|
+
// Flatten the FindRobotsForMatch shape into one object per
|
|
96
|
+
// robot so the LLM doesn't have to dig through .robot.* —
|
|
97
|
+
// identical shape to the claude-code adapter's formatter.
|
|
98
|
+
const payload = {
|
|
99
|
+
success: true,
|
|
100
|
+
query: result.query,
|
|
101
|
+
total: result.total,
|
|
102
|
+
robots: result.robots.map((m) => ({
|
|
103
|
+
id: m.robot.id,
|
|
104
|
+
name: m.robot.name,
|
|
105
|
+
namespace: m.robot.namespace,
|
|
106
|
+
kind: m.robot.kind,
|
|
107
|
+
sensors: m.robot.sensors,
|
|
108
|
+
capabilities: m.robot.capabilities ?? null,
|
|
109
|
+
cameraTopic: m.robot.cameraTopic,
|
|
110
|
+
online: m.online,
|
|
111
|
+
matched_capability_explicitly: m.matched_capability_explicitly,
|
|
112
|
+
score: m.score,
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
117
|
+
details: payload,
|
|
118
|
+
};
|
|
119
|
+
} catch (e) {
|
|
120
|
+
const fail = {
|
|
121
|
+
success: false,
|
|
122
|
+
error: e instanceof Error ? e.message : String(e),
|
|
123
|
+
};
|
|
124
|
+
return { content: [{ type: "text", text: JSON.stringify(fail) }], details: fail };
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: run_mission — Phase 1.c of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* Executes a multi-step mission by chaining capabilities. Each step
|
|
5
|
+
* names a capability (from `ros2_list_capabilities`); the runner maps
|
|
6
|
+
* the capability to a previously-registered OpenClaw tool, calls that
|
|
7
|
+
* tool's execute() with the step's resolved inputs, and feeds the
|
|
8
|
+
* structured output into later steps via `{{stepId.outputs.field}}`
|
|
9
|
+
* template references.
|
|
10
|
+
*
|
|
11
|
+
* The transport-agnostic runner itself lives in @agenticros/core; this
|
|
12
|
+
* file just provides the OpenClaw-flavoured tool wrapper plus the
|
|
13
|
+
* capability → tool-name binding map (which mirrors the one in
|
|
14
|
+
* packages/agenticros-claude-code/src/tools.ts so all adapters agree on
|
|
15
|
+
* what `drive_base`, `find_object`, etc. actually do).
|
|
16
|
+
*
|
|
17
|
+
* Mirrored across all three adapters — see
|
|
18
|
+
* docs/strategy-ai-agents-plus-ros.md §4 Phase 1.c.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Type } from "@sinclair/typebox";
|
|
22
|
+
import type { AgentTool, OpenClawPluginApi } from "../plugin-api.js";
|
|
23
|
+
import type {
|
|
24
|
+
AgenticROSConfig,
|
|
25
|
+
CapabilityToolBindings,
|
|
26
|
+
Mission,
|
|
27
|
+
MissionToolDispatcher,
|
|
28
|
+
} from "@agenticros/core";
|
|
29
|
+
import {
|
|
30
|
+
listAllCapabilities,
|
|
31
|
+
runMission,
|
|
32
|
+
generateMissionId,
|
|
33
|
+
createMemoryTranscriptSink,
|
|
34
|
+
missionTranscriptNamespace,
|
|
35
|
+
compileGoalToMission,
|
|
36
|
+
} from "@agenticros/core";
|
|
37
|
+
import { resolveRobotForTool } from "./_robot-helpers.js";
|
|
38
|
+
import { getMissionRegistry } from "../mission-registry.js";
|
|
39
|
+
import { getMemory } from "../memory.js";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Snapshot of the registered tools, keyed by tool name. Built in
|
|
43
|
+
* `tools/index.ts` as each tool is registered, then passed in so this
|
|
44
|
+
* tool can dispatch sub-tool calls by name.
|
|
45
|
+
*/
|
|
46
|
+
export type ToolRegistry = Map<string, AgentTool>;
|
|
47
|
+
|
|
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
|
+
export function registerMissionTool(
|
|
128
|
+
api: OpenClawPluginApi,
|
|
129
|
+
config: AgenticROSConfig,
|
|
130
|
+
registry: ToolRegistry,
|
|
131
|
+
): void {
|
|
132
|
+
api.registerTool({
|
|
133
|
+
name: "run_mission",
|
|
134
|
+
label: "ROS2 Run Mission",
|
|
135
|
+
description:
|
|
136
|
+
"Execute a multi-step mission by chaining capabilities (the verbs returned by ros2_list_capabilities). " +
|
|
137
|
+
"PASS EITHER a natural-language `goal` (recommended for simple verbs like 'find a chair', " +
|
|
138
|
+
"'take a picture', 'follow me', 'find a chair and drive toward it') OR an explicit `mission.steps[]` " +
|
|
139
|
+
"plan when you need precise control. Steps run sequentially; each step's outputs are available to " +
|
|
140
|
+
"later steps via {{stepId.outputs.fieldName}} template references. " +
|
|
141
|
+
"Default on_fail behaviour is 'stop' (abort on first error). Returns a per-step result list, a " +
|
|
142
|
+
"summary line, a mission_id you can pass to mission_cancel to abort mid-run, and (when a goal was " +
|
|
143
|
+
"provided) the compiled plan + candidate match list so you can see what the planner did. " +
|
|
144
|
+
"When memory is enabled, every step is also written to the shared memory under namespace " +
|
|
145
|
+
"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.",
|
|
149
|
+
parameters: Type.Object({
|
|
150
|
+
goal: Type.Optional(
|
|
151
|
+
Type.String({
|
|
152
|
+
description:
|
|
153
|
+
"Natural-language goal — the local planner compiles it into a mission against the capability registry. " +
|
|
154
|
+
"Examples: 'find a chair', 'find a chair and drive toward it', 'take a picture', 'follow me', " +
|
|
155
|
+
"'measure depth', 'drive forward at 0.3 m/s', 'turn left', 'stop'. " +
|
|
156
|
+
"Either goal or mission must be provided; mission wins if both are set.",
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
robot_id: Type.Optional(
|
|
160
|
+
Type.String({
|
|
161
|
+
description:
|
|
162
|
+
"Optional robot id (from ros2_list_robots) — used when 'goal' is provided to scope every compiled step to one robot.",
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
mission: Type.Optional(
|
|
166
|
+
Type.Object({
|
|
167
|
+
name: Type.Optional(Type.String()),
|
|
168
|
+
goal: Type.Optional(Type.String()),
|
|
169
|
+
robot_id: Type.Optional(
|
|
170
|
+
Type.String({
|
|
171
|
+
description:
|
|
172
|
+
"Default robot id (from ros2_list_robots) injected into every step's tool args. Per-step inputs.robot_id overrides this for individual steps.",
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
steps: Type.Array(
|
|
176
|
+
Type.Object({
|
|
177
|
+
id: Type.String(),
|
|
178
|
+
capability: Type.String(),
|
|
179
|
+
inputs: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
180
|
+
on_fail: Type.Optional(Type.Union([Type.Literal("stop"), Type.Literal("continue")])),
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
async execute(toolCallId, params, signal) {
|
|
188
|
+
const caps = listAllCapabilities(config);
|
|
189
|
+
const missionRaw = params["mission"];
|
|
190
|
+
const goalRaw = params["goal"];
|
|
191
|
+
const topLevelRobotId = typeof params["robot_id"] === "string" ? (params["robot_id"] as string) : undefined;
|
|
192
|
+
|
|
193
|
+
// Phase 1.g — accept either an explicit mission OR a
|
|
194
|
+
// natural-language goal. The planner is deterministic + rule-based,
|
|
195
|
+
// so an agent can preview the compile via the same call. We surface
|
|
196
|
+
// candidates/suggestions in details so a failed compile is actionable.
|
|
197
|
+
let mission: Mission;
|
|
198
|
+
let plannerInfo:
|
|
199
|
+
| { compiled_from_goal: string; candidates: unknown[]; unmatched_verbs?: string[] }
|
|
200
|
+
| undefined;
|
|
201
|
+
if (missionRaw && typeof missionRaw === "object" && !Array.isArray(missionRaw)) {
|
|
202
|
+
mission = missionRaw as Mission;
|
|
203
|
+
if (!Array.isArray(mission.steps)) {
|
|
204
|
+
const text = "mission.steps must be an array of step objects.";
|
|
205
|
+
return { content: [{ type: "text", text }], details: { success: false, error: text } };
|
|
206
|
+
}
|
|
207
|
+
} else if (typeof goalRaw === "string" && goalRaw.trim().length > 0) {
|
|
208
|
+
const planned = compileGoalToMission(goalRaw, caps, { robot_id: topLevelRobotId });
|
|
209
|
+
if (!planned.mission) {
|
|
210
|
+
const details = {
|
|
211
|
+
success: false,
|
|
212
|
+
error: planned.error,
|
|
213
|
+
goal: goalRaw,
|
|
214
|
+
suggestions: planned.suggestions,
|
|
215
|
+
...(planned.unmatched_verbs ? { unmatched_verbs: planned.unmatched_verbs } : {}),
|
|
216
|
+
};
|
|
217
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
218
|
+
}
|
|
219
|
+
mission = planned.mission;
|
|
220
|
+
plannerInfo = {
|
|
221
|
+
compiled_from_goal: goalRaw,
|
|
222
|
+
candidates: planned.candidates,
|
|
223
|
+
...(planned.unmatched_verbs ? { unmatched_verbs: planned.unmatched_verbs } : {}),
|
|
224
|
+
};
|
|
225
|
+
} else {
|
|
226
|
+
const text =
|
|
227
|
+
'run_mission requires either "mission" (object with steps[]) or "goal" (natural-language string). Pass at least one.';
|
|
228
|
+
return { content: [{ type: "text", text }], details: { success: false, error: text } };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Validate mission.robot_id up-front so the agent gets a single
|
|
232
|
+
// clean error (with known ids) instead of one per step.
|
|
233
|
+
if (typeof mission.robot_id === "string" && mission.robot_id.trim().length > 0) {
|
|
234
|
+
const resolved = resolveRobotForTool(config, { robot_id: mission.robot_id });
|
|
235
|
+
if ("error" in resolved) return resolved.error;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const dispatcher: MissionToolDispatcher = async (toolName, toolArgs) => {
|
|
239
|
+
const tool = registry.get(toolName);
|
|
240
|
+
if (!tool) {
|
|
241
|
+
return {
|
|
242
|
+
text: `Tool "${toolName}" is not registered with this OpenClaw plugin instance.`,
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const res = await tool.execute(toolCallId, toolArgs, signal);
|
|
247
|
+
const text = res.content
|
|
248
|
+
.map((c) => (c.type === "text" ? c.text : `[image: ${c.mimeType}]`))
|
|
249
|
+
.join("\n");
|
|
250
|
+
// Tool results don't carry an isError flag in the OpenClaw shape;
|
|
251
|
+
// surface details if it's an object so the runner can pick it up
|
|
252
|
+
// as structured outputs.
|
|
253
|
+
const outputs =
|
|
254
|
+
res.details && typeof res.details === "object" && !Array.isArray(res.details)
|
|
255
|
+
? (res.details as Record<string, unknown>)
|
|
256
|
+
: undefined;
|
|
257
|
+
return { text, outputs };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Phase 1.f — generate + register a mission_id, wire transcripts.
|
|
261
|
+
// mission_cancel uses the registry to find this entry mid-run.
|
|
262
|
+
// The transcript sink is only attached when memory is enabled
|
|
263
|
+
// (initMemory() populated getMemory()); otherwise it stays a
|
|
264
|
+
// no-op so opt-in deployments aren't affected.
|
|
265
|
+
const missionId = generateMissionId();
|
|
266
|
+
const missionRegistry = getMissionRegistry();
|
|
267
|
+
const { entry: regEntry, dispose: disposeRegistry } = missionRegistry.register(
|
|
268
|
+
missionId,
|
|
269
|
+
{ name: mission.name },
|
|
270
|
+
);
|
|
271
|
+
const memory = getMemory();
|
|
272
|
+
const transcript = memory ? createMemoryTranscriptSink(memory, missionId) : undefined;
|
|
273
|
+
|
|
274
|
+
let result;
|
|
275
|
+
try {
|
|
276
|
+
result = await runMission(mission, caps, MISSION_BINDINGS, dispatcher, {
|
|
277
|
+
mission_id: missionId,
|
|
278
|
+
cancellation: regEntry.cancellation,
|
|
279
|
+
transcript,
|
|
280
|
+
adapter: "openclaw",
|
|
281
|
+
});
|
|
282
|
+
} finally {
|
|
283
|
+
disposeRegistry();
|
|
284
|
+
}
|
|
285
|
+
const compact = {
|
|
286
|
+
status: result.status,
|
|
287
|
+
mission_id: result.mission_id,
|
|
288
|
+
...(result.cancellation_reason ? { cancellation_reason: result.cancellation_reason } : {}),
|
|
289
|
+
...(transcript ? { transcript_namespace: missionTranscriptNamespace(missionId) } : {}),
|
|
290
|
+
...(plannerInfo ? { planner: plannerInfo } : {}),
|
|
291
|
+
steps_run: result.steps_run,
|
|
292
|
+
steps_total: result.steps_total,
|
|
293
|
+
duration_ms: result.duration_ms,
|
|
294
|
+
summary: result.summary,
|
|
295
|
+
steps: result.steps.map((s) => ({
|
|
296
|
+
id: s.id,
|
|
297
|
+
capability: s.capability,
|
|
298
|
+
status: s.status,
|
|
299
|
+
inputs: s.inputs,
|
|
300
|
+
outputs: s.outputs,
|
|
301
|
+
...(s.error ? { error: s.error } : {}),
|
|
302
|
+
duration_ms: s.duration_ms,
|
|
303
|
+
})),
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: `${result.summary}\n${JSON.stringify(compact)}` }],
|
|
307
|
+
details: compact,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|