agent-can 0.1.0

Agent-first CAN control daemon
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
# agent-can implementation specification

## Status

Build document for the current implementation target. This supersedes the question-and-answer structure in [`spec.md`](/Users/tomford/code/projects/agent-can/spec.md) for implementation work.

`spec.md` can remain as design history. New code should follow this document.

## Product goal

`agent-can` gives a local agent or human operator a safe, observable way to:

- connect to one real CAN bus through a supported adapter
- inspect live traffic
- load zero or more DBC files at connect time as semantic overlays for reads and writes
- send raw or semantic messages
- keep a short in-memory recent history
- export raw trace logs in a standard ASCII format

The long-term requirement remains CLI and MCP parity from one Rust executable. MCP should land on the same core contract rather than as a second implementation.

## Scope

Ship:

- one live CAN session per local daemon
- local IPC between front-end and daemon
- CLI as the first front-end
- shared contract beneath transports
- raw-first observation model
- connect-time-fixed DBC alias registry
- semantic discovery through `schema`
- observed-traffic discovery through `message_list`
- detailed reads through `message_read`
- one-shot and periodic `message_send`
- `message_stop`
- one exported raw trace for the active session

## Core model

There is one daemon-backed live session. The daemon owns:

- the hardware session
- the connect-time-loaded DBC alias set
- the rolling raw event buffer
- the latest-observation index
- periodic send schedules
- trace export state

CLI and MCP are thin transport layers over the same shared contract. They may start or attach to the one local daemon, but they should not own business logic.

The DBC layer is a semantic overlay over raw CAN data. Raw traffic is the source of truth. DBCs do not change what happened on the wire; they only change how the runtime can describe or construct it.

No runtime state should be stored in DBC-shaped form. The daemon should store raw events, raw latest-observation state, raw periodic-send state, and raw trace output. DBC definitions should be used only to decode raw data for reads and to encode semantic inputs for writes.

If the DBC layer were removed entirely, the runtime should still function with raw selectors and raw payloads.

`.asc` export remains raw trace output. It should not be semanticized.

If a human needs to work with multiple physical buses at once, that should be handled by separate local sessions rather than by making one agent-facing contract multi-bus-aware.

## User-facing surface

The current verb set is:

- `adapters_list`
- `connect`
- `disconnect`
- `status`
- `schema`
- `message_list`
- `message_read`
- `message_send`
- `message_stop`
- `trace_start`
- `trace_stop`

CLI grouping should be:

```text
agent-can adapters list
agent-can connect ...
agent-can disconnect
agent-can status
agent-can schema ...
agent-can message list ...
agent-can message read ...
agent-can message send ...
agent-can message stop ...
agent-can trace start ...
agent-can trace stop ...
```

MCP should reuse the same verbs as flat tool names.

## Single help-text source

The CLI help text, MCP tool descriptions, and any contract-level usage text should come from one shared source.

Reason:

- `schema` and `message_list` are easy to confuse
- selector rules need to stay identical across transports
- raw-vs-semantic behavior must not drift between `-h`, MCP descriptions, and docs

Implementation direction:

- create shared verb metadata under the contract layer
- keep verb summary, argument help, selector rules, and semantic notes there
- have CLI command builders and MCP tool registration consume that metadata

Do not maintain separate prose copies of tool semantics in multiple front-ends.

## Shared selector syntax

The app should use one selector model everywhere:

- `--filter`
- `--select`
- `--target`

Rules:

- `0x...` means raw arbitration-ID selection
- otherwise treat the value as semantic DBC selection over `alias.message`
- semantic patterns use the same matching syntax everywhere
- operations that require one concrete message must resolve to exactly one match

The DBC path through any selector should be shallow:

- resolve the semantic definition
- encode or decode through DBC as needed
- continue the operation in raw form

There should not be a separate raw-vs-semantic execution pipeline under the API surface.

## Transport and module shape

Recommended target shape:

- `transport`
  - `cli`
  - `mcp`
- `contract`
- `runtime`
- `ipc`
- `can`
- `dbc`
- `trace`

Current code already has the right broad seams. Do not block implementation on a large rename. In particular:

- `src/protocol.rs` can stay in place initially even if the conceptual name is now `contract`
- `src/daemon/server.rs` is the right initial home for the shared daemon service logic
- `src/can/dbc.rs` should be evolved rather than replaced wholesale

The important constraint is semantic centralization, not immediate file renaming.

## Adapter discovery

### `adapters_list`

`adapters_list` should stay minimal.

It should return the locally available adapter or backend names that can be passed to `connect`.

## Session model

### `connect`

`connect` starts or attaches to the one live session.

Arguments:

- `adapter`
- `bitrate`
- optional `bitrate_data`
- optional `fd`
- optional repeatable `dbc`, where each entry provides `alias` and `path`

Rules:

- if no session exists, create it
- a session may start with zero DBCs loaded
- if a session exists with identical open parameters, including the full DBC alias/path set, return an already-connected result
- if a session exists with different open parameters, fail until the operator calls `disconnect`
- validate and load the full DBC set during connect
- if any requested DBC fails validation or load, connect should fail without creating a partial live session
- changing the DBC set requires `disconnect` followed by `connect`

### `disconnect`

`disconnect` is explicit daemon teardown.

Disconnecting should:

- stop periodic sends
- stop and finalize trace export
- clear ephemeral in-memory state by exiting the daemon

### `status`

`status` is the detailed operational view for the live session.

It should include:

- connection state
- adapter/backend
- bitrate and FD settings
- loaded DBC aliases with source path
- active trace export destination
- active periodic schedules

## DBC model

### Connect-time-loaded aliases

DBC files are supplied as part of `connect` for the active session.

The alias set is fixed for the session lifetime. To change it, disconnect and reconnect with a different DBC list.

Alias rules:

- alias is required
- alias is unique within the session
- the same file may be loaded under different aliases only if we later find a real use; the implementation does not need to encourage it

### Overlapping arbitration IDs

Do not hard-fail when two loaded DBCs describe the same arbitration ID.

Reason:

- the DBC layer is only a semantic overlay
- semantic reads and writes are alias-qualified
- raw observations are still keyed by the underlying frame identity
- rejecting overlaps will block legitimate multi-DBC workflows without improving raw correctness

Required behavior:

- `schema` shows all semantic definitions, even when arbitration IDs overlap
- `message_read --select alias.message` decodes with that exact semantic definition
- `message_send --target alias.message` encodes with that exact semantic definition
- raw trace and raw history remain unaffected by overlaps

### `schema`

`schema` is semantic discovery for the current session. It answers: what messages could this session interpret or construct from the DBC set loaded at connect time?

This is intentionally different from `message_list`, which answers: what traffic has actually been observed?

Arguments:

- optional `filter`

Filtering rules:

- `0x...` arbitration ID
- semantic pattern over `alias.message`

Each result should include enough information for an agent to construct a valid semantic send without opening the DBC externally:

- `alias.message`
- source alias
- arbitration ID
- frame format flags needed for send
- DLC / message size
- signal list
- per-signal type information
- per-signal unit when present
- per-signal min/max when present
- scaling metadata needed to explain valid values if helpful

The goal is agent usability, not perfect DBC introspection completeness.

## Observation model

### Raw-first storage

The daemon should store raw RX and TX events, not pre-materialized decoded mailbox state as its main truth.

Maintain:

- a rolling event buffer
- a latest-observation index derived from those events

Per-event fields should include at least:

- monotonic sequence number
- wall-clock receive/send timestamp
- direction: RX or TX
- arbitration ID
- extended flag
- FD flag when present
- DLC / payload length
- raw payload bytes

### Retention

Use a short rolling window plus a hard size cap.

Default:

- target roughly 60 seconds of recent events
- enforce a hard event-count cap so memory usage stays bounded on busy traffic

The count cap is the primary safety bound. The time window is the operator-facing behavior target.

Keep both values as internal tunables.

### Latest-observation index

Maintain a derived index keyed by raw frame identity so `message_list` and `message_read` do not need to scan the full buffer for the common case.

The raw identity key should be at least:

- arbitration ID
- extended flag

Direction should remain queryable, but latest-value identity should primarily follow the CAN frame identity rather than semantic name.

## Read surfaces

### `message_list`

`message_list` is observed-traffic inventory. It should not return decoded signal values.

Arguments:

- optional `filter`
- optional `allow_raw`
- optional `include_tx`

Behavior:

- returns compact message-level entries derived from observed traffic
- default excludes TX-only observations
- `include_tx` includes TX events in the observed set
- `filter` uses shared selector rules:
  - `0x...` means arbitration-ID match
  - otherwise semantic/glob match over `alias.message`

Each entry should include:

- arbitration ID
- last-seen timestamp
- RX/TX presence as applicable
- either a semantic message name or a raw arbitration ID

Presentation rule:

- if a raw observation matches one semantic definition, emit that semantic message as its own entry
- if a raw observation matches multiple semantic definitions, emit one entry per semantic message name
- every entry should include arbitration ID
- keep duplicated semantic entries separate when overlaps exist

Implementation direction:

- it is acceptable to evaluate observed raw messages against each loaded DBC set and append the semantic matches
- grouping semantic matches back under one arbitration ID is not required

`allow_raw` behavior:

- when DBCs are loaded, default output may suppress raw-only entries to stay concise
- `allow_raw=true` includes entries with no semantic match
- when no DBC is loaded, all observed raw frame identities are shown

### `message_read`

`message_read` is detailed inspection for one selection.

Arguments:

- `select`
- optional `count`
- optional `include_tx`

Selector rules:

- `0x...` selects by raw arbitration ID
- otherwise select a semantic pattern over `alias.message` that resolves to one concrete message

Behavior with no `count`:

- selection by `alias.message` returns the latest decode using that DBC definition
- selection by `0x...` returns the latest raw frame

Behavior with `count`:

- returns the most recent matching observations from the rolling buffer
- selection by `alias.message` decodes each matching raw observation through that semantic definition
- selection by `0x...` returns raw observations

## Send surfaces

### `message_send`

`message_send` uses the same selector rules as the read surfaces.

Arguments:

- `target`
- `data`
- optional `periodicity`

Selector rules:

- `0x...` means raw send to that arbitration ID
- otherwise target a semantic pattern over `alias.message` that resolves to one concrete message

Data rules:

- if `target` is `alias.message`, `data` is a complete signal map
- if `target` is `0x...`, `data` is raw payload bytes

Semantic send rules:

- full message required
- omitted signals are an error
- no silent zero-fill
- no read-modify-write
- one-shot send when `periodicity` is absent
- encode the semantic payload to raw bytes, then continue exactly as a raw send

Raw send rules:

- raw sends are first-class
- raw sends are allowed with or without periodicity
- raw and semantic sends should share the same scheduling path once the payload has been resolved to raw bytes
- periodic send state should store raw arbitration ID, flags, payload bytes, and timing, not DBC-shaped objects

Scheduling rule:

- create or overwrite the single periodic schedule for that target identity when `periodicity` is present

The runtime should validate against DBC-defined ranges where the DBC exposes them.

### `message_stop`

Stop the one periodic schedule for the given target identity.

Arguments:

- `target`

One periodic schedule exists per target identity.

`message_stop --target 0x...` should stop a raw periodic send by arbitration ID. `message_stop --target alias.message` should stop a semantic periodic send by its resolved message identity.

## Trace export

### `trace_start`

Start one exported raw trace for the active session.

Rules:

- one active trace export at a time
- output format should be ASCII CAN trace output suitable for human debugging and external tools
- export raw RX and TX events

### `trace_stop`

Stop the current trace export.

Disconnecting should also stop and finalize the trace cleanly.

## Runtime failure posture

If the backend starts erroring repeatedly, the daemon should prefer to stay alive in a degraded or error state rather than shutting itself down.

The normal teardown path remains explicit `disconnect`. Only truly unrecoverable backend failure should force daemon exit.

Periodic sends may continue while receive or decode is degraded, as long as transmit capability is still healthy.

## Implementation plan

### Phase 1: replace the current command contract

Current runtime still exposes:

- `open`
- `status`
- `mailboxes`
- `mailbox`
- `send_raw`
- `send_message`
- `bus list`
- `close`

First step is replacing this with the shared singleton-session contract.

Primary files:

- [`src/cli/args.rs`]/Users/tomford/code/projects/agent-can/src/cli/args.rs
- [`src/cli/commands.rs`]/Users/tomford/code/projects/agent-can/src/cli/commands.rs
- [`src/protocol.rs`]/Users/tomford/code/projects/agent-can/src/protocol.rs

Required changes:

- regroup CLI into `adapters`, `connect`, `disconnect`, `status`, `schema`, `message`, `trace`
- remove mailbox-oriented request and response shapes
- remove bus-name fields from request and response envelopes
- define request and response models for the current verbs
- add shared help metadata here or directly adjacent to this layer

### Phase 2: make connect own the fixed DBC set and collapse to one session

Current `open` requires one DBC path and the daemon stores exactly one DBC overlay.

Primary files:

- [`src/daemon/config.rs`]/Users/tomford/code/projects/agent-can/src/daemon/config.rs
- [`src/daemon/server.rs`]/Users/tomford/code/projects/agent-can/src/daemon/server.rs
- [`src/ipc.rs`]/Users/tomford/code/projects/agent-can/src/ipc.rs
- [`src/can/dbc.rs`]/Users/tomford/code/projects/agent-can/src/can/dbc.rs

Required changes:

- allow `connect` to accept zero or more aliased DBC inputs
- let a session start with zero DBCs loaded
- replace single-overlay state with a session alias registry populated only at connect time
- compare existing-session identity using both bus-open parameters and the full DBC alias/path set
- treat connect as atomic across backend open plus DBC validation and load
- replace any bus registry or bus-routed IPC expectations with one fixed daemon endpoint
- preserve existing encode and decode helpers where possible

### Phase 3: add semantic discovery and overlap-safe decode behavior

Primary file:

- [`src/can/dbc.rs`]/Users/tomford/code/projects/agent-can/src/can/dbc.rs

Required changes:

- expose semantic definition inventory for `schema`
- support multiple DBC definitions per raw frame identity
- support alias-qualified decode on read
- support alias-qualified encode on send
- keep enough signal metadata to support agent-facing semantic discovery

### Phase 4: replace mailbox state with raw-first observation state and add periodic send ownership

Current daemon state stores decoded mailboxes as the main read model.

Primary files:

- [`src/daemon/server.rs`]/Users/tomford/code/projects/agent-can/src/daemon/server.rs
- [`src/can/dbc.rs`]/Users/tomford/code/projects/agent-can/src/can/dbc.rs
- [`src/protocol.rs`]/Users/tomford/code/projects/agent-can/src/protocol.rs

Required changes:

- introduce raw event records
- introduce bounded rolling buffer
- introduce latest-observation index keyed by raw frame identity
- derive `message_list` and `message_read` from this state
- add periodic send schedule table keyed by target identity
- overwrite existing schedule on repeated `message_send` with `periodicity`
- implement `message_stop`
- keep periodic state in raw encoded form rather than DBC-shaped form

Regression risk:

- existing decoded mailbox reads are simpler than the target model
- selection and filtering behavior will drift unless contract tests are added at the same time

### Phase 5: raw trace export

Primary files:

- add a `trace` module
- wire export ownership into the daemon state

Required changes:

- start and stop one trace export for the live session
- write RX and TX raw events
- finalize cleanly on `trace_stop` and `disconnect`

### Phase 6: MCP transport on the shared contract

After the CLI contract and daemon behavior are stable:

- add `agent-can --mcp`
- register MCP tools from the shared verb metadata
- route MCP requests into the same request handlers used by CLI forwarding

MCP is not a separate runtime. It is another transport over the same contract.

## Testing requirements

Minimum required coverage before calling the implementation coherent:

- CLI argument parsing for the grouped verb surface
- contract serialization for all request and response shapes
- singleton session lifecycle: connect, attach, disconnect
- connect with differing open parameters, including DBC alias/path set, fails until disconnect
- connect succeeds with zero DBCs loaded
- connect rejects invalid DBC inputs without creating a partial session
- overlapping arbitration-ID DBC definitions do not hard-fail
- `message_list` emits separate semantic entries when overlaps exist
- `message_read alias.message` decodes through the selected alias when overlaps exist
- `schema` distinguishes semantic inventory from observed traffic
- `message_send` rejects missing signals for semantic sends
- `message_send --target 0x...` performs raw send without a separate raw flag
- raw periodic sends and semantic periodic sends converge to the same stored raw schedule format
- `message_stop --target 0x...` stops a raw periodic send
- periodic overwrite semantics
- trace start and stop lifecycle
- degraded backend state preserves inspectability until explicit disconnect or unrecoverable failure

Add regression tests when replacing mailbox behavior, because that is the highest-risk semantic shift from the current code.

## Build order

Recommended order:

1. Contract and CLI verb reshape.
2. Connect with a fixed DBC set and fixed single-session IPC.
3. DBC alias registry plus `schema`.
4. Raw event buffer, `message_list` and `message_read`, and periodic send ownership.
5. Trace export.
6. MCP transport.

This order keeps the singleton session model stable before transport expansion.