car-runtime 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/docs/ASSISTANT.md +491 -0
- package/docs/CLI.md +3051 -0
- package/docs/GUIDE.md +266 -0
- package/docs/MCP.md +382 -0
- package/docs/SPEC.md +169 -0
- package/docs/agent-ir-spec.md +677 -0
- package/docs/websocket-protocol.md +10 -0
- package/index.d.ts +84 -0
- package/package.json +5 -3
package/docs/GUIDE.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Building agents on CAR
|
|
2
|
+
|
|
3
|
+
Two copy-paste prompts that give an LLM (Claude / ChatGPT / Cursor) enough context
|
|
4
|
+
to produce a working CAR agent on the first try — one for a single agent, one for
|
|
5
|
+
a multi-agent system that learns from its own traces. Fill in the `TASK:` line and
|
|
6
|
+
paste the whole block. For the data shapes these reference, see [SPEC.md](./SPEC.md);
|
|
7
|
+
for runnable versions, see [`examples/`](./examples/).
|
|
8
|
+
|
|
9
|
+
## Build your first agent
|
|
10
|
+
|
|
11
|
+
Paste everything in the block below into an LLM and fill in the `TASK:` line.
|
|
12
|
+
|
|
13
|
+
````markdown
|
|
14
|
+
I want to build an AI agent using **Common Agent Runtime (CAR)**, a Rust-native
|
|
15
|
+
runtime where models propose actions and the runtime validates + executes them
|
|
16
|
+
deterministically.
|
|
17
|
+
|
|
18
|
+
TASK: <DESCRIBE WHAT YOU WANT THE AGENT TO DO — e.g. "read a directory of PDFs,
|
|
19
|
+
extract titles + abstracts, produce a JSON report">
|
|
20
|
+
|
|
21
|
+
Use the Python binding `car_runtime` (pip install car-runtime — import name is
|
|
22
|
+
`car_runtime`) unless I tell you otherwise. Keep everything in one file.
|
|
23
|
+
|
|
24
|
+
## What CAR gives you
|
|
25
|
+
|
|
26
|
+
- `CarRuntime()` — a stateful runtime. Exposes `state_*`, `add_fact`,
|
|
27
|
+
`register_tool`, `register_policy`, `verify_proposal`, `execute_proposal`,
|
|
28
|
+
`infer_tracked`, `infer_stream`, plus persistence + memory graph.
|
|
29
|
+
- **You write the tools.** Tools are Python functions dispatched by a callback.
|
|
30
|
+
The runtime owns the DAG, state, policies, verification — not the tools.
|
|
31
|
+
- **Proposals are plans.** A proposal is JSON describing a list of actions and
|
|
32
|
+
their dependencies. Verify before executing.
|
|
33
|
+
|
|
34
|
+
## Action / proposal shape
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"actions": [
|
|
39
|
+
{ "id": "a1", "type": "tool_call", "tool": "read_file",
|
|
40
|
+
"parameters": {"path": "/tmp/foo.txt"}, "dependencies": [] },
|
|
41
|
+
{ "id": "a2", "type": "tool_call", "tool": "summarize",
|
|
42
|
+
"parameters": {"text_ref": "$a1.output"}, "dependencies": ["a1"] }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Valid `type` values: `tool_call`, `state_write`, `state_read`, `assertion`.
|
|
48
|
+
|
|
49
|
+
## Tool callback contract
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
def tool_fn(call_json: str) -> str:
|
|
53
|
+
call = json.loads(call_json)
|
|
54
|
+
if call["tool"] == "read_file":
|
|
55
|
+
return json.dumps({"content": open(call["params"]["path"]).read()})
|
|
56
|
+
return json.dumps({"error": f"unknown tool: {call['tool']}"})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The callback takes ONE argument — a JSON string describing the whole call
|
|
60
|
+
(`{"tool", "params", "action_id", "request_id", "timeout_ms", "session_id",
|
|
61
|
+
"attempt"}`), not the tool name and params as two separate arguments.
|
|
62
|
+
Return a JSON string. Errors are just a `{"error": "..."}` payload — the runtime
|
|
63
|
+
handles retries + replans if configured.
|
|
64
|
+
|
|
65
|
+
## Skeleton to fill in
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import json
|
|
69
|
+
import car_runtime
|
|
70
|
+
|
|
71
|
+
def build_agent():
|
|
72
|
+
rt = car_runtime.CarRuntime()
|
|
73
|
+
|
|
74
|
+
# 1. Register the tools your agent will use.
|
|
75
|
+
for tool_name in ["<TOOL_1>", "<TOOL_2>"]:
|
|
76
|
+
rt.register_tool(tool_name)
|
|
77
|
+
|
|
78
|
+
# 2. Add safety policies.
|
|
79
|
+
rt.register_policy("no_rm", "deny_tool_param",
|
|
80
|
+
target="shell", key="command", pattern="rm -rf")
|
|
81
|
+
|
|
82
|
+
# 3. Seed facts the agent should know (optional).
|
|
83
|
+
rt.add_fact("goal", "<WHAT THE AGENT IS TRYING TO DO>", "pattern")
|
|
84
|
+
|
|
85
|
+
# 4. Build a proposal (hand-written, or generated by infer_tracked).
|
|
86
|
+
proposal = {"actions": [ ... ]}
|
|
87
|
+
|
|
88
|
+
# 5. Verify first — cheap, catches bad plans before any tool runs.
|
|
89
|
+
check = json.loads(rt.verify_proposal(json.dumps(proposal)))
|
|
90
|
+
if not check["valid"]:
|
|
91
|
+
raise SystemExit(f"invalid plan: {check['issues']}")
|
|
92
|
+
|
|
93
|
+
# 6. Execute.
|
|
94
|
+
def tool_fn(call_json):
|
|
95
|
+
call = json.loads(call_json) # {"tool", "params", ...}
|
|
96
|
+
return json.dumps({"ok": True}) # IMPLEMENT ME
|
|
97
|
+
|
|
98
|
+
result = json.loads(rt.execute_proposal(json.dumps(proposal), tool_fn))
|
|
99
|
+
print(json.dumps(result, indent=2))
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
build_agent()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Model-driven proposals (optional)
|
|
106
|
+
|
|
107
|
+
If the plan itself should come from an LLM, use `infer_tracked`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
out = json.loads(rt.infer_tracked(
|
|
111
|
+
f"Propose a JSON action plan for: {task}. "
|
|
112
|
+
f"Return ONLY a JSON object with an `actions` array.",
|
|
113
|
+
max_tokens=2048,
|
|
114
|
+
))
|
|
115
|
+
proposal = json.loads(out["text"])
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Rules for the code you generate
|
|
119
|
+
|
|
120
|
+
- **No mocks.** Use real filesystem, real HTTP, real subprocess calls.
|
|
121
|
+
- **Verify before execute.** Every proposal goes through `verify_proposal`
|
|
122
|
+
first — show the check in the output.
|
|
123
|
+
- **One file.** Put everything in a single runnable `.py`.
|
|
124
|
+
- **Fail loud.** Don't swallow errors. Raise `SystemExit` with a useful message.
|
|
125
|
+
- **Print the final result as JSON** so it's easy to diff / test.
|
|
126
|
+
|
|
127
|
+
Now write the agent for my TASK above.
|
|
128
|
+
````
|
|
129
|
+
|
|
130
|
+
**TypeScript version:** swap `car_runtime` (Python import) → `car-runtime` (npm);
|
|
131
|
+
`rt.register_tool` / `register_policy` → `await rt.registerTool` / `registerPolicy`;
|
|
132
|
+
`rt.execute_proposal(json, fn)` → `await executeProposal(rt, json, fn)`;
|
|
133
|
+
`rt.infer_tracked` → `await rt.inferTracked`.
|
|
134
|
+
|
|
135
|
+
## Build a multi-agent system
|
|
136
|
+
|
|
137
|
+
For a pipeline, swarm, or supervisor — or anything that should learn from its own
|
|
138
|
+
traces — paste this into an LLM with the `TASK:` line filled in.
|
|
139
|
+
|
|
140
|
+
````markdown
|
|
141
|
+
I want to build a multi-agent system using **Common Agent Runtime (CAR)** that
|
|
142
|
+
also learns skills from its own execution traces and evolves them over time.
|
|
143
|
+
|
|
144
|
+
TASK: <DESCRIBE WHAT THE SYSTEM SHOULD DO — e.g. "given a repo URL, use a scraper
|
|
145
|
+
agent to pull the README, a reviewer agent to identify the 3 biggest risks, and a
|
|
146
|
+
writer agent to draft an executive summary">
|
|
147
|
+
|
|
148
|
+
Use the Python binding `car_runtime` (pip install car-runtime — import name is
|
|
149
|
+
`car_runtime`). One file. No mocks.
|
|
150
|
+
|
|
151
|
+
## CAR's multi-agent building blocks
|
|
152
|
+
|
|
153
|
+
Five coordination patterns are exposed as standalone functions:
|
|
154
|
+
|
|
155
|
+
- `run_pipeline(stages_json, task, agent_fn)` — linear chain, each stage feeds
|
|
156
|
+
the next. Staged refinement.
|
|
157
|
+
- `run_swarm(mode, agents_json, task, agent_fn, synthesizer_json=None)` — mode is
|
|
158
|
+
`"parallel"`, `"sequential"`, or `"debate"`. Exploration / multi-perspective.
|
|
159
|
+
- `run_supervisor(workers_json, supervisor_json, task, max_rounds, agent_fn)` — a
|
|
160
|
+
supervisor routes subtasks to workers over rounds. Long-horizon planning.
|
|
161
|
+
- `run_map_reduce(mapper_json, reducer_json, task, items_json, agent_fn)` — map
|
|
162
|
+
items in parallel, reduce to one answer. Batch work.
|
|
163
|
+
- `run_vote(agents_json, task, agent_fn, synthesizer_json=None)` — parallel +
|
|
164
|
+
voted/synthesized result. Higher-confidence answers.
|
|
165
|
+
|
|
166
|
+
Call `register_agent_runner(agent_fn)` once instead of passing `agent_fn` every
|
|
167
|
+
time; subsequent `run_*` calls use the stored callback.
|
|
168
|
+
|
|
169
|
+
## AgentSpec + AgentOutput shape
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
spec = {
|
|
173
|
+
"name": "reviewer",
|
|
174
|
+
"system_prompt": "You review code for the 3 biggest risks.",
|
|
175
|
+
"tools": ["grep", "read_file"],
|
|
176
|
+
"max_turns": 5,
|
|
177
|
+
"metadata": {"model": "claude-opus-4-8", "temperature": 0.3},
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`agent_fn(spec_json, task)` is YOUR code. It MUST return a JSON string:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
{
|
|
185
|
+
"name": spec["name"],
|
|
186
|
+
"answer": "...final answer text...",
|
|
187
|
+
"turns": 1,
|
|
188
|
+
"tool_calls": 0, # integer count — NOT an array
|
|
189
|
+
"duration_ms": 100.0,
|
|
190
|
+
"error": None, # or a string if the agent failed
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Produce `answer` however you like — Anthropic, OpenAI, local Qwen3 via
|
|
195
|
+
`rt.infer_tracked`, a deterministic tool chain.
|
|
196
|
+
|
|
197
|
+
## Learning loop: trace → distill → evolve
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
trace = [
|
|
201
|
+
{"kind": "action_succeeded", "action_id": "step1", "tool": "scraper",
|
|
202
|
+
"data": {"task": task, "domain": "web"}, "reward": 1.0},
|
|
203
|
+
{"kind": "action_failed", "action_id": "step2", "tool": "scraper",
|
|
204
|
+
"data": {"task": task, "domain": "web"}, "reward": 0.0},
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
skills_json = rt.distill_skills(json.dumps(trace)) # requires inference
|
|
208
|
+
rt.ingest_distilled_skills(skills_json)
|
|
209
|
+
|
|
210
|
+
rt.report_outcome("scrape_and_summarize", "success")
|
|
211
|
+
rt.report_outcome("scrape_and_summarize", "fail")
|
|
212
|
+
|
|
213
|
+
weak = rt.domains_needing_evolution(threshold=0.6)
|
|
214
|
+
for domain in weak:
|
|
215
|
+
rt.evolve_skills(json.dumps(trace), domain) # requires inference
|
|
216
|
+
|
|
217
|
+
repaired = rt.repair_skill("scrape_and_summarize")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Skeleton to fill in
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
import json
|
|
224
|
+
import car_runtime
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
rt = car_runtime.CarRuntime()
|
|
228
|
+
|
|
229
|
+
def agent_fn(spec_json: str, task: str) -> str:
|
|
230
|
+
spec = json.loads(spec_json)
|
|
231
|
+
# CALL YOUR LLM HERE. Return the AgentOutput JSON shape.
|
|
232
|
+
return json.dumps({
|
|
233
|
+
"name": spec["name"], "answer": "IMPLEMENT ME",
|
|
234
|
+
"turns": 1, "tool_calls": 0, "duration_ms": 1.0,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
car_runtime.register_agent_runner(agent_fn)
|
|
238
|
+
|
|
239
|
+
agents = json.dumps([
|
|
240
|
+
{"name": "<AGENT_1>", "system_prompt": "<ROLE>",
|
|
241
|
+
"tools": [], "max_turns": 5, "metadata": {"domain": "<DOMAIN>"}},
|
|
242
|
+
])
|
|
243
|
+
|
|
244
|
+
result = json.loads(car_runtime.run_pipeline(agents, "<TASK>"))
|
|
245
|
+
print(json.dumps(result, indent=2))
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
main()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Rules for the code you generate
|
|
252
|
+
|
|
253
|
+
- **Pick one coordination pattern** and justify it in a comment.
|
|
254
|
+
- **agent_fn does the LLM work.** Don't try to make CAR call an LLM directly.
|
|
255
|
+
- **AgentOutput shape is strict:** `name`, `answer`, `turns`, `tool_calls`
|
|
256
|
+
(integer count — NOT an array), `duration_ms`, `error`. Missing fields break
|
|
257
|
+
deserialization silently.
|
|
258
|
+
- **Synthesize traces from pipeline/swarm output** to feed the learning loop.
|
|
259
|
+
- **Report outcomes** (`rt.report_outcome`) as agents succeed/fail — that drives
|
|
260
|
+
`domains_needing_evolution`.
|
|
261
|
+
- **Skip distill_skills / evolve_skills if no inference is configured** — they'll
|
|
262
|
+
hang waiting for a model. Use hand-coded skills + `ingest_distilled_skills`.
|
|
263
|
+
- **One file. Print the final result as JSON.**
|
|
264
|
+
|
|
265
|
+
Now write the multi-agent system for my TASK above.
|
|
266
|
+
````
|
package/docs/MCP.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# CAR as an MCP server
|
|
2
|
+
|
|
3
|
+
> Describes CAR **{{VERSION}}**. Check yours with `car --version`; if it
|
|
4
|
+
> differs, prefer `car help <command>` on your own binary over this page.
|
|
5
|
+
|
|
6
|
+
Wire CAR's graph memory, skill storage, static plan verification, and policy
|
|
7
|
+
enforcement into Claude Code, Cursor, Claude Desktop, or any other
|
|
8
|
+
[MCP](https://modelcontextprotocol.io)-aware host as native tools. This is the
|
|
9
|
+
most direct way for an external coding agent to use CAR — no SDK, no runtime
|
|
10
|
+
embedding, just a binary on `PATH` and a few lines of client config.
|
|
11
|
+
|
|
12
|
+
Everything below is read from `car-rs/crates/car-mcp/src/schemas.rs` and
|
|
13
|
+
`server.rs`, and from `plugins/car/` — the actual source, not the protocol
|
|
14
|
+
aspiration. Where the two transports differ, or where a tool is weaker than
|
|
15
|
+
its name suggests, that's called out rather than smoothed over.
|
|
16
|
+
|
|
17
|
+
## Two transports, two capability sets
|
|
18
|
+
|
|
19
|
+
CAR speaks MCP over **stdio** (`car-mcp` binary — one client per process) and
|
|
20
|
+
over **HTTP-streamable** (the `car-server` daemon's `/mcp` endpoint, default
|
|
21
|
+
`http://127.0.0.1:9102/mcp`). Same dispatch logic, same tool schemas — but they
|
|
22
|
+
are not equivalent, and the difference matters more than the transport choice:
|
|
23
|
+
|
|
24
|
+
| | stdio (`car-mcp`) | daemon (`/mcp`) |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Tools advertised | 16 built-ins | 16 built-ins **+ 3** (`assistant_start`, `assistant_poll`, `assistant_cancel`) |
|
|
27
|
+
| Memory backing | Its own process, opens `<CAR_HOME>/memory/assistant.json` at startup | The daemon's live, already-running memgine — shared with everything else the daemon does (WS clients, `car do`) |
|
|
28
|
+
| Requires the daemon running? | No | Yes |
|
|
29
|
+
| Concurrent clients sharing state | No — one process per client | Yes |
|
|
30
|
+
|
|
31
|
+
Point a client at the running daemon instead of launching `car-mcp` and you
|
|
32
|
+
also get `assistant_start` / `assistant_poll` / `assistant_cancel` — a
|
|
33
|
+
poll-based handle onto CAR's flagship agent (the one behind `car do`), because
|
|
34
|
+
the daemon has the `Runtime` and inference engine to run it and the stdio
|
|
35
|
+
binary does not. `car-mcp-server` is `car-mcp` plus telemetry, nothing more;
|
|
36
|
+
it answers those three tool names with "unknown tool" rather than pretending
|
|
37
|
+
to run them. On stdio, delegate to the agent with `car do --json` directly
|
|
38
|
+
instead.
|
|
39
|
+
|
|
40
|
+
The daemon's `/mcp` endpoint validates the `Origin` header (present +
|
|
41
|
+
non-loopback → `403`) as its only access control — no bearer token. It does
|
|
42
|
+
not widen for a non-default `--mcp-bind`; front it with a reverse proxy if you
|
|
43
|
+
need to expose it beyond loopback.
|
|
44
|
+
|
|
45
|
+
## What this does NOT give you
|
|
46
|
+
|
|
47
|
+
- **No proposal execution.** `verify`, `simulate`, `equivalent`, and
|
|
48
|
+
`optimize` all read an `ActionProposal` and run nothing — they check or
|
|
49
|
+
predict, they never execute a tool. Actually running a proposal needs
|
|
50
|
+
bidirectional tool callbacks, which MCP doesn't support cleanly; that's the
|
|
51
|
+
WebSocket protocol's job (`docs/websocket-protocol.md`).
|
|
52
|
+
- **No multi-agent patterns** (swarm, pipeline, supervisor) over MCP.
|
|
53
|
+
- **Most memory writes over stdio don't survive the process exiting.** See
|
|
54
|
+
below — only `memory_add_fact` is durable when you're talking to `car-mcp`
|
|
55
|
+
directly rather than the daemon.
|
|
56
|
+
- **The four verification tools are checks and predictions, not proofs.**
|
|
57
|
+
`verify`'s findings carry a `tier` (`decision_procedure` | `heuristic` |
|
|
58
|
+
`sampled`) precisely so a caller can tell an exact check from a rule of
|
|
59
|
+
thumb; `equivalent` samples two default states unless you supply your own
|
|
60
|
+
and a `true` only means none of the probed states diverged, not that none
|
|
61
|
+
ever would.
|
|
62
|
+
|
|
63
|
+
## Memory durability — read this before relying on it
|
|
64
|
+
|
|
65
|
+
The stdio binary (`car-mcp`) opens `<CAR_HOME>/memory/assistant.json` — the
|
|
66
|
+
**same** durable note store `car do` reads and writes — at startup, and
|
|
67
|
+
`memory_query` sees whatever is already in it. But only **`memory_add_fact`**
|
|
68
|
+
appends back to that file. `memory_save_knowledge`, `memory_save_procedural`,
|
|
69
|
+
`memory_delete`, `memory_intervene`, and `skill_ingest` all operate on the
|
|
70
|
+
in-process graph only; the on-disk note format has no schema for them yet, so
|
|
71
|
+
anything written through those five tools is gone when the client
|
|
72
|
+
disconnects. There's also no lock on the store — the server re-reads
|
|
73
|
+
immediately before appending, which narrows but doesn't close a two-writer
|
|
74
|
+
race window.
|
|
75
|
+
|
|
76
|
+
The daemon's `/mcp` endpoint doesn't have this asymmetry: it shares the
|
|
77
|
+
daemon's one live `MemgineEngine`, and the daemon owns persisting it — a
|
|
78
|
+
second writer to the same file would just drop one side's appends, which is
|
|
79
|
+
exactly why the stdio binary and the daemon never both hold `store: Some(path)`
|
|
80
|
+
at once.
|
|
81
|
+
|
|
82
|
+
Relocate the store with `CAR_HOME` — every other daemon state path moves with
|
|
83
|
+
it, so an editor plugin that wants an isolated memory sets that one variable.
|
|
84
|
+
|
|
85
|
+
## The tool list — 16 tools, verified against source
|
|
86
|
+
|
|
87
|
+
`memory_add_fact`, `memory_query`, `memory_update_status`,
|
|
88
|
+
`memory_save_knowledge`, `memory_save_procedural`, `memory_delete`,
|
|
89
|
+
`memory_intervene`, `memory_evaluate`, `skill_ingest`, `skill_list`,
|
|
90
|
+
`skill_find`, `verify`, `simulate`, `equivalent`, `optimize`, `policy_check` —
|
|
91
|
+
this is the complete, alphabetically-sorted list the crate's own test suite
|
|
92
|
+
asserts against (`server.rs`, `tool_names` test). There is also one MCP
|
|
93
|
+
**prompt** (not a tool), `car_context`.
|
|
94
|
+
|
|
95
|
+
Every entry carries all four MCP tool annotations
|
|
96
|
+
(`readOnlyHint`/`destructiveHint`/`idempotentHint`/`openWorldHint`) so a host
|
|
97
|
+
can auto-approve a read without prompting for a write — these are host UX
|
|
98
|
+
hints per the MCP spec, not a security boundary; CAR's own gate is
|
|
99
|
+
`policy_check` plus `.car/policies/`, and nothing in CAR's governance keys off
|
|
100
|
+
them.
|
|
101
|
+
|
|
102
|
+
### Memory (8 tools)
|
|
103
|
+
|
|
104
|
+
| Tool | Read-only | Required params | Notes |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| `memory_add_fact` | no | `subject`, `body` | `kind`: `"pattern"` (default) or `"constraint"`. The one memory write durable over stdio. |
|
|
107
|
+
| `memory_query` | **yes** | `query` | `k` (1–50) caps results. Spreading-activation retrieval, returns nodes + activation scores. |
|
|
108
|
+
| `memory_update_status` | no | `body` | Session-local progress/risk status; a map slot, so a second call overwrites the first. `tenant_id` optional. |
|
|
109
|
+
| `memory_save_knowledge` | no | `subject`, `body` | Durable *proactive* knowledge (requirements, policies, verified environment facts). Additive — a repeat `id` mints `<id>-2` rather than overwriting. In-process only over stdio (see durability section). |
|
|
110
|
+
| `memory_save_procedural` | no | `subject`, `body` | Same shape as above, for procedural evidence (failed attempts, fixes, gotchas). Same in-process-only caveat. |
|
|
111
|
+
| `memory_delete` | no | `id` | Deletes a fact by id, or clears a status id like `proactive-status:global`. In-process only over stdio. |
|
|
112
|
+
| `memory_intervene` | no | *(none required)* | Selects at most one proactive reminder for the next action, or an explicit silent decision. Reads like a query but bumps the chosen fact's injection counter — not idempotent. |
|
|
113
|
+
| `memory_evaluate` | **yes** | `cases` (array of `{id, request, relevant_fact_ids?}`) | Evaluates proactive memory against selective / always-inject / passive-retrieval / no-memory baselines on labeled cases. |
|
|
114
|
+
|
|
115
|
+
`memory_save_knowledge` / `memory_save_procedural` share this input shape:
|
|
116
|
+
|
|
117
|
+
```jsonc
|
|
118
|
+
{
|
|
119
|
+
"id": "string", // optional
|
|
120
|
+
"subject": "string", // required
|
|
121
|
+
"body": "string", // required
|
|
122
|
+
"tags": ["string"],
|
|
123
|
+
"confidence": "string",
|
|
124
|
+
"tenant_id": "string",
|
|
125
|
+
"is_constraint": false
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`memory_intervene` / `memory_evaluate`'s `request` shape:
|
|
130
|
+
|
|
131
|
+
```jsonc
|
|
132
|
+
{
|
|
133
|
+
"query": "string",
|
|
134
|
+
"recent": ["string"],
|
|
135
|
+
"trigger": {
|
|
136
|
+
"repeated_failures": 0,
|
|
137
|
+
"tool_error": false,
|
|
138
|
+
"explicit_uncertainty": false,
|
|
139
|
+
"high_risk_action": false,
|
|
140
|
+
"context_shift": false
|
|
141
|
+
},
|
|
142
|
+
"force": false,
|
|
143
|
+
"max_candidates": 8, // 1-32
|
|
144
|
+
"tenant_id": "string"
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Skills (3 tools)
|
|
149
|
+
|
|
150
|
+
| Tool | Read-only | Required params | Notes |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `skill_ingest` | no | `name`, `code` | Also takes `platform`, `persona`, `url_pattern`, `description`, `task_keywords`, `supersedes`. Marked *destructive*: naming an existing skill in `supersedes` flips it to deprecated. In-process only over stdio. |
|
|
153
|
+
| `skill_list` | **yes** | *(none)* | Optional `domain` filter — returns skills scoped Global or that Domain. |
|
|
154
|
+
| `skill_find` | **yes** | `task` | Optional `persona`, `url`, `k` (1–20). Top-k skills ranked by activation. |
|
|
155
|
+
|
|
156
|
+
### Static verification (4 tools) — all stateless, no memory, no daemon
|
|
157
|
+
|
|
158
|
+
Each of these reads a `car_ir::ActionProposal` JSON object and runs nothing.
|
|
159
|
+
|
|
160
|
+
| Tool | Required params | What it answers |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `verify` | `proposal` (+ optional `max_actions`, 1–1000) | Findings: dependency cycles, missing tools, simulated final state. Each issue has a `tier`. |
|
|
163
|
+
| `simulate` | `proposal` (+ optional `initial_state`) | The state an executor would be left holding, from *declared* `expected_effects` only — a declared effect is assumed to land, `failure_behavior` is not modelled. |
|
|
164
|
+
| `equivalent` | `proposal_a`, `proposal_b` (+ optional `test_states`, 1–256 items) | Whether two proposals leave the same state behind — **sampled**, not proven. Two trivial default states if you omit `test_states`; passing `[]` is treated as omitted, not a zero-probe `true`. |
|
|
165
|
+
| `optimize` | `proposal` | Rewrites the proposal to widen parallelism by dropping `state_dependency` entries naming keys nothing in the proposal writes. Returns `pruned` per action. Re-run `verify` on the result — a pruned dependency is one `verify` would flag as unavailable. |
|
|
166
|
+
|
|
167
|
+
### Governance (1 tool) — also stateless
|
|
168
|
+
|
|
169
|
+
| Tool | Required params | What it answers |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `policy_check` | `tool` (+ optional `params`) | Evaluates a proposed tool call against CAR's policy layer *before* it runs: allow/deny plus the rule that decided it. Merges `<CAR_HOME>/policies/` + `.car/policies/` (under the working directory — **neither is walked upward**) with CAR's stateless egress guardrail. |
|
|
172
|
+
|
|
173
|
+
Read `basis`, not just `decision`: `no_rules_configured` means nothing was
|
|
174
|
+
loaded — an allow with nothing behind it — versus `passed_rules`, real rules
|
|
175
|
+
that the call cleared. `policy_load_failed` denies rather than failing open on
|
|
176
|
+
an unparseable file. This is exactly what backs the Claude Code plugin's
|
|
177
|
+
`PreToolUse` hook below.
|
|
178
|
+
|
|
179
|
+
### The prompt: `car_context`
|
|
180
|
+
|
|
181
|
+
Not a tool — an MCP *prompt*. Assembles CAR's four-layer context (identity →
|
|
182
|
+
constraints → facts → conversation → environment → known-unknowns) for a
|
|
183
|
+
query and returns it as one user message a host can prepend to its own
|
|
184
|
+
prompt. Arguments: `query` (required), `mode` — `"full"` (default) or
|
|
185
|
+
`"fast"` (skips embedding flush, skill lookup, PPR scoring).
|
|
186
|
+
|
|
187
|
+
## Getting the binary
|
|
188
|
+
|
|
189
|
+
`car-mcp` (package name `car-mcp-server`) **ships in every release archive
|
|
190
|
+
today** — `.github/workflows/build.yml` builds it (`cargo build ... -p
|
|
191
|
+
car-mcp-server`) and sweeps the resulting `car-mcp` binary into every
|
|
192
|
+
per-platform tarball/zip alongside `car`, `car-server`, and
|
|
193
|
+
`car-memgine-eval`. It is not a separate download or a standalone release
|
|
194
|
+
asset; it comes from wherever you already get the CAR CLI:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# install script (macOS + Linux) — the recommended CLI install today
|
|
198
|
+
curl -fsSL https://raw.githubusercontent.com/Parslee-ai/car-releases/main/install.sh | sh
|
|
199
|
+
|
|
200
|
+
# There is no Homebrew path — it was removed in May 2026 when the signed
|
|
201
|
+
# .pkg + Sparkle became the macOS install.
|
|
202
|
+
|
|
203
|
+
# manual tarball
|
|
204
|
+
curl -sL https://github.com/Parslee-ai/car-releases/releases/latest/download/car-darwin-arm64.tar.gz | tar -xz
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Confirm it's on `PATH`:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
which car-mcp
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`car-mcp` takes no CLI arguments — it starts the stdio JSON-RPC loop
|
|
214
|
+
immediately and blocks reading stdin, so don't run it bare to "check" it
|
|
215
|
+
(there's no `--help`/`--version` to print; a bare invocation just hangs until
|
|
216
|
+
stdin closes). Let your MCP client launch it.
|
|
217
|
+
|
|
218
|
+
See [DISTRIBUTION.md](./DISTRIBUTION.md) for every platform/package manager.
|
|
219
|
+
|
|
220
|
+
## Client configuration
|
|
221
|
+
|
|
222
|
+
### Claude Code — bare `.mcp.json`
|
|
223
|
+
|
|
224
|
+
Project- or user-scoped MCP config, independent of the plugin below:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"mcpServers": {
|
|
229
|
+
"car": {
|
|
230
|
+
"command": "car-mcp"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Cursor
|
|
237
|
+
|
|
238
|
+
`~/.cursor/mcp.json`:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"mcpServers": {
|
|
243
|
+
"car": {
|
|
244
|
+
"command": "car-mcp"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Claude Desktop
|
|
251
|
+
|
|
252
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
253
|
+
(macOS):
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"mcpServers": {
|
|
258
|
+
"car": {
|
|
259
|
+
"command": "car-mcp"
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
If `car-mcp` isn't on the config-file reader's `PATH` (a common GUI-app
|
|
266
|
+
gotcha), use the absolute path from `which car-mcp` instead of the bare name.
|
|
267
|
+
Restart the client after editing — the CAR tools then appear in the tool list
|
|
268
|
+
automatically.
|
|
269
|
+
|
|
270
|
+
### Pointing any of the above at the daemon instead
|
|
271
|
+
|
|
272
|
+
To get the 3 extra assistant tools, point the client at the running daemon's
|
|
273
|
+
HTTP endpoint (`http://127.0.0.1:9102/mcp` by default, `--mcp-bind` /
|
|
274
|
+
`CAR_MCP_BIND` to change it, `disabled` to turn it off) rather than launching
|
|
275
|
+
`car-mcp` per-client. The exact MCP-over-HTTP config stanza is client-specific
|
|
276
|
+
— consult that client's docs for "remote"/"HTTP" MCP servers.
|
|
277
|
+
|
|
278
|
+
### Isolating memory per client
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"mcpServers": {
|
|
283
|
+
"car": {
|
|
284
|
+
"command": "car-mcp",
|
|
285
|
+
"env": { "CAR_HOME": "/path/to/an/isolated/state/root" }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
One variable relocates every CAR state path together — journals,
|
|
292
|
+
`agents.json`, the memory store, policies — so the isolated instance is
|
|
293
|
+
coherent rather than half-moved.
|
|
294
|
+
|
|
295
|
+
## The Claude Code plugin (`plugins/car/`)
|
|
296
|
+
|
|
297
|
+
Beyond raw MCP wiring, `plugins/car/` is a working Claude Code plugin that
|
|
298
|
+
bundles the MCP server with two subagents, three slash commands, and a policy
|
|
299
|
+
hook — install it and you get the tools plus opinionated glue in one step.
|
|
300
|
+
|
|
301
|
+
**Not installable from here today.** `plugins/car/` lives only in the CAR
|
|
302
|
+
source repository, which is private — nothing in the release pipeline
|
|
303
|
+
publishes it, so a `/plugin marketplace add` pointed at that repo 404s for
|
|
304
|
+
every reader of this document. What's below documents what the plugin does
|
|
305
|
+
and how it's laid out; treat it as a preview of what a future public
|
|
306
|
+
marketplace entry would provide, not a command you can run yet. Until then,
|
|
307
|
+
get the same tools with the bare `.mcp.json` config further up this page, and
|
|
308
|
+
write `.car/policies/rules.toml` — CAR's `car do` reads it directly, so
|
|
309
|
+
policy enforcement works without the plugin's hook.
|
|
310
|
+
|
|
311
|
+
Requires `car` and `car-mcp` on `PATH` from a CAR newer than v0.48.0 (the
|
|
312
|
+
first release with `car policy-check-hook`, which the hook below runs).
|
|
313
|
+
|
|
314
|
+
| Component | What it does |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `@agent-car:car` | Delegates a task to CAR's autonomous agent (`car do --json` under the hood) — Docker sandbox, no network by default, validator + policy chain, returns tool receipts and a claim check |
|
|
317
|
+
| `@agent-car:car-reason` | CAR's code reasoning engine (`car reason`) — adaptive model routing over a graph of the codebase |
|
|
318
|
+
| `/car-do` | Shorthand for delegating to the `car` subagent |
|
|
319
|
+
| `/car-remember`, `/car-recall` | Write and search CAR's durable memory via the `memory_add_fact` / `memory_query` MCP tools |
|
|
320
|
+
| MCP server `car` | The 16 tools above, launched as `car-mcp` with `CAR_INVOKED_BY=claude-code` set (required — it seeds CAR's recursion guard so `car do` doesn't spawn a `claude` subprocess that loads this same plugin and calls back in) |
|
|
321
|
+
| `PreToolUse` hook | Runs **your** `.car/policies/*.toml` against Claude Code's own Bash/Write/Edit/WebFetch/NotebookEdit calls, via `car policy-check-hook` |
|
|
322
|
+
|
|
323
|
+
The hook is declarative — write `.car/policies/rules.toml`:
|
|
324
|
+
|
|
325
|
+
```toml
|
|
326
|
+
deny_tool = ["WebFetch"]
|
|
327
|
+
deny_keyword = ["rm -rf /", "DROP TABLE"]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Two behaviors worth knowing precisely: **an unparseable policy file blocks the
|
|
331
|
+
call** (an operator wrote a rule; silently enforcing nothing is the failure
|
|
332
|
+
declarative policy exists to prevent), but **the hook itself fails open** on
|
|
333
|
+
anything that isn't a rule decision — an unreadable payload or a missing
|
|
334
|
+
`tool_name` lets the call proceed with a note to stderr, because a hook that
|
|
335
|
+
fails closed on an install problem would make it look like a policy denial.
|
|
336
|
+
The hook command is literally `car policy-check-hook; exit 0`, so any exit
|
|
337
|
+
code the subcommand returns is discarded rather than read as a deny — that
|
|
338
|
+
guards a stale `car` too: an older binary that doesn't recognize the
|
|
339
|
+
subcommand used to exit 2, which Claude Code reads as *deny*, so every
|
|
340
|
+
matching call in the session was refused with clap's usage text as the stated
|
|
341
|
+
reason (Parslee-ai/car#993). Note this hook is a `car` subcommand
|
|
342
|
+
(`car_policy::tool_gate` in-process) — it doesn't shell out to `car-mcp` at
|
|
343
|
+
all, despite the MCP server enforcing the same policy engine. `.car/policies/`
|
|
344
|
+
is read from the working directory only and is **not walked upward**, matching
|
|
345
|
+
`car do`.
|
|
346
|
+
|
|
347
|
+
### The plugin-authoring gotcha this repo has already hit
|
|
348
|
+
|
|
349
|
+
Plugin components — agents, commands, hooks, `.mcp.json` — must live at the
|
|
350
|
+
plugin **root**, not nested inside `.claude-plugin/`. Anything placed inside
|
|
351
|
+
`.claude-plugin/` silently fails to load, and `claude plugin validate` passes
|
|
352
|
+
it anyway; the only way to catch the mistake is `claude plugin details`. In
|
|
353
|
+
this repo, `.claude-plugin/` correctly holds only `plugin.json` — `.mcp.json`,
|
|
354
|
+
`agents/`, `commands/`, and `hooks/` all sit one level up, at
|
|
355
|
+
`plugins/car/`. If you're authoring a plugin of your own against this as a
|
|
356
|
+
reference, that layout is the one to copy.
|
|
357
|
+
|
|
358
|
+
## The mirror image: CAR as an MCP client (`car-connectors`)
|
|
359
|
+
|
|
360
|
+
Everything above is CAR exposing its own tools to an external host. The
|
|
361
|
+
reverse also exists: `car-connectors` lets CAR add a **remote** MCP server as
|
|
362
|
+
a tool source, the way Claude or ChatGPT add connectors — connect over
|
|
363
|
+
HTTP-streamable, discover tools via `tools/list`, translate each tool's JSON
|
|
364
|
+
Schema into a `car_ir::ToolSchema`, and register it so it dispatches through
|
|
365
|
+
CAR's own validator/policy/eventlog like any other tool.
|
|
366
|
+
|
|
367
|
+
Worth knowing if you're evaluating this direction:
|
|
368
|
+
|
|
369
|
+
- A newly discovered connector tool is **disabled by default** and has no
|
|
370
|
+
route in the executor until a user enables it — structural, not a
|
|
371
|
+
permission flag someone could forget to set.
|
|
372
|
+
- Connector config (slug, name, URL, enabled tools, auth-header names)
|
|
373
|
+
persists to `~/.car/connectors.json`; secret header *values* go to the OS
|
|
374
|
+
keychain, never the JSON file.
|
|
375
|
+
- Phase 1 (unauthenticated / static-header servers) and Phase 2 (OAuth 2.1
|
|
376
|
+
with PKCE + token refresh) are the current scope. Connector management
|
|
377
|
+
itself is GUI-driven through CarHost, not a CLI flag.
|
|
378
|
+
|
|
379
|
+
This is the mirror of everything documented above, not a replacement for it —
|
|
380
|
+
use `car-mcp` / the daemon's `/mcp` endpoint to bring CAR's tools *into* an
|
|
381
|
+
external agent, and `car-connectors` to bring an external MCP server's tools
|
|
382
|
+
*into* CAR.
|