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,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission planner — Phase 1.g of the AgenticROS strategy.
|
|
3
|
+
*
|
|
4
|
+
* Compiles a natural-language goal ("find a chair and drive toward
|
|
5
|
+
* it", "take a picture", "follow me") into a declarative `Mission`
|
|
6
|
+
* the runner can execute. The compiler is intentionally rule-based
|
|
7
|
+
* (no LLM dependency) so:
|
|
8
|
+
*
|
|
9
|
+
* - Phase 1 doesn't ship with a hard Ollama requirement
|
|
10
|
+
* - Tests are deterministic (same input → same plan)
|
|
11
|
+
* - Failures are explainable ("I matched verb X but had no input
|
|
12
|
+
* for Y") — agents can self-correct without an extra round-trip
|
|
13
|
+
*
|
|
14
|
+
* A future LLM-backed planner is intended to live behind the same
|
|
15
|
+
* `compileGoalToMission` contract — Phase 2+ work. For now this
|
|
16
|
+
* covers the canonical verbs in `BUILTIN_CAPABILITIES` + the
|
|
17
|
+
* skill-declared `find_object` / `follow_person` that ship with
|
|
18
|
+
* AgenticROS.
|
|
19
|
+
*
|
|
20
|
+
* The planner ONLY emits steps for capabilities present in the
|
|
21
|
+
* `capabilities` argument it's handed. Skill capabilities that
|
|
22
|
+
* aren't installed are silently skipped — the planner never
|
|
23
|
+
* fabricates calls to non-existent tools. When nothing matches,
|
|
24
|
+
* the result includes hints + the recognised verb list so the
|
|
25
|
+
* agent can recover.
|
|
26
|
+
*
|
|
27
|
+
* See: docs/strategy-ai-agents-plus-ros.md §4 Phase 1.g.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { Capability } from "../capabilities.js";
|
|
31
|
+
import type { Mission, MissionStep } from "../mission.js";
|
|
32
|
+
|
|
33
|
+
/** One candidate match the planner considered for the goal. */
|
|
34
|
+
export interface PlannerCandidate {
|
|
35
|
+
/** Capability id we matched. */
|
|
36
|
+
capability_id: string;
|
|
37
|
+
/** 0..1 confidence — higher = better fit. */
|
|
38
|
+
confidence: number;
|
|
39
|
+
/** Human-readable explanation ("matched 'find' verb on token 'find'"). */
|
|
40
|
+
rationale: string;
|
|
41
|
+
/** Inputs the matcher extracted (e.g. `target: "chair"`). */
|
|
42
|
+
inputs: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Outcome of `compileGoalToMission`. */
|
|
46
|
+
export interface PlannerResult {
|
|
47
|
+
/** When non-null, the compiled mission ready to run. */
|
|
48
|
+
mission: Mission | null;
|
|
49
|
+
/** Free-text explanation on failure (otherwise undefined). */
|
|
50
|
+
error?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Verbs the planner could parse from the goal but didn't act on
|
|
53
|
+
* (e.g. user asked for "navigate" which isn't bound to a runnable
|
|
54
|
+
* capability today). Surfaced so the agent can pick a different
|
|
55
|
+
* phrasing without re-prompting the user.
|
|
56
|
+
*/
|
|
57
|
+
unmatched_verbs?: string[];
|
|
58
|
+
/**
|
|
59
|
+
* The candidate matches the planner ranked. Always present; the
|
|
60
|
+
* first one wins when the planner picks a single step (multi-step
|
|
61
|
+
* patterns are explicit in `mission.steps`).
|
|
62
|
+
*/
|
|
63
|
+
candidates: PlannerCandidate[];
|
|
64
|
+
/**
|
|
65
|
+
* Hints surfaced to help the agent recover from a failed compile
|
|
66
|
+
* (e.g. "try 'take a picture' or 'follow me'"). Empty when the
|
|
67
|
+
* compile succeeds.
|
|
68
|
+
*/
|
|
69
|
+
suggestions: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface CompileOptions {
|
|
73
|
+
/** Optional mission name; defaults to "Goal: <goal>". */
|
|
74
|
+
mission_name?: string;
|
|
75
|
+
/** Optional default robot id propagated onto the mission. */
|
|
76
|
+
robot_id?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Canonical verb → list of synonyms / surface forms we recognise. */
|
|
80
|
+
const VERB_SYNONYMS: Record<string, readonly string[]> = {
|
|
81
|
+
find: [
|
|
82
|
+
"find",
|
|
83
|
+
"locate",
|
|
84
|
+
"look for",
|
|
85
|
+
"search for",
|
|
86
|
+
"where is",
|
|
87
|
+
"where's",
|
|
88
|
+
"spot",
|
|
89
|
+
"detect",
|
|
90
|
+
"scan for",
|
|
91
|
+
],
|
|
92
|
+
see: [
|
|
93
|
+
"take a picture",
|
|
94
|
+
"take a snapshot",
|
|
95
|
+
"take a photo",
|
|
96
|
+
"take picture",
|
|
97
|
+
"take snapshot",
|
|
98
|
+
"take photo",
|
|
99
|
+
"snap a picture",
|
|
100
|
+
"snap a photo",
|
|
101
|
+
"what do you see",
|
|
102
|
+
"what can you see",
|
|
103
|
+
"what does the camera see",
|
|
104
|
+
"show me what you see",
|
|
105
|
+
"show me the camera",
|
|
106
|
+
"see",
|
|
107
|
+
],
|
|
108
|
+
measure: [
|
|
109
|
+
"measure depth",
|
|
110
|
+
"measure distance",
|
|
111
|
+
"how far",
|
|
112
|
+
"how close",
|
|
113
|
+
"depth to",
|
|
114
|
+
"distance to",
|
|
115
|
+
],
|
|
116
|
+
follow: [
|
|
117
|
+
"follow me",
|
|
118
|
+
"follow the person",
|
|
119
|
+
"follow that person",
|
|
120
|
+
"follow whoever",
|
|
121
|
+
"follow",
|
|
122
|
+
"stay with me",
|
|
123
|
+
],
|
|
124
|
+
drive: [
|
|
125
|
+
"drive forward",
|
|
126
|
+
"drive backward",
|
|
127
|
+
"drive backwards",
|
|
128
|
+
"go forward",
|
|
129
|
+
"go backward",
|
|
130
|
+
"go backwards",
|
|
131
|
+
"back up",
|
|
132
|
+
"move forward",
|
|
133
|
+
"move backward",
|
|
134
|
+
"turn left",
|
|
135
|
+
"turn right",
|
|
136
|
+
"rotate left",
|
|
137
|
+
"rotate right",
|
|
138
|
+
"stop",
|
|
139
|
+
"halt",
|
|
140
|
+
"hold still",
|
|
141
|
+
"hold position",
|
|
142
|
+
],
|
|
143
|
+
list_topics: [
|
|
144
|
+
"list topics",
|
|
145
|
+
"list the topics",
|
|
146
|
+
"show topics",
|
|
147
|
+
"show me the topics",
|
|
148
|
+
"what topics",
|
|
149
|
+
"topic list",
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Multi-step conjunction patterns ("X and then Y", "X then Y", "X and Y"). */
|
|
154
|
+
const CONJUNCTIONS = [" and then ", ", then ", ". then ", " then ", " and "];
|
|
155
|
+
|
|
156
|
+
/** Default linear speed (m/s) for "drive forward" without an explicit value. */
|
|
157
|
+
const DEFAULT_LINEAR_SPEED = 0.2;
|
|
158
|
+
/** Default angular speed (rad/s) for "turn left" without an explicit value. */
|
|
159
|
+
const DEFAULT_ANGULAR_SPEED = 0.5;
|
|
160
|
+
|
|
161
|
+
function normalise(s: string): string {
|
|
162
|
+
return s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Detect the first matching verb synonym in the goal. Returns canonical verb + matched surface form. */
|
|
166
|
+
function detectVerb(goal: string): { verb: keyof typeof VERB_SYNONYMS; matched: string } | null {
|
|
167
|
+
const g = normalise(goal);
|
|
168
|
+
// Drive variants are checked before generic "go" prefixes so "go forward"
|
|
169
|
+
// doesn't accidentally swallow "go find a chair".
|
|
170
|
+
const ordered: Array<keyof typeof VERB_SYNONYMS> = [
|
|
171
|
+
"list_topics",
|
|
172
|
+
"follow",
|
|
173
|
+
"measure",
|
|
174
|
+
"see",
|
|
175
|
+
"drive",
|
|
176
|
+
"find",
|
|
177
|
+
];
|
|
178
|
+
for (const verb of ordered) {
|
|
179
|
+
for (const synonym of VERB_SYNONYMS[verb]) {
|
|
180
|
+
// Use word boundaries so "find" doesn't match inside "finder".
|
|
181
|
+
const re = new RegExp(`(^|\\b)${escapeRegex(synonym)}(\\b|$)`);
|
|
182
|
+
if (re.test(g)) return { verb, matched: synonym };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function escapeRegex(s: string): string {
|
|
189
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Extract the target object from a "find X" / "look for X" / "where is X" goal. */
|
|
193
|
+
function extractFindTarget(goal: string, matchedVerb: string): string | null {
|
|
194
|
+
const g = normalise(goal);
|
|
195
|
+
const idx = g.indexOf(matchedVerb);
|
|
196
|
+
if (idx < 0) return null;
|
|
197
|
+
let after = g.slice(idx + matchedVerb.length).trim();
|
|
198
|
+
// Strip "a"/"an"/"the" articles.
|
|
199
|
+
after = after.replace(/^(a |an |the )/, "").trim();
|
|
200
|
+
// Stop at conjunctions / sentence boundaries so "find a chair and drive…"
|
|
201
|
+
// captures only "chair".
|
|
202
|
+
for (const conj of CONJUNCTIONS) {
|
|
203
|
+
const c = after.indexOf(conj);
|
|
204
|
+
if (c >= 0) {
|
|
205
|
+
after = after.slice(0, c).trim();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Strip trailing punctuation.
|
|
210
|
+
after = after.replace(/[.,!?;:]+$/, "").trim();
|
|
211
|
+
if (after.length === 0) return null;
|
|
212
|
+
return after;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Compile a "drive ..." sub-goal into linear_x / angular_z inputs. */
|
|
216
|
+
function compileDrive(goal: string): Record<string, unknown> {
|
|
217
|
+
const g = normalise(goal);
|
|
218
|
+
let linear = 0;
|
|
219
|
+
let angular = 0;
|
|
220
|
+
|
|
221
|
+
if (/(^|\b)(stop|halt|hold (still|position))\b/.test(g)) {
|
|
222
|
+
return { linear_x: 0, angular_z: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Speed extraction: "at 0.3 m/s" / "0.5 m/s" / "at half speed".
|
|
226
|
+
const speedMatch = g.match(/(\d+(?:\.\d+)?)\s*(?:m\/s|meters? per second)/);
|
|
227
|
+
const explicitLinearSpeed = speedMatch ? Number(speedMatch[1]) : null;
|
|
228
|
+
|
|
229
|
+
// Linear direction.
|
|
230
|
+
if (/\b(forward|forwards|ahead)\b/.test(g) || /\b(go|drive|move)\s+forward/.test(g)) {
|
|
231
|
+
linear = explicitLinearSpeed ?? DEFAULT_LINEAR_SPEED;
|
|
232
|
+
}
|
|
233
|
+
if (/\b(backward|backwards|back\s*up|reverse)\b/.test(g)) {
|
|
234
|
+
linear = -(explicitLinearSpeed ?? DEFAULT_LINEAR_SPEED);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Angular direction.
|
|
238
|
+
const angularMatch = g.match(/(\d+(?:\.\d+)?)\s*(?:rad\/s|radians? per second)/);
|
|
239
|
+
const explicitAngularSpeed = angularMatch ? Number(angularMatch[1]) : null;
|
|
240
|
+
if (/\b(left|counter[-\s]?clockwise)\b/.test(g)) {
|
|
241
|
+
angular = explicitAngularSpeed ?? DEFAULT_ANGULAR_SPEED;
|
|
242
|
+
}
|
|
243
|
+
if (/\b(right|clockwise)\b/.test(g)) {
|
|
244
|
+
angular = -(explicitAngularSpeed ?? DEFAULT_ANGULAR_SPEED);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { linear_x: linear, angular_z: angular };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface SubStepResult {
|
|
251
|
+
step: MissionStep;
|
|
252
|
+
candidate: PlannerCandidate;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Try to compile one (possibly compound) goal segment into a single
|
|
257
|
+
* mission step. Returns null when no recognised verb maps to a
|
|
258
|
+
* provided capability.
|
|
259
|
+
*/
|
|
260
|
+
function compileSegment(
|
|
261
|
+
segment: string,
|
|
262
|
+
index: number,
|
|
263
|
+
availableIds: Set<string>,
|
|
264
|
+
): SubStepResult | null {
|
|
265
|
+
const detected = detectVerb(segment);
|
|
266
|
+
if (!detected) return null;
|
|
267
|
+
|
|
268
|
+
switch (detected.verb) {
|
|
269
|
+
case "find": {
|
|
270
|
+
if (!availableIds.has("find_object")) return null;
|
|
271
|
+
const target = extractFindTarget(segment, detected.matched);
|
|
272
|
+
if (!target) return null;
|
|
273
|
+
return {
|
|
274
|
+
step: {
|
|
275
|
+
id: stepId("find", index),
|
|
276
|
+
capability: "find_object",
|
|
277
|
+
inputs: { target },
|
|
278
|
+
},
|
|
279
|
+
candidate: {
|
|
280
|
+
capability_id: "find_object",
|
|
281
|
+
confidence: 0.85,
|
|
282
|
+
rationale: `matched verb "${detected.matched}" with target "${target}"`,
|
|
283
|
+
inputs: { target },
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
case "see": {
|
|
288
|
+
if (!availableIds.has("take_snapshot")) return null;
|
|
289
|
+
return {
|
|
290
|
+
step: { id: stepId("snap", index), capability: "take_snapshot" },
|
|
291
|
+
candidate: {
|
|
292
|
+
capability_id: "take_snapshot",
|
|
293
|
+
confidence: 0.9,
|
|
294
|
+
rationale: `matched verb "${detected.matched}"`,
|
|
295
|
+
inputs: {},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
case "measure": {
|
|
300
|
+
if (!availableIds.has("measure_depth")) return null;
|
|
301
|
+
return {
|
|
302
|
+
step: { id: stepId("depth", index), capability: "measure_depth" },
|
|
303
|
+
candidate: {
|
|
304
|
+
capability_id: "measure_depth",
|
|
305
|
+
confidence: 0.9,
|
|
306
|
+
rationale: `matched verb "${detected.matched}"`,
|
|
307
|
+
inputs: {},
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
case "follow": {
|
|
312
|
+
if (!availableIds.has("follow_person")) return null;
|
|
313
|
+
return {
|
|
314
|
+
step: { id: stepId("follow", index), capability: "follow_person" },
|
|
315
|
+
candidate: {
|
|
316
|
+
capability_id: "follow_person",
|
|
317
|
+
confidence: 0.9,
|
|
318
|
+
rationale: `matched verb "${detected.matched}"`,
|
|
319
|
+
inputs: {},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
case "drive": {
|
|
324
|
+
if (!availableIds.has("drive_base")) return null;
|
|
325
|
+
const inputs = compileDrive(segment);
|
|
326
|
+
// Reject a no-op drive (e.g. "drive" without direction) so we
|
|
327
|
+
// don't ship a confusing "drive nothing" step.
|
|
328
|
+
const lx = Number(inputs?.linear_x ?? 0);
|
|
329
|
+
const az = Number(inputs?.angular_z ?? 0);
|
|
330
|
+
// "stop" is a legit zero-velocity step; detect it before the no-op guard.
|
|
331
|
+
const isStop = /(^|\b)(stop|halt|hold (still|position))\b/.test(normalise(segment));
|
|
332
|
+
if (!isStop && lx === 0 && az === 0) return null;
|
|
333
|
+
return {
|
|
334
|
+
step: {
|
|
335
|
+
id: stepId("drive", index),
|
|
336
|
+
capability: "drive_base",
|
|
337
|
+
inputs,
|
|
338
|
+
},
|
|
339
|
+
candidate: {
|
|
340
|
+
capability_id: "drive_base",
|
|
341
|
+
confidence: isStop ? 0.95 : 0.8,
|
|
342
|
+
rationale: isStop
|
|
343
|
+
? `matched stop verb`
|
|
344
|
+
: `matched drive verb "${detected.matched}" → linear_x=${lx}, angular_z=${az}`,
|
|
345
|
+
inputs,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
case "list_topics": {
|
|
350
|
+
if (!availableIds.has("list_topics")) return null;
|
|
351
|
+
return {
|
|
352
|
+
step: { id: stepId("topics", index), capability: "list_topics" },
|
|
353
|
+
candidate: {
|
|
354
|
+
capability_id: "list_topics",
|
|
355
|
+
confidence: 0.95,
|
|
356
|
+
rationale: `matched verb "${detected.matched}"`,
|
|
357
|
+
inputs: {},
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function stepId(prefix: string, index: number): string {
|
|
366
|
+
return index === 0 ? prefix : `${prefix}${index + 1}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Split a compound goal on conjunctions ("X and Y", "X then Y") into segments. */
|
|
370
|
+
function splitOnConjunctions(goal: string): string[] {
|
|
371
|
+
let parts = [goal];
|
|
372
|
+
for (const conj of CONJUNCTIONS) {
|
|
373
|
+
const out: string[] = [];
|
|
374
|
+
for (const p of parts) {
|
|
375
|
+
out.push(...p.split(conj));
|
|
376
|
+
}
|
|
377
|
+
parts = out;
|
|
378
|
+
}
|
|
379
|
+
return parts.map((p) => p.trim()).filter((p) => p.length > 0);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Special-case the canonical compound pattern "find X and drive
|
|
384
|
+
* toward it" (or "approach it", "go to it"). The second step's
|
|
385
|
+
* angular_z is wired from the first step's horizontal_offset via a
|
|
386
|
+
* mission-runner template, so the robot actually steers toward the
|
|
387
|
+
* detected object instead of just plodding forward.
|
|
388
|
+
*/
|
|
389
|
+
function tryFindAndApproach(
|
|
390
|
+
goal: string,
|
|
391
|
+
availableIds: Set<string>,
|
|
392
|
+
): { steps: MissionStep[]; candidates: PlannerCandidate[] } | null {
|
|
393
|
+
if (!availableIds.has("find_object") || !availableIds.has("drive_base")) return null;
|
|
394
|
+
const g = normalise(goal);
|
|
395
|
+
const findVerbs = VERB_SYNONYMS.find;
|
|
396
|
+
const approachVerbs = [
|
|
397
|
+
"drive toward",
|
|
398
|
+
"drive to",
|
|
399
|
+
"approach",
|
|
400
|
+
"go toward",
|
|
401
|
+
"go to it",
|
|
402
|
+
"head toward",
|
|
403
|
+
"move toward",
|
|
404
|
+
];
|
|
405
|
+
// Pattern: <find-synonym> <target> ... <conjunction> ... <approach-synonym>
|
|
406
|
+
for (const find of findVerbs) {
|
|
407
|
+
const findIdx = g.indexOf(find);
|
|
408
|
+
if (findIdx < 0) continue;
|
|
409
|
+
const after = g.slice(findIdx + find.length);
|
|
410
|
+
// Approach verb must come AFTER the find verb in the same goal.
|
|
411
|
+
const approachHit = approachVerbs.find((v) => after.includes(v));
|
|
412
|
+
if (!approachHit) continue;
|
|
413
|
+
const conjIdx = CONJUNCTIONS.map((c) => after.indexOf(c)).filter((i) => i >= 0).sort((a, b) => a - b)[0];
|
|
414
|
+
// Either an explicit conjunction or just "find X. approach it" — accept both.
|
|
415
|
+
const targetSegment = conjIdx !== undefined
|
|
416
|
+
? after.slice(0, conjIdx).trim()
|
|
417
|
+
: after.split(approachHit)[0].trim();
|
|
418
|
+
const cleaned = targetSegment.replace(/^(a |an |the )/, "").replace(/[.,!?;:]+$/, "").trim();
|
|
419
|
+
if (!cleaned) continue;
|
|
420
|
+
return {
|
|
421
|
+
steps: [
|
|
422
|
+
{
|
|
423
|
+
id: "find",
|
|
424
|
+
capability: "find_object",
|
|
425
|
+
inputs: { target: cleaned },
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
id: "approach",
|
|
429
|
+
capability: "drive_base",
|
|
430
|
+
inputs: {
|
|
431
|
+
linear_x: DEFAULT_LINEAR_SPEED,
|
|
432
|
+
// Wire heading correction from find_object.outputs.horizontal_offset
|
|
433
|
+
// (positive = object right of centre → turn right → negative angular_z).
|
|
434
|
+
// The runner substitutes this template just before dispatch.
|
|
435
|
+
angular_z: "{{find.outputs.horizontal_offset}}",
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
candidates: [
|
|
440
|
+
{
|
|
441
|
+
capability_id: "find_object",
|
|
442
|
+
confidence: 0.9,
|
|
443
|
+
rationale: `compound goal: find "${cleaned}" → approach`,
|
|
444
|
+
inputs: { target: cleaned },
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
capability_id: "drive_base",
|
|
448
|
+
confidence: 0.85,
|
|
449
|
+
rationale: `approach step wired from find_object.outputs.horizontal_offset`,
|
|
450
|
+
inputs: { linear_x: DEFAULT_LINEAR_SPEED, angular_z: "{{find.outputs.horizontal_offset}}" },
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Recognised verbs the planner can handle today — surfaced in error messages. */
|
|
459
|
+
const RECOGNISED_VERBS_SUMMARY = [
|
|
460
|
+
"find / locate / look for <object>",
|
|
461
|
+
"take a picture / snapshot / what do you see",
|
|
462
|
+
"measure depth / how far",
|
|
463
|
+
"follow me / follow person",
|
|
464
|
+
"drive forward / backward, turn left / right, stop",
|
|
465
|
+
"list topics",
|
|
466
|
+
"find <object> and drive toward it (multi-step)",
|
|
467
|
+
];
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Compile a free-text goal into a runnable `Mission`.
|
|
471
|
+
*
|
|
472
|
+
* Returns a structured `PlannerResult`: when `mission` is non-null
|
|
473
|
+
* the caller can hand it straight to `runMission`; when it's null
|
|
474
|
+
* the caller should surface `error` + `suggestions` to the agent so
|
|
475
|
+
* it can self-correct.
|
|
476
|
+
*/
|
|
477
|
+
export function compileGoalToMission(
|
|
478
|
+
goal: string,
|
|
479
|
+
capabilities: Capability[],
|
|
480
|
+
options?: CompileOptions,
|
|
481
|
+
): PlannerResult {
|
|
482
|
+
const trimmed = (goal ?? "").trim();
|
|
483
|
+
if (!trimmed) {
|
|
484
|
+
return {
|
|
485
|
+
mission: null,
|
|
486
|
+
error: "Goal is empty. Provide a natural-language description of what the robot should do.",
|
|
487
|
+
candidates: [],
|
|
488
|
+
suggestions: RECOGNISED_VERBS_SUMMARY,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const availableIds = new Set(capabilities.map((c) => c.id));
|
|
493
|
+
|
|
494
|
+
// Compound find-then-approach is the showcase Phase 1.c pattern.
|
|
495
|
+
// Check it first so "find a chair and drive toward it" doesn't get
|
|
496
|
+
// demoted to two unrelated steps via the generic conjunction split.
|
|
497
|
+
const compound = tryFindAndApproach(trimmed, availableIds);
|
|
498
|
+
if (compound) {
|
|
499
|
+
return {
|
|
500
|
+
mission: buildMission(compound.steps, trimmed, options),
|
|
501
|
+
candidates: compound.candidates,
|
|
502
|
+
suggestions: [],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Generic multi-segment path: split on " and " / " then " etc and
|
|
507
|
+
// compile each segment independently. Any segment that fails to
|
|
508
|
+
// map to a capability is recorded as `unmatched_verbs` but does
|
|
509
|
+
// NOT abort the compile — the agent can re-issue a tighter goal.
|
|
510
|
+
const segments = splitOnConjunctions(trimmed);
|
|
511
|
+
const steps: MissionStep[] = [];
|
|
512
|
+
const candidates: PlannerCandidate[] = [];
|
|
513
|
+
const unmatched: string[] = [];
|
|
514
|
+
segments.forEach((segment, i) => {
|
|
515
|
+
const compiled = compileSegment(segment, i, availableIds);
|
|
516
|
+
if (compiled) {
|
|
517
|
+
steps.push(compiled.step);
|
|
518
|
+
candidates.push(compiled.candidate);
|
|
519
|
+
} else {
|
|
520
|
+
unmatched.push(segment);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
if (steps.length > 0) {
|
|
525
|
+
return {
|
|
526
|
+
mission: buildMission(steps, trimmed, options),
|
|
527
|
+
candidates,
|
|
528
|
+
...(unmatched.length > 0 ? { unmatched_verbs: unmatched } : {}),
|
|
529
|
+
suggestions: [],
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Nothing matched. Build a helpful error including the recognised
|
|
534
|
+
// verbs AND the verb registry the runtime has so the agent can
|
|
535
|
+
// refine. We list the first 12 capability ids to keep the response
|
|
536
|
+
// compact — extending the list when a registry grows is fine.
|
|
537
|
+
const knownCaps = capabilities.slice(0, 12).map((c) => `${c.id} (verb: ${c.verb})`);
|
|
538
|
+
return {
|
|
539
|
+
mission: null,
|
|
540
|
+
error:
|
|
541
|
+
`Couldn't compile goal "${trimmed}" into a runnable plan. ` +
|
|
542
|
+
`The planner is rule-based today and recognises the verbs below. ` +
|
|
543
|
+
`Try rephrasing, or call run_mission with an explicit mission.steps[].`,
|
|
544
|
+
candidates: [],
|
|
545
|
+
suggestions: [
|
|
546
|
+
...RECOGNISED_VERBS_SUMMARY,
|
|
547
|
+
`Available capabilities in this fleet: ${knownCaps.join(", ")}`,
|
|
548
|
+
],
|
|
549
|
+
...(unmatched.length > 0 ? { unmatched_verbs: unmatched } : {}),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function buildMission(
|
|
554
|
+
steps: MissionStep[],
|
|
555
|
+
goal: string,
|
|
556
|
+
options: CompileOptions | undefined,
|
|
557
|
+
): Mission {
|
|
558
|
+
return {
|
|
559
|
+
name: options?.mission_name ?? `Goal: ${goal}`,
|
|
560
|
+
goal,
|
|
561
|
+
...(options?.robot_id ? { robot_id: options.robot_id } : {}),
|
|
562
|
+
steps,
|
|
563
|
+
};
|
|
564
|
+
}
|