agenticros 0.1.11 → 0.1.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenticros",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "description": "AgenticROS - agentic AI for ROS-powered robots. Single CLI to launch real-robot or simulated demos, manage configuration, and inspect status.",
6
6
  "keywords": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "packedAt": "2026-06-06T14:01:37.009Z",
2
+ "packedAt": "2026-06-06T14:30:19.631Z",
3
3
  "repo": "https://github.com/PlaiPin/agenticros",
4
4
  "note": "This directory is a snapshot of the agenticros monorepo source. `agenticros init` will copy it to ~/agenticros and run pnpm install + colcon build there.",
5
5
  "layout": {
@@ -360,11 +360,40 @@ export async function handleToolCall(
360
360
  const topics = await transport.listTopics();
361
361
  const MAX = 50;
362
362
  const truncated = topics.length > MAX ? topics.slice(0, MAX) : topics;
363
+
364
+ // Surface task-specific control hints so the agent (and any LLM
365
+ // calling list_topics to figure out what's available) doesn't have to
366
+ // memorise the topic surface for each demo. Cheap to compute and only
367
+ // included when matching topics are actually present.
368
+ const hints: Record<string, string> = {};
369
+ const armJoints = topics
370
+ .filter((t) => /^\/arm\/[a-z0-9_]+\/cmd_pos$/i.test(t.name))
371
+ .map((t) => t.name.replace(/^\/arm\/(.+)\/cmd_pos$/, "$1"));
372
+ if (armJoints.length > 0) {
373
+ hints["arm"] =
374
+ `Detected the AgenticROS sim arm (joints: ${armJoints.join(", ")}). ` +
375
+ "To move a joint, call ros2_publish with topic '/arm/<joint>/cmd_pos', " +
376
+ "type 'std_msgs/msg/Float64', message {data: <radians>}. " +
377
+ "Example: rotate shoulder_pan 90 degrees left -> publish {data: 1.5707} " +
378
+ "to /arm/shoulder_pan/cmd_pos. Listen to /joint_states (sensor_msgs/msg/JointState) " +
379
+ "to read current positions.";
380
+ }
381
+ const baseTwistTopics = topics.filter(
382
+ (t) => /\/cmd_vel$/.test(t.name) && t.type === "geometry_msgs/msg/Twist",
383
+ );
384
+ if (baseTwistTopics.length > 0) {
385
+ hints["base"] =
386
+ "To drive the base, publish geometry_msgs/msg/Twist to a cmd_vel topic. " +
387
+ `Available cmd_vel topics: ${baseTwistTopics.map((t) => t.name).join(", ")}. ` +
388
+ "When in sim mode the unnamespaced /cmd_vel is the one the simulator listens on.";
389
+ }
390
+
363
391
  const text = JSON.stringify({
364
392
  success: true,
365
393
  topics: truncated,
366
394
  total: topics.length,
367
395
  truncated: topics.length > MAX,
396
+ ...(Object.keys(hints).length > 0 ? { hints } : {}),
368
397
  });
369
398
  return { content: [{ type: "text", text }] };
370
399
  }
@@ -91,7 +91,11 @@ export class LocalTransport implements RosTransport {
91
91
  // Give DDS discovery a brief moment to receive announcements from peers
92
92
  // before we report "connected". Without this the very first listTopics()
93
93
  // can race the executor and return empty even though peers exist.
94
- await new Promise<void>((resolve) => setTimeout(resolve, 750));
94
+ // 1500ms was chosen empirically on Jetson + Humble where /arm/*/cmd_pos
95
+ // topics from a sim_arm gz_bridge child take ~1.2s to propagate via
96
+ // multicast (vs ~400ms on x86); the prior 750ms cap caused first
97
+ // list_topics() calls to miss them.
98
+ await new Promise<void>((resolve) => setTimeout(resolve, 1500));
95
99
 
96
100
  this.setStatus("connected");
97
101
  } catch (err) {
@@ -280,24 +284,41 @@ export class LocalTransport implements RosTransport {
280
284
  async listTopics(): Promise<TopicInfo[]> {
281
285
  this.ensureConnected();
282
286
 
283
- // DDS discovery is asynchronous. On a freshly-connected node,
284
- // getTopicNamesAndTypes() trickles in results as peers respond - the
285
- // first call may return 0, the second 2, the third all of them.
286
- // Poll until the count is stable across two consecutive 300ms reads (or
287
- // time-bounded to ~3s) so we don't return a partial view.
287
+ // DDS discovery is asynchronous AND stateful. On a freshly-connected
288
+ // node, getTopicNamesAndTypes() trickles in results as peers respond -
289
+ // the first call may return 0, the second 2, the third all of them.
290
+ //
291
+ // Two issues motivated the current tuning:
292
+ //
293
+ // 1. A short single-stable-hit policy (600ms) was returning truncated
294
+ // views to Claude: e.g. with the AMR sim alone it'd see /cmd_vel
295
+ // etc. on first call, then a second call 2s later would see all
296
+ // 29 topics including /arm/*/cmd_pos. Claude only calls
297
+ // list_topics once and was missing arm joint topics entirely.
298
+ //
299
+ // 2. DDS multicast announcements on Jetson take longer than on x86 -
300
+ // we routinely see /arm/* topics appear ~1.5s after first call.
301
+ //
302
+ // Fix: require 3 consecutive matching polls (2 stable hits at 300ms
303
+ // intervals = ~900ms of stability), with a 6 s deadline. Worst case
304
+ // ~900ms latency on a steady graph, ~5s+ if discovery is still
305
+ // streaming. The min-count guard (>=3) avoids exiting early when only
306
+ // /clock + /tf + /tf_static have come through.
288
307
  const externalFilter = (t: { name: string; types: string[] }) =>
289
308
  !INTERNAL_TOPIC_PREFIXES.some((prefix) => t.name.startsWith(prefix));
290
309
 
291
- const deadline = Date.now() + 3000;
310
+ const deadline = Date.now() + 6000;
311
+ const MIN_STABLE_COUNT = 3;
312
+ const REQUIRED_STABLE_HITS = 2;
292
313
  let raw: Array<{ name: string; types: string[] }> = [];
293
314
  let prevCount = -1;
294
315
  let stableHits = 0;
295
316
  while (Date.now() < deadline) {
296
317
  raw = this.node.getTopicNamesAndTypes();
297
318
  const externalCount = raw.filter(externalFilter).length;
298
- if (externalCount === prevCount && externalCount > 0) {
319
+ if (externalCount === prevCount && externalCount >= MIN_STABLE_COUNT) {
299
320
  stableHits++;
300
- if (stableHits >= 1) break; // one stable confirmation is enough
321
+ if (stableHits >= REQUIRED_STABLE_HITS) break;
301
322
  } else {
302
323
  stableHits = 0;
303
324
  prevCount = externalCount;