# 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.