Skip to main content

phoxal_api/
lib.rs

1//! The single API layer (D60/D61/D1).
2//!
3//! This crate is the versioned API contract tree. It depends only on the
4//! [`phoxal-bus`](phoxal_bus) ABI floor (the contract primitive traits and the
5//! typed-topic builders) and the [`phoxal-macros`](phoxal_macros) proc-macros; it
6//! does **not** depend on the `phoxal` engine. Normal participants import the
7//! train-selected facade with `use phoxal::api`; concrete modules such as
8//! `phoxal_api::v0_1` remain available to compatibility adapters.
9//!
10//! # Concrete API revisions
11//!
12//! An API revision is a conventional `vM_N` module generated by
13//! [`phoxal_api_tree!`]. Each version module carries:
14//!
15//! - a zero-variant marker `enum Api {}` implementing [`ApiVersion`], whose
16//!   [`ApiVersion::ID`] is the concrete wire identity (for example `"v0.1"`);
17//! - the version-local wire bodies, one `pub mod` per contract node holding plain
18//!   serde structs/enums and their [`ContractBody`] impls;
19//! - an api-local `topic` builder rooted at `topic::new()`.
20//!
21//! Published concrete revisions are immutable. A child may extend one earlier
22//! revision; the generator materializes the complete child tree with its own
23//! identity. Exactly one `latest` alias is selected for each framework train.
24//!
25//! [`Api`]: v0_2::Api
26//!
27//! # Train-selected revision and per-contract identity
28//!
29//! A participant declares its bus surface with a companion
30//! `#[derive(phoxal::Api)]` handle struct.
31//! Official handle fields name types through the complete train-selected facade.
32//! The derive records each field's resolved [`ContractBody::VERSION`] and
33//! [`ContractBody::CONTRACT`] in the participant's embedded metadata and does
34//! not declare a participant-wide API version.
35//! Across the graph, compatibility is **name identity** (D1) - two participants
36//! interoperate on a contract iff they use the exact same version-qualified name
37//! (`v0.1::drive::Target`), which is real on the wire because the revision
38//! is folded into the key ([`ContractBody::TOPIC`]). There is no `schema_id`: a
39//! stable contract type is immutable, so the name alone is the whole identity.
40//!
41//! # Plain serde wire bodies, provenance in metadata
42//!
43//! A wire body is just its serde encoding - there is no `{"v":…}` envelope or any
44//! other version tag inside the payload (D62). Identity lives entirely in the
45//! Zenoh key (the version-qualified [`ContractBody::TOPIC`]); the bus metadata
46//! alongside the encoded body carries only provenance (source + logical time) and
47//! the codec that produced the bytes - never schema/family/version. Keeping
48//! identity out of both the payload and the metadata means the body bytes for an
49//! unchanged contract are identical across codecs, and a receiver's per-key
50//! subscription is the whole fast-reject.
51//!
52//! # Topic
53//!
54//! [`ContractBody::TOPIC`] is derived from the contract node's path in the tree,
55//! never written by hand: the version, then the `/`-joined node path plus the
56//! topic leaf, with each dynamic node contributing a `{var}` placeholder, e.g.
57//! `v0.1/component/{instance}/motor/{capability}/command`. A fully static path
58//! has a literal key (`v0.1/drive/state`). Folding the revision into the key
59//! (D1) is what makes two differently-versioned contracts physically distinct
60//! Zenoh keys - they cannot collide, so there is no `SCHEMA_ID`/`FAMILY` needed to
61//! disambiguate them.
62//!
63//! # The api-local topic builder
64//!
65//! Each version module exposes a `topic` builder that mirrors the node tree:
66//! `api::topic::new()` returns a root, one method per top-level node walks down the
67//! tree, a dynamic node's method takes its variable as `impl Display`, and a leaf
68//! method binds the topic's side-branded kind to its version-local body. For
69//! example `api::topic::new().drive().state()` yields a
70//! `Topic<Subscribe<drive::State>>` (the CLIENT observes the owner's `state`) over
71//! the version-qualified key `v0.1/drive/state`, and
72//! `api::topic::new().component("base").motor("left").command()` fills the dynamic
73//! segments to produce `v0.1/component/base/motor/left/command`. Because the
74//! builder is generated from the same tree as `TOPIC`, the built key and the
75//! documented key stay in lockstep.
76//!
77//! ## Owner side: `topic::internal`
78//!
79//! The PUBLIC `topic::new()...` chain above is the **client** side. The matching
80//! **owner** side lives at `api::topic::internal::new(cap)...` (L1 + L2, plan #00):
81//! the same node tree and keys, but the leaf brands flip so the owner gets the side
82//! it must take - `api::topic::internal::new(cap).drive().state()` is
83//! `Topic<Publish<drive::State>>` (the owner publishes its telemetry), and
84//! `api::topic::internal::new(cap).drive().target()` is `Topic<Subscribe<drive::Target>>`
85//! (the owner reads its command input). A query owner reaches its `ServeQuery`
86//! brand the same way. The `internal` chain is the deliberate, greppable owner
87//! opt-in; a participant acquires the topics of its OWN node through it and everything
88//! it consumes through the public chain.
89//!
90//! The `internal::new` entry requires the runner-minted owner capability
91//! ([`OwnerCap`](phoxal_bus::OwnerCap), Layer 2): a participant obtains it from
92//! `phoxal::SetupContext::owner_capability()` and passes it in. On the documented
93//! surface, owning a topic therefore cannot happen by accident - only with a
94//! capability the runner mints.
95
96use phoxal_macros::phoxal_api_tree;
97
98/// The contract primitive traits, re-exported from the `phoxal-bus` crate (the
99/// ABI floor) so they stay addressable at `phoxal_api::ApiVersion` /
100/// `phoxal_api::ContractBody`.
101///
102/// - [`ApiVersion`] is the marker trait identifying one API version (D60),
103///   implemented only by the zero-variant `enum Api {}` that [`phoxal_api_tree!`]
104///   generates inside each revision module; its `ID` is the concrete dotted
105///   wire identity (for example `"v0.1"`).
106/// - [`ContractBody`] is a version-local wire body (D61): a plain serde type
107///   bound to exactly one [`ApiVersion`] and one contract topic. Every body
108///   declared inside a [`phoxal_api_tree!`] node gets a generated impl; handles,
109///   `SetupContext` builders, and the `Service`/`Driver` derive assertions key
110///   off its `Api`/`TOPIC`. `TOPIC` is version-qualified (D1) and *is* the
111///   compatibility key - there is no `SCHEMA_ID`/`FAMILY`; its serde encoding
112///   *is* the wire payload, with no version envelope (D62).
113pub use phoxal_bus::{ApiVersion, ContractBody};
114
115phoxal_api_tree! {
116    version v0_1 {
117        drive {
118            /// Why actuation authority is in its current state.
119            enum StopReason {
120                NoTarget,
121                TargetStale,
122                TargetFromFuture,
123                TargetNotFinite,
124                ActuatorCommandNotFinite,
125                Inactive,
126                EmergencyStop,
127                Fault,
128            }
129
130            /// Whether the drive is actively commanding the actuators.
131            enum ActuatorAuthority {
132                Active,
133                Stopped,
134            }
135
136            /// A requested or limited planar velocity.
137            struct Target {
138                linear_x_mps: f32,
139                angular_z_radps: f32,
140                curvature_limit_radpm: Option<f32>,
141            }
142
143            /// The drive participant's published control state.
144            struct State {
145                target: Target,
146                limited_target: Target,
147                actuator_authority: ActuatorAuthority,
148                stop_reason: Option<StopReason>,
149                target_age_ns: Option<u64>,
150            }
151
152            topic target: command Target;
153            topic state: state State;
154        }
155
156        joint(joint) {
157            /// Per-joint position/velocity (and optional effort) on a dynamic
158            /// per-joint key.
159            struct JointState {
160                position_rad: f64,
161                velocity_radps: f64,
162                effort_nm: Option<f64>,
163            }
164
165            topic state: state JointState;
166        }
167
168        frame {
169            /// A parent → child rigid transform (translation + xyzw quaternion).
170            struct FrameTransform {
171                parent_frame_id: String,
172                child_frame_id: String,
173                translation_m: [f64; 3],
174                rotation_quat_xyzw: [f64; 4],
175                stamp_ns: Option<u64>,
176            }
177
178            /// Transforms that do not change over time.
179            struct StaticTransforms {
180                transforms: Vec<FrameTransform>,
181            }
182
183            /// The current transform tree.
184            struct Tree {
185                transforms: Vec<FrameTransform>,
186            }
187
188            /// Ask for the transform between two frames, optionally at a time.
189            struct LookupRequest {
190                target_frame_id: String,
191                source_frame_id: String,
192                at_ns: Option<u64>,
193            }
194
195            /// The resolved transform, or `None` if it is not available.
196            struct LookupResponse {
197                transform: Option<FrameTransform>,
198            }
199
200            topic tree: state Tree;
201            topic static_transforms: state StaticTransforms;
202            topic lookup: query LookupRequest => LookupResponse;
203        }
204
205        power {
206            /// A platform power command.
207            #[derive(Copy, Eq)]
208            enum Command {
209                Reboot,
210                Shutdown,
211            }
212
213            /// Where the power participant is in handling a command.
214            #[derive(Copy, Eq)]
215            enum Status {
216                Idle,
217                Rebooting,
218                ShuttingDown,
219                Failed,
220            }
221
222            /// Why a power command was rejected outright.
223            #[derive(Copy, Eq)]
224            #[serde(rename_all = "snake_case")]
225            enum RejectedReason {
226                HostIntegrationUnavailable,
227                CommandRejected,
228            }
229
230            /// Why an accepted power command later failed.
231            #[derive(Copy, Eq)]
232            #[serde(rename_all = "snake_case")]
233            enum FailedReason {
234                HostCommandFailed,
235            }
236
237            /// The power participant's published state.
238            struct State {
239                status: Status,
240                detail: Option<String>,
241            }
242
243            topic command: command Command;
244            topic state: state State;
245        }
246
247        motion {
248            struct Target {
249                linear_x_mps: f32,
250                angular_z_radps: f32,
251                curvature_limit_radpm: Option<f32>,
252            }
253
254            #[derive(Copy, Eq)]
255            #[serde(rename_all = "snake_case")]
256            enum Source {
257                Manual,
258                Navigation,
259                EmergencyStop,
260            }
261
262            #[derive(Copy, Eq)]
263            #[serde(rename_all = "snake_case")]
264            enum ZeroReason {
265                NoCandidate,
266                ManualCandidateStale,
267                NavigationCandidateStale,
268                ManualCandidateFromFuture,
269                NavigationCandidateFromFuture,
270                ManualCandidateNotFinite,
271                NavigationCandidateNotFinite,
272                EmergencyStopEngaged,
273                SafetyConstraintsUnavailable,
274                SafetyProtectiveStop,
275            }
276
277            #[derive(Copy, Eq)]
278            #[serde(rename_all = "snake_case")]
279            enum SafetyRuntime {
280                Absent,
281                Present,
282            }
283
284            struct ManualCommand {
285                linear_x_mps: f64,
286                angular_z_radps: f64,
287            }
288
289            #[derive(Eq)]
290            struct EmergencyStopRequest {
291                engaged: bool,
292            }
293
294            struct State {
295                manual_candidate_age_ns: Option<u64>,
296                autonomous_candidate_age_ns: Option<u64>,
297                safety_constraints_age_ns: Option<u64>,
298                selected_source: Option<Source>,
299                final_target: Target,
300                zero_reason: Option<ZeroReason>,
301                safety_runtime: SafetyRuntime,
302                software_estop_engaged: bool,
303                component_estop_blocked: bool,
304                active_safety_constraints: Vec<super::safety::Constraint>,
305            }
306
307            topic manual: command ManualCommand;
308            topic estop: command EmergencyStopRequest;
309            topic state: state State;
310        }
311
312        safety {
313            /// Why safety is stopping or limiting body motion.
314            #[derive(Copy, Eq)]
315            #[serde(rename_all = "snake_case")]
316            enum ConstraintReason {
317                WorldUnavailable,
318                MapUnavailable,
319                DrivableSpaceUnavailable,
320                LocalizationUnavailable,
321                LocalizationUncertain,
322                ObstacleProximity,
323                RangeSensorFault,
324                DriveFault,
325                BatteryLow,
326                BatteryCritical,
327                SpeedZone,
328                OperatorPolicy,
329            }
330
331            /// Typed origin of one constraint, suitable for operator diagnosis.
332            #[derive(Copy, Eq)]
333            #[serde(rename_all = "snake_case")]
334            enum ConstraintSourceKind {
335                WorldModel,
336                Map,
337                Localization,
338                Range,
339                Drive,
340                Battery,
341                Operator,
342            }
343
344            struct ConstraintSource {
345                kind: ConstraintSourceKind,
346                participant_id: String,
347                component_id: Option<String>,
348                capability_id: Option<String>,
349            }
350
351            struct Constraint {
352                reason: ConstraintReason,
353                source: ConstraintSource,
354                stop: bool,
355                max_linear_speed_mps: Option<f32>,
356                max_angular_speed_radps: Option<f32>,
357                observed_value: Option<f32>,
358                valid_from_ns: u64,
359                expires_at_ns: u64,
360            }
361
362            /// The sole safety-to-motion control product. Motion accepts it only
363            /// in the same epoch and before `expires_at_ns`.
364            struct MotionConstraints {
365                sequence: u64,
366                stop: bool,
367                max_linear_speed_mps: Option<f32>,
368                max_angular_speed_radps: Option<f32>,
369                constraints: Vec<Constraint>,
370                expires_at_ns: u64,
371            }
372
373            /// Operator-facing state mirrors the exact product consumed by motion.
374            struct State {
375                clear: bool,
376                motion: MotionConstraints,
377            }
378
379            topic constraints: state MotionConstraints;
380            topic state: state State;
381        }
382
383        navigation {
384            #[derive(Eq)]
385            struct RequestId {
386                value: String,
387            }
388
389            struct Pose {
390                x_m: f64,
391                y_m: f64,
392                yaw_rad: Option<f64>,
393            }
394
395            struct Path {
396                poses: Vec<Pose>,
397                map_revision: Option<u64>,
398            }
399
400            enum RequestKind {
401                GotoPose(Pose),
402                FollowPath(Path),
403                Cancel(RequestId),
404            }
405
406            struct Request {
407                request_id: RequestId,
408                kind: RequestKind,
409            }
410
411            enum State {
412                Idle,
413                Accepted(RequestId),
414                Running(RequestId),
415            }
416
417            #[derive(Copy, Eq)]
418            #[serde(rename_all = "snake_case")]
419            enum FailureReason {
420                LocalizationUnavailable,
421                MapUnavailable,
422                MapChanged,
423                NoPath,
424                Blocked,
425                Internal,
426            }
427
428            #[derive(Copy, Eq)]
429            #[serde(rename_all = "snake_case")]
430            enum RefusalReason {
431                Busy,
432                InvalidRequest,
433                Unsupported,
434            }
435
436            enum Outcome {
437                Succeeded,
438                Failed(FailureReason),
439                Refused(RefusalReason),
440                Cancelled,
441                TimedOut,
442            }
443
444            struct Progress {
445                request_id: RequestId,
446                distance_remaining_m: f64,
447                path_index: u32,
448            }
449
450            struct Result {
451                request_id: RequestId,
452                outcome: Outcome,
453            }
454
455            struct Candidate {
456                request_id: RequestId,
457                linear_x_mps: f32,
458                angular_z_radps: f32,
459            }
460
461            struct FrontierRequest {
462                map_revision: Option<u64>,
463            }
464
465            struct Frontier {
466                x_m: f64,
467                y_m: f64,
468                score: f32,
469                size: u32,
470            }
471
472            struct FrontierResponse {
473                frontier: Option<Frontier>,
474                map_revision: Option<u64>,
475            }
476
477            topic request: command Request;
478            topic state: state State;
479            topic progress: state Progress;
480            topic result: state Result;
481            topic candidate: state Candidate;
482            topic next_frontier: query FrontierRequest => FrontierResponse;
483        }
484
485        behavior {
486            #[derive(Eq)]
487            struct RequestId {
488                value: String,
489            }
490
491            #[derive(Copy, Eq)]
492            #[serde(rename_all = "snake_case")]
493            enum ConflictPolicy {
494                Reject,
495                Queue,
496                Interrupt,
497            }
498
499            enum Value {
500                Bool(bool),
501                Integer(i64),
502                Number(f64),
503                String(String),
504                Pose(super::navigation::Pose),
505            }
506
507            struct Request {
508                request_id: RequestId,
509                behavior_id: String,
510                args: ::std::collections::BTreeMap<String, Value>,
511                priority: u8,
512                conflict_policy: ConflictPolicy,
513            }
514
515            enum Command {
516                Pause,
517                Resume,
518                Cancel,
519            }
520
521            #[derive(Copy, Eq)]
522            #[serde(rename_all = "snake_case")]
523            enum ExecutionStatus {
524                Idle,
525                Running,
526                Paused,
527                Succeeded,
528                Failed,
529                Cancelled,
530                Abandoned,
531            }
532
533            #[derive(Copy, Eq)]
534            #[serde(rename_all = "snake_case")]
535            enum NodeStatus {
536                Idle,
537                Running,
538                Succeeded,
539                Failed,
540                Skipped,
541                Waiting,
542                Cancelling,
543            }
544
545            #[derive(Copy, Eq)]
546            #[serde(rename_all = "snake_case")]
547            enum FailureReason {
548                MissingCapability,
549                ActionRefused,
550                ActionFailed,
551                ActionTimedOut,
552                ActionCancelled,
553                ConditionFailed,
554                SafetyStopped,
555                EmergencyStopped,
556                ResourceConflict,
557                InvalidArgument,
558                InvalidBlackboardValue,
559                SubtreeFailed,
560                ExecutionAbandoned,
561                InternalError,
562            }
563
564            struct Failure {
565                reason: FailureReason,
566                detail: Option<String>,
567                node_path: Option<String>,
568                action_id: Option<String>,
569            }
570
571            struct DefinitionRef {
572                id: String,
573                version: String,
574                content_hash: String,
575            }
576
577            struct State {
578                execution_id: Option<String>,
579                root_behavior_id: Option<String>,
580                active_request_id: Option<RequestId>,
581                active_behavior_id: Option<String>,
582                status: ExecutionStatus,
583                active_node_path: Option<String>,
584                failure: Option<Failure>,
585            }
586
587            struct Snapshot {
588                execution_id: Option<String>,
589                root: Option<DefinitionRef>,
590                definition_stack: Vec<DefinitionRef>,
591                active_request_id: Option<RequestId>,
592                active_behavior_id: Option<String>,
593                status: ExecutionStatus,
594                node_statuses: ::std::collections::BTreeMap<String, NodeStatus>,
595                active_node_path: Option<String>,
596                blackboard: ::std::collections::BTreeMap<String, Value>,
597                args: ::std::collections::BTreeMap<String, Value>,
598                started_at_ns: Option<u64>,
599                updated_at_ns: u64,
600                failure: Option<Failure>,
601            }
602
603            enum EventKind {
604                ExecutionStarted,
605                ExecutionPaused,
606                ExecutionResumed,
607                ExecutionCompleted,
608                ExecutionCancelled,
609                ExecutionAbandoned,
610                NodeTransition(NodeStatus),
611                RequestAccepted,
612                RequestCompleted(ExecutionStatus),
613                RequestRejected(FailureReason),
614            }
615
616            struct Event {
617                sequence: u64,
618                execution_id: Option<String>,
619                request_id: Option<RequestId>,
620                behavior_id: Option<String>,
621                content_hash: Option<String>,
622                node_path: Option<String>,
623                kind: EventKind,
624                failure: Option<Failure>,
625                participant_id: String,
626                logical_time_ns: u64,
627            }
628
629            topic command: command Command;
630            topic request: command Request;
631            topic state: state State;
632            topic snapshot: state Snapshot;
633            topic event: state Event;
634        }
635
636        logs(participant_id) {
637            /// Wall-clock timestamp carried by a structured bus log event.
638            struct Timestamp {
639                unix_seconds: i64,
640                nanos: u32,
641            }
642
643            /// The severity level of a structured bus log event.
644            #[derive(Copy, Eq)]
645            #[serde(rename_all = "snake_case")]
646            enum Level {
647                Error,
648                Warn,
649                Info,
650                Debug,
651                Trace,
652            }
653
654            /// A scalar tracing field value captured from a log event.
655            #[serde(untagged)]
656            enum LogValue {
657                Bool(bool),
658                I64(i64),
659                U64(u64),
660                F64(f64),
661                String(String),
662            }
663
664            /// One structured runner log event published out-of-band.
665            struct Event {
666                seq: u64,
667                time: Timestamp,
668                level: Level,
669                target: String,
670                message: String,
671                fields: ::std::collections::BTreeMap<String, LogValue>,
672                /// Complete records lost before publication because a bounded
673                /// queue or publish attempt was saturated.
674                dropped: u32,
675                /// Values or fields truncated inside this published record to
676                /// keep its wire representation bounded.
677                #[serde(default)]
678                truncated: u32,
679            }
680
681            topic self: state Event;
682        }
683
684        tool {
685            /// Opaque identity for one retention-tool process together with a
686            /// position in its completed follow stream. A snapshot cursor
687            /// covers the retained completed items; a bus snapshot's optional
688            /// `current` window deliberately has the next sequence. Consumers
689            /// compare `generation` for equality only and must never parse or
690            /// order it.
691            #[derive(Eq)]
692            struct Cursor {
693                generation: String,
694                sequence: u64,
695            }
696
697            /// Which side of one participant-local bus buffer a runtime row
698            /// measures. The version-qualified `topic` field remains the wire
699            /// identity; direction is never inferred from its spelling.
700            #[derive(Copy, Eq, Ord, PartialOrd)]
701            #[serde(rename_all = "snake_case")]
702            enum RuntimeDirection {
703                Publish,
704                Subscribe,
705                /// Used only by the bounded overflow row, which may combine
706                /// omitted rows from both directions.
707                Mixed,
708            }
709
710            /// The concrete bounded buffer whose pressure a runtime row
711            /// measures.
712            #[derive(Copy, Eq, Ord, PartialOrd)]
713            #[serde(rename_all = "snake_case")]
714            enum RuntimeBufferKind {
715                /// Per-topic view of the one shared process outbound queue.
716                /// Its sample capacity is repeated on each row and must not be
717                /// summed; the queue's separate byte pressure is not in v0.1.
718                Outbound,
719                /// Keep-last slot. Depth `1` means occupied, never backlog.
720                Latest,
721                Subscriber,
722                /// Used only by the bounded overflow row.
723                Mixed,
724            }
725
726            /// Host-monotonic scheduled-step work completed during one rollup
727            /// window. An unscheduled participant reports `None` instead.
728            struct RuntimeStep {
729                target_period_ns: u64,
730                completed: u64,
731                errors: u64,
732                mean_duration_ns: u64,
733                max_duration_ns: u64,
734                mean_lateness_ns: u64,
735                max_lateness_ns: u64,
736                missed_ticks: u64,
737                overruns: u64,
738            }
739
740            /// One exact version-qualified topic/direction/buffer row. These
741            /// are process-lifetime setup declarations: dropping an authoring
742            /// handle does not dynamically unregister a row. Empty `topic`
743            /// plus `Mixed` direction/kind identifies the explicit overflow
744            /// row; `overflowed_rows` is zero on normal rows.
745            struct RuntimeTopic {
746                topic: String,
747                direction: RuntimeDirection,
748                buffer_kind: RuntimeBufferKind,
749                count: u64,
750                /// Finite, non-negative message rate. Retention tools clamp
751                /// malformed non-finite inputs before they reach snapshots.
752                rate_hz: f32,
753                drops: u64,
754                latest_overwrites: u64,
755                bounded_evictions: u64,
756                /// Sample capacity. Outbound rows repeat the shared process
757                /// queue capacity and are non-additive; byte pressure is not
758                /// represented. Latest capacity/depth describe slot occupancy.
759                capacity: u64,
760                current_depth: u64,
761                high_water_depth: u64,
762                decode_errors: u64,
763                overflowed_rows: u32,
764            }
765
766            log {
767                /// Requests tool-log's complete current bounded snapshot. The
768                /// first protocol version intentionally has no pagination or
769                /// filtering surface.
770                struct SnapshotRequest {}
771
772                /// Wall-clock timestamp copied from one participant-originated
773                /// structured `v0.1::logs` event.
774                struct Timestamp {
775                    unix_seconds: i64,
776                    nanos: u32,
777                }
778
779                #[derive(Copy, Eq)]
780                #[serde(rename_all = "snake_case")]
781                enum Level {
782                    Error,
783                    Warn,
784                    Info,
785                    Debug,
786                    Trace,
787                }
788
789                #[serde(untagged)]
790                enum LogValue {
791                    Bool(bool),
792                    I64(i64),
793                    U64(u64),
794                    F64(f64),
795                    String(String),
796                }
797
798                /// One retained participant log. `sequence` is assigned by
799                /// tool-log at ingest and is independent of the producer's
800                /// `source_sequence`.
801                struct Record {
802                    sequence: u64,
803                    participant_id: String,
804                    source_sequence: u64,
805                    time: Timestamp,
806                    level: Level,
807                    target: String,
808                    message: String,
809                    fields: ::std::collections::BTreeMap<String, LogValue>,
810                    dropped: u32,
811                    truncated: u32,
812                }
813
814                /// The complete bounded tool-log state at `cursor`.
815                struct Snapshot {
816                    cursor: crate::v0_1::tool::Cursor,
817                    /// Cumulative structured log samples evicted from
818                    /// tool-log's bounded ingest subscriber in this process.
819                    /// An increase is observable, unrecoverable source loss;
820                    /// it is distinct from producer-side `Record::dropped`.
821                    ingest_dropped: u64,
822                    records: Vec<Record>,
823                }
824
825                /// One live record following the snapshot query. A consumer
826                /// must re-query when the generation changes or the sequence is
827                /// not exactly one after its installed cursor.
828                struct Follow {
829                    cursor: crate::v0_1::tool::Cursor,
830                    /// Current cumulative tool-log ingest loss counter.
831                    ingest_dropped: u64,
832                    record: Record,
833                }
834
835                topic snapshot: query SnapshotRequest => Snapshot;
836                topic follow: state Follow;
837            }
838
839            bus {
840                /// Requests tool-bus's complete current bounded snapshot. The
841                /// first protocol version intentionally has no pagination or
842                /// filtering surface.
843                struct SnapshotRequest {}
844
845                /// One topic-producer pair measured during a one-second window.
846                /// Empty topic and producer identify the bounded overflow row.
847                struct TopicMetric {
848                    topic: String,
849                    from_participant: String,
850                    ingress_rate_hz: f32,
851                    count: u64,
852                }
853
854                /// One retained bus-rate window. `window_ns` is a duration, not
855                /// a production timestamp.
856                struct Window {
857                    sequence: u64,
858                    topics: Vec<TopicMetric>,
859                    throughput_msg_s: f32,
860                    window_ns: u64,
861                }
862
863                /// The current partial counters plus the bounded recent
864                /// completed-window history. The partial `current` window has
865                /// the next sequence that its eventual follow item will use;
866                /// the snapshot cursor covers only `windows`.
867                struct Snapshot {
868                    cursor: crate::v0_1::tool::Cursor,
869                    current: Option<Window>,
870                    windows: Vec<Window>,
871                }
872
873                /// One newly completed bus-rate window.
874                struct Follow {
875                    cursor: crate::v0_1::tool::Cursor,
876                    window: Window,
877                }
878
879                topic snapshot: query SnapshotRequest => Snapshot;
880                topic follow: state Follow;
881            }
882
883            device {
884                /// One mounted filesystem capacity observation. The contract
885                /// intentionally excludes OS-specific device names and I/O
886                /// counters; an unavailable storage inventory is represented
887                /// by `Sample::disks == None`, never fabricated zero rows.
888                struct Disk {
889                    mount_point: String,
890                    file_system: String,
891                    used_bytes: u64,
892                    total_bytes: u64,
893                }
894
895                /// Portable whole-device observations produced by one
896                /// runner-owned tool-device. Every optional field is `None`
897                /// when the current platform cannot provide a truthful value.
898                /// These totals must never be attributed to a runtime.
899                struct Sample {
900                    /// Logical device identity within the robot root. The
901                    /// current execution environment is always `main`.
902                    device_id: String,
903                    cpu_pct: Option<f32>,
904                    ram_used_bytes: Option<u64>,
905                    ram_total_bytes: Option<u64>,
906                    swap_used_bytes: Option<u64>,
907                    swap_total_bytes: Option<u64>,
908                    load_1m: Option<f32>,
909                    load_5m: Option<f32>,
910                    load_15m: Option<f32>,
911                    uptime_s: Option<u64>,
912                    disks: Option<Vec<Disk>>,
913                    /// Host-monotonic duration between sampler refreshes.
914                    window_ns: u64,
915                }
916
917                /// Requests tool-telemetry's bounded device history.
918                struct SnapshotRequest {
919                    /// Optional exact logical device filter.
920                    device_id: Option<String>,
921                    /// Maximum newest records to return. Zero selects the
922                    /// tool's bounded default.
923                    limit: u32,
924                    /// Exclusive global ingest-sequence upper bound.
925                    before_sequence: Option<u64>,
926                }
927
928                struct Record {
929                    sequence: u64,
930                    sample: Sample,
931                    /// Text fields or rows truncated by tool-telemetry.
932                    truncated: u32,
933                }
934
935                struct Snapshot {
936                    cursor: crate::v0_1::tool::Cursor,
937                    records: Vec<Record>,
938                    /// Records evicted by the absolute capacity bound before
939                    /// their five-minute age horizon elapsed.
940                    capacity_evictions: u64,
941                    next_before_sequence: Option<u64>,
942                }
943
944                struct Follow {
945                    cursor: crate::v0_1::tool::Cursor,
946                    record: Record,
947                }
948
949                topic sample: state Sample;
950                topic snapshot: query SnapshotRequest => Snapshot;
951                topic follow: state Follow;
952            }
953
954            runtime {
955                /// Runner-originated portable performance rollup. The runner
956                /// publishes at most one per host-monotonic grid interval;
957                /// envelope provenance identifies the participant. Interval
958                /// counters are best-effort sequential atomic samples, not a
959                /// transactional stop-the-world boundary, so concurrent queue
960                /// activity may land on either neighboring rollup.
961                struct Rollup {
962                    window_ns: u64,
963                    step: Option<crate::v0_1::tool::RuntimeStep>,
964                    topics: Vec<crate::v0_1::tool::RuntimeTopic>,
965                    overflow: Option<crate::v0_1::tool::RuntimeTopic>,
966                }
967
968                /// Requests tool-telemetry's current bounded five-minute
969                /// runtime history.
970                struct SnapshotRequest {
971                    /// Optional exact participant filter. `None` selects the
972                    /// complete per-robot history.
973                    participant_id: Option<String>,
974                    /// Maximum newest records to return. Zero selects the
975                    /// tool's bounded default.
976                    limit: u32,
977                    /// Exclusive global ingest-sequence upper bound for
978                    /// backward pagination. `None` starts at the newest record.
979                    before_sequence: Option<u64>,
980                }
981
982                /// One retained rollup. `sequence` is assigned by
983                /// tool-telemetry at ingest, independent of producer metadata.
984                /// Duplicate normal topic keys are deterministically
985                /// re-aggregated before row bounds are applied.
986                struct Record {
987                    sequence: u64,
988                    participant_id: String,
989                    /// Text values truncated by tool-telemetry's ingest bound.
990                    /// Oversized/excess topic identities are not truncated;
991                    /// they are aggregated into the explicit overflow row.
992                    truncated: u32,
993                    window_ns: u64,
994                    step: Option<crate::v0_1::tool::RuntimeStep>,
995                    topics: Vec<crate::v0_1::tool::RuntimeTopic>,
996                    overflow: Option<crate::v0_1::tool::RuntimeTopic>,
997                }
998
999                struct Snapshot {
1000                    cursor: crate::v0_1::tool::Cursor,
1001                    records: Vec<Record>,
1002                    /// Records evicted by the absolute memory cap before their
1003                    /// five-minute age horizon elapsed.
1004                    capacity_evictions: u64,
1005                    /// Pass this as the next request's `before_sequence` to
1006                    /// continue backward. `None` means the retained matching
1007                    /// history is complete.
1008                    next_before_sequence: Option<u64>,
1009                }
1010
1011                struct Follow {
1012                    cursor: crate::v0_1::tool::Cursor,
1013                    record: Record,
1014                }
1015
1016                topic rollup: state Rollup;
1017                topic snapshot: query SnapshotRequest => Snapshot;
1018                topic follow: state Follow;
1019            }
1020        }
1021
1022        bus {
1023            uplink {
1024                /// Reserved observable state for a future authenticated router
1025                /// uplink. Phase 1 has no publisher for this contract.
1026                #[derive(Copy, Eq)]
1027                #[serde(rename_all = "snake_case")]
1028                enum UplinkPhase {
1029                    Disabled,
1030                    Connecting,
1031                    Connected,
1032                    /// Reserved for future retry telemetry.
1033                    Retrying,
1034                }
1035
1036                /// Reserved router-owned state for a future optional site uplink.
1037                struct State {
1038                    phase: UplinkPhase,
1039                    connect: Option<String>,
1040                    /// Reserved for future retry telemetry.
1041                    retry_attempt: u32,
1042                    /// Optional human-readable diagnostic while not connected,
1043                    /// such as DNS failure or waiting for the configured remote
1044                    /// link. Invalid endpoints fail configuration before startup.
1045                    detail: Option<String>,
1046                }
1047
1048                topic state: state State;
1049            }
1050        }
1051
1052
1053
1054
1055        perception {
1056            /// A single detected object: class, confidence, and pose in a frame.
1057            struct Detection {
1058                class_id: String,
1059                confidence: f32,
1060                position_m: [f64; 3],
1061                frame_id: String,
1062                track_id: Option<u64>,
1063            }
1064
1065            /// A batch of detections from one perception cycle.
1066            struct Detections {
1067                detections: Vec<Detection>,
1068                stamp_ns: Option<u64>,
1069            }
1070
1071            /// The perception participant's published health.
1072            struct State {
1073                healthy: bool,
1074                detector: String,
1075            }
1076
1077            topic detections: state Detections;
1078            topic state: state State;
1079        }
1080
1081        video {
1082            /// Ask to open a video stream for a capability at an optional size.
1083            struct OpenRequest {
1084                capability: String,
1085                width_px: Option<u32>,
1086                height_px: Option<u32>,
1087            }
1088
1089            /// The id of the stream that was opened.
1090            struct OpenResponse {
1091                stream_id: String,
1092            }
1093
1094            topic open: query OpenRequest => OpenResponse;
1095
1096            stream(stream) {
1097                /// Where one open video stream is in its lifecycle.
1098                #[derive(Copy, Eq)]
1099                #[serde(rename_all = "snake_case")]
1100                enum StreamPhase {
1101                    Starting,
1102                    Active,
1103                    Stopped,
1104                }
1105
1106                /// The published state of one video stream: its lifecycle phase
1107                /// and the number of source frames seen so far. The video participant
1108                /// publishes it per stream; clients subscribe, hence `state`.
1109                struct StreamState {
1110                    phase: StreamPhase,
1111                    frames_seen: u64,
1112                }
1113
1114                topic state: state StreamState;
1115            }
1116        }
1117
1118        simulation {
1119            /// One robot node that the simulator spawn authority should import.
1120            struct RobotSpawn {
1121                robot_id: String,
1122                node_string: String,
1123            }
1124
1125            /// Requests the current complete robot spawn set.
1126            struct SpawnRequest {
1127                known_revision: Option<u64>,
1128            }
1129
1130            /// The complete robot spawn set for one simulation world.
1131            struct SpawnSet {
1132                revision: u64,
1133                robots: Vec<RobotSpawn>,
1134            }
1135
1136            /// The authoritative advancing simulation clock. Publication means
1137            /// the world advanced; silence means it did not.
1138            struct Clock {
1139                now_ns: u64,
1140                step: u64,
1141            }
1142
1143            /// A command to the simulator's run loop.
1144            #[derive(Copy, Eq)]
1145            enum Control {
1146                Pause,
1147                Resume,
1148                Reset,
1149            }
1150
1151            /// The simulated robot's ground-truth planar pose.
1152            struct RobotPose {
1153                x_m: f64,
1154                y_m: f64,
1155                yaw_rad: f64,
1156            }
1157
1158            /// Whether the simulated robot is in contact, with optional detail.
1159            struct Contact {
1160                in_contact: bool,
1161                detail: Option<String>,
1162            }
1163
1164            topic spawn: query SpawnRequest => SpawnSet;
1165            topic clock: state Clock;
1166            topic control: command Control;
1167            topic robot_pose: state RobotPose;
1168            topic contact: state Contact;
1169        }
1170
1171        // Per-instance component capabilities (D17/D38: framework participant / driver
1172        // territory). `component(instance)` selects a manifest-declared component;
1173        // each child `kind(capability)` is a self-contained node whose key is
1174        // `component/{instance}/<kind>/{capability}/<leaf>`. Nodes duplicate any
1175        // types they share by design - the node path disambiguates, so the names
1176        // are path-local.
1177        component(instance) {
1178            motor(capability) {
1179                /// A per-actuator command.
1180                enum Command {
1181                    Velocity(f32),
1182                    Torque(f32),
1183                    Stop,
1184                }
1185
1186                topic command: command Command;
1187            }
1188
1189            encoder(capability) {
1190                /// Per-encoder sample on a dynamic per-instance key.
1191                struct Sample {
1192                    position_rad: f64,
1193                    velocity_radps: f32,
1194                }
1195
1196                topic sample: state Sample;
1197            }
1198
1199            accelerometer(capability) {
1200                /// Raw accelerometer sample in the sensor-local frame in m/s^2.
1201                struct Sample {
1202                    linear_acceleration: [f32; 3],
1203                }
1204
1205                topic sample: state Sample;
1206            }
1207
1208            gyroscope(capability) {
1209                /// Raw angular velocity sample in the sensor-local frame in rad/s.
1210                struct Sample {
1211                    angular_velocity: [f32; 3],
1212                }
1213
1214                topic sample: state Sample;
1215            }
1216
1217            magnetometer(capability) {
1218                /// Raw magnetic-field sample in the sensor-local frame.
1219                struct Sample {
1220                    magnetic_field: [f32; 3],
1221                }
1222
1223                topic sample: state Sample;
1224            }
1225
1226            imu(capability) {
1227                #[derive(Copy, Eq)]
1228                #[serde(rename_all = "snake_case")]
1229                enum SensorHealth {
1230                    Nominal,
1231                    Degraded,
1232                    Fault,
1233                }
1234
1235                #[derive(Copy)]
1236                struct Bias {
1237                    angular_velocity_radps: [f32; 3],
1238                    linear_acceleration_mps2: [f32; 3],
1239                }
1240
1241                struct Sample {
1242                    orientation: Option<[f32; 4]>,
1243                    angular_velocity_radps: [f32; 3],
1244                    linear_acceleration_mps2: [f32; 3],
1245                    covariance: Option<[f32; 9]>,
1246                    noise_density: Option<[f32; 3]>,
1247                    sensor_frame_id: Option<String>,
1248                    measured_at_ns: Option<u64>,
1249                    health: SensorHealth,
1250                    bias: Option<Bias>,
1251                }
1252
1253                topic sample: state Sample;
1254            }
1255
1256            range(capability) {
1257                #[derive(Copy, Eq)]
1258                #[serde(rename_all = "snake_case")]
1259                enum SensorHealth {
1260                    Nominal,
1261                    Degraded,
1262                    Fault,
1263                }
1264
1265                #[derive(Copy)]
1266                struct Limits {
1267                    min_m: f32,
1268                    max_m: f32,
1269                }
1270
1271                #[derive(Copy)]
1272                struct SampleQuality {
1273                    valid: bool,
1274                    confidence: Option<f32>,
1275                }
1276
1277                struct Sample {
1278                    distance_m: f32,
1279                    limits: Option<Limits>,
1280                    measured_at_ns: Option<u64>,
1281                    quality: Option<SampleQuality>,
1282                    health: SensorHealth,
1283                }
1284
1285                topic sample: state Sample;
1286            }
1287
1288            gnss(capability) {
1289                /// A GNSS fix: geodetic position plus a 3x3 position covariance.
1290                struct Sample {
1291                    latitude: f64,
1292                    longitude: f64,
1293                    altitude: f64,
1294                    position_covariance: [f64; 9],
1295                }
1296
1297                topic sample: state Sample;
1298            }
1299
1300            camera(capability) {
1301                #[derive(Copy, Eq)]
1302                #[serde(rename_all = "snake_case")]
1303                enum Encoding {
1304                    Jpeg,
1305                    Png,
1306                    L8,
1307                    Rgb8,
1308                    Rgba8,
1309                }
1310
1311                #[derive(Copy)]
1312                struct Intrinsics {
1313                    fx: f32,
1314                    fy: f32,
1315                    cx: f32,
1316                    cy: f32,
1317                }
1318
1319                struct Distortion {
1320                    model: String,
1321                    coefficients: Vec<f32>,
1322                }
1323
1324                #[derive(Copy)]
1325                struct ExposureTiming {
1326                    exposure_start_ns: Option<u64>,
1327                    exposure_duration_ns: Option<u64>,
1328                }
1329
1330                struct CalibrationIdentity {
1331                    id: String,
1332                    version: String,
1333                }
1334
1335                /// One camera frame: encoded pixel bytes plus optional calibration
1336                /// and timing metadata.
1337                struct Frame {
1338                    width: u32,
1339                    height: u32,
1340                    encoding: Encoding,
1341                    intrinsics: Option<Intrinsics>,
1342                    distortion: Option<Distortion>,
1343                    exposure: Option<ExposureTiming>,
1344                    measured_at_ns: Option<u64>,
1345                    calibration: Option<CalibrationIdentity>,
1346                    #[serde(with = "serde_bytes")]
1347                    data: Vec<u8>,
1348                }
1349
1350                topic frame: state Frame;
1351            }
1352
1353            depth(capability) {
1354                #[derive(Copy, Eq)]
1355                #[serde(rename_all = "snake_case")]
1356                enum Encoding {
1357                    U16Millimeters,
1358                }
1359
1360                #[derive(Copy, Eq)]
1361                #[serde(rename_all = "snake_case")]
1362                enum InvalidSamplePolicy {
1363                    ZeroIsInvalid,
1364                    NonFiniteIsInvalid,
1365                }
1366
1367                #[derive(Copy)]
1368                struct Intrinsics {
1369                    fx: f32,
1370                    fy: f32,
1371                    cx: f32,
1372                    cy: f32,
1373                }
1374
1375                struct Distortion {
1376                    model: String,
1377                    coefficients: Vec<f32>,
1378                }
1379
1380                #[derive(Copy)]
1381                struct ExposureTiming {
1382                    exposure_start_ns: Option<u64>,
1383                    exposure_duration_ns: Option<u64>,
1384                }
1385
1386                struct CalibrationIdentity {
1387                    id: String,
1388                    version: String,
1389                }
1390
1391                /// One depth frame: per-pixel millimetre samples plus optional
1392                /// calibration and timing metadata.
1393                struct Frame {
1394                    samples_mm: Vec<u16>,
1395                    encoding: Encoding,
1396                    invalid_sample_policy: InvalidSamplePolicy,
1397                    width: Option<u32>,
1398                    height: Option<u32>,
1399                    intrinsics: Option<Intrinsics>,
1400                    distortion: Option<Distortion>,
1401                    exposure: Option<ExposureTiming>,
1402                    measured_at_ns: Option<u64>,
1403                    calibration: Option<CalibrationIdentity>,
1404                }
1405
1406                topic frame: state Frame;
1407            }
1408
1409            lidar(capability) {
1410                #[derive(Copy, Eq)]
1411                #[serde(rename_all = "snake_case")]
1412                enum SensorHealth {
1413                    Nominal,
1414                    Degraded,
1415                    Fault,
1416                }
1417
1418                #[derive(Copy)]
1419                struct ScanGeometry {
1420                    angle_min_rad: f32,
1421                    angle_increment_rad: f32,
1422                }
1423
1424                #[derive(Copy)]
1425                struct RangeLimits {
1426                    min_m: f32,
1427                    max_m: f32,
1428                }
1429
1430                #[derive(Copy)]
1431                struct ScanQuality {
1432                    valid_points: u32,
1433                }
1434
1435                struct Ranges {
1436                    ranges: Vec<f32>,
1437                    geometry: Option<ScanGeometry>,
1438                    limits: Option<RangeLimits>,
1439                    measured_at_ns: Option<u64>,
1440                    quality: Option<ScanQuality>,
1441                    health: SensorHealth,
1442                }
1443
1444                struct Points {
1445                    points: Vec<[f32; 3]>,
1446                    limits: Option<RangeLimits>,
1447                    measured_at_ns: Option<u64>,
1448                    quality: Option<ScanQuality>,
1449                    health: SensorHealth,
1450                }
1451
1452                /// One lidar scan, either as polar ranges or as cartesian points.
1453                #[serde(tag = "kind", rename_all = "snake_case")]
1454                enum Scan {
1455                    Ranges(Ranges),
1456                    Points(Points),
1457                }
1458
1459                topic scan: state Scan;
1460            }
1461
1462            mmwave(capability) {
1463                /// One mmWave radar detection: position, velocity, and SNR.
1464                #[derive(Copy)]
1465                struct Detection {
1466                    position: [f32; 3],
1467                    velocity: [f32; 3],
1468                    snr: f32,
1469                }
1470
1471                /// One mmWave radar scan as a set of detections.
1472                struct Scan {
1473                    detections: Vec<Detection>,
1474                }
1475
1476                topic scan: state Scan;
1477            }
1478
1479            microphone(capability) {
1480                /// One audio frame as raw encoded bytes.
1481                struct Frame {
1482                    data: Vec<u8>,
1483                }
1484
1485                topic frame: state Frame;
1486            }
1487
1488            led(capability) {
1489                /// A per-LED on/off command.
1490                #[derive(Copy, Eq)]
1491                enum Command {
1492                    On,
1493                    Off,
1494                }
1495
1496                topic command: command Command;
1497            }
1498
1499            emergency_stop(capability) {
1500                /// Per-instance emergency-stop state.
1501                #[derive(Eq)]
1502                struct State {
1503                    engaged: bool,
1504                }
1505
1506                topic state: state State;
1507            }
1508        }
1509
1510        odometry {
1511            /// A planar pose + twist estimate in the odometry frame.
1512            struct State {
1513                x_m: f64,
1514                y_m: f64,
1515                yaw_rad: f64,
1516                linear_x_mps: f32,
1517                angular_z_radps: f32,
1518            }
1519
1520            topic state: state State;
1521        }
1522
1523        localize {
1524            /// A planar localization estimate in the map frame.
1525            struct LocalizationState {
1526                x_m: f64,
1527                y_m: f64,
1528                yaw_rad: f64,
1529                confidence: f32,
1530            }
1531
1532            topic state: state LocalizationState;
1533        }
1534
1535        map {
1536            /// A published map revision marker.
1537            struct Revision {
1538                revision: u64,
1539                resolution_m: f32,
1540            }
1541
1542            /// Request a rectangular submap window (map-frame metres).
1543            struct SubmapRequest {
1544                min_x_m: f64,
1545                min_y_m: f64,
1546                max_x_m: f64,
1547                max_y_m: f64,
1548            }
1549
1550            /// An occupancy-grid window: row-major cells, 0..=100 + 255 = unknown.
1551            struct SubmapResponse {
1552                width: u32,
1553                height: u32,
1554                resolution_m: f32,
1555                cells: Vec<u8>,
1556            }
1557
1558            topic revision: state Revision;
1559            topic submap: query SubmapRequest => SubmapResponse;
1560        }
1561
1562        asset {
1563            /// Fetch a stored asset by path.
1564            struct GetRequest {
1565                path: String,
1566            }
1567
1568            /// The asset bytes, a not-found marker, or a rejected path.
1569            enum GetResponse {
1570                Found { bytes: Vec<u8> },
1571                Missing,
1572                InvalidPath,
1573            }
1574
1575            topic get: query GetRequest => GetResponse;
1576        }
1577
1578        battery {
1579            /// Battery state reported by the active simulator or hardware owner.
1580            struct State {
1581                voltage_v: f32,
1582                current_a: f32,
1583                charge_ratio: f32,
1584            }
1585
1586            topic state: state State;
1587        }
1588
1589        joypad {
1590            /// Whether an observed controller is ready for the fixed manual
1591            /// input preset, disconnected, or connected without a compatible
1592            /// control mapping.
1593            enum DeviceStatus {
1594                Ready,
1595                Disconnected,
1596                Unsupported,
1597            }
1598
1599            /// One gamepad the tool can see. `id` is a STABLE wire id the tool
1600            /// assigns (name/guid-derived) - NOT a process-local gilrs id.
1601            struct Device {
1602                id: String,
1603                name: String,
1604                status: DeviceStatus,
1605            }
1606
1607            /// The joypad tool's published device state.
1608            struct Devices {
1609                available: Vec<Device>,
1610                selected: Option<String>,
1611                enabled: bool,
1612                /// Structural reason manual input cannot be enabled in this
1613                /// session (for example robot-model or backend limitations),
1614                /// independent of transient device/request errors.
1615                unavailable_reason: Option<String>,
1616                /// One-shot acknowledgement of a failed select/enable/rescan
1617                /// request. Event-driven consumers may show it once; periodic
1618                /// state heartbeats omit it. The tool also writes the failure
1619                /// to its log stream for durable diagnostics.
1620                last_error: Option<String>,
1621            }
1622
1623            /// Client asks the tool to select a device by its stable id.
1624            struct Select {
1625                id: String,
1626            }
1627
1628            /// Client asks the tool to enable or disable manual input.
1629            struct SetEnabled {
1630                enabled: bool,
1631            }
1632
1633            /// Client asks the tool to re-enumerate devices.
1634            struct Rescan {}
1635
1636            topic devices: state Devices;
1637            topic select: command Select;
1638            topic set_enabled: command SetEnabled;
1639            topic rescan: command Rescan;
1640        }
1641    }
1642    version v0_2 extends v0_1 {
1643        tool {
1644            device {
1645                /// Portable whole-device observations produced by one
1646                /// runner-owned tool-device. Every optional field is `None`
1647                /// when the current platform cannot provide a truthful value.
1648                /// These totals must never be attributed to a runtime.
1649                replace struct Sample {
1650                    /// Bounded execution-device label supplied by the project
1651                    /// supervisor. Every per-robot sampler in one project uses
1652                    /// the same value, so robot-rooted records remain joinable
1653                    /// without creating device-rooted bus authority.
1654                    device_id: String,
1655                    cpu_pct: Option<f32>,
1656                    ram_used_bytes: Option<u64>,
1657                    ram_total_bytes: Option<u64>,
1658                    swap_used_bytes: Option<u64>,
1659                    swap_total_bytes: Option<u64>,
1660                    load_1m: Option<f32>,
1661                    load_5m: Option<f32>,
1662                    load_15m: Option<f32>,
1663                    uptime_s: Option<u64>,
1664                    disks: Option<Vec<Disk>>,
1665                    /// Host-monotonic duration between sampler refreshes.
1666                    window_ns: u64,
1667                }
1668            }
1669        }
1670    }
1671    latest v0_2;
1672}
1673
1674#[cfg(test)]
1675mod tests;