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