car-runtime 0.7.0 → 0.8.1

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.
Files changed (3) hide show
  1. package/README.md +33 -5
  2. package/index.d.ts +340 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,6 +3,12 @@
3
3
  Node.js bindings for **Common Agent Runtime** — a deterministic execution
4
4
  layer for AI agents. Models propose; the runtime validates and executes.
5
5
 
6
+ As of v0.8, this package is a **thin daemon client**: every method
7
+ proxies to a singleton `car-server` daemon over WebSocket. The
8
+ binary that runs the inference, holds the per-session memory graph,
9
+ and dispatches tools is shipped alongside the .node module in this
10
+ same package — start it once per host before using the bindings.
11
+
6
12
  ## Install
7
13
 
8
14
  ```bash
@@ -11,20 +17,38 @@ npm install car-runtime
11
17
 
12
18
  The package's install script downloads the matching native binary for your
13
19
  platform from [car-releases](https://github.com/Parslee-ai/car-releases/releases).
14
- Supported platforms: macOS (14+) arm64/x64, Linux x64/arm64 glibc.
20
+ Both the `.node` module **and** the `car-server` daemon binary are
21
+ included. Supported platforms: macOS (14+) arm64/x64, Linux
22
+ x64/arm64 glibc, Windows x64.
23
+
24
+ ## Start the daemon
25
+
26
+ ```bash
27
+ # Foreground (Ctrl-C to stop). Default port 9100; auth on by default.
28
+ npx --package=car-runtime car-server
29
+
30
+ # Or background:
31
+ npx --package=car-runtime car-server &
32
+ ```
33
+
34
+ On macOS, the SwiftUI menubar host (`car-host`) launches the daemon
35
+ automatically; install with `car-host install` (registers a launchd
36
+ LaunchAgent so it comes up at login).
15
37
 
16
38
  ## Quickstart
17
39
 
18
40
  ```typescript
19
41
  import { CarRuntime, executeProposal } from 'car-runtime';
20
42
 
21
- const rt = new CarRuntime();
43
+ const rt = new CarRuntime(); // lazy-connects to ws://127.0.0.1:9100/
22
44
 
23
- // Register tools + policies.
45
+ // Register tools + policies. These calls proxy to the daemon's
46
+ // per-session runtime and persist for the lifetime of this WS
47
+ // connection.
24
48
  await rt.registerTool('shell');
25
49
  await rt.registerPolicy('no_rm', 'deny_tool_param', 'shell', 'command', 'rm -rf');
26
50
 
27
- // Verify before executing.
51
+ // Verify before executing — pure static analysis on the daemon side.
28
52
  const proposal = JSON.stringify({
29
53
  actions: [{
30
54
  id: 'a1',
@@ -38,7 +62,11 @@ const proposal = JSON.stringify({
38
62
  const check = JSON.parse(await rt.verifyProposal(proposal));
39
63
  if (!check.valid) throw new Error(`invalid: ${JSON.stringify(check.issues)}`);
40
64
 
41
- // Execute with a JS tool callback.
65
+ // Execute with a JS tool callback. The callback runs in this Node
66
+ // process; the daemon's WsToolExecutor calls back over the same WS
67
+ // when an action needs to dispatch a tool. Proxied via
68
+ // `register_handler("tools.execute", ...)` on the underlying
69
+ // DaemonClient (phase 7.4).
42
70
  const result = await executeProposal(rt, proposal, async (callJson) => {
43
71
  const { tool, params } = JSON.parse(callJson);
44
72
  // Dispatch to your tool implementation.
package/index.d.ts CHANGED
@@ -4,26 +4,26 @@
4
4
  * caller is expected to `JSON.parse` the result. This keeps the FFI surface
5
5
  * small and avoids coupling the native binding to any specific TS shape.
6
6
  *
7
- * ## Runtime modes (closes #139's cross-process admission gap)
7
+ * ## v0.8.0 daemon-only
8
8
  *
9
- * `new CarRuntime()` runs in one of two modes, selected at construction:
9
+ * Every method talks to the singleton car-server daemon over WebSocket
10
+ * JSON-RPC. There is no embedded-engine fallback (the v0.7.x
11
+ * `CAR_FFI_MODE=embedded` knob is retired). Start `car-server` before
12
+ * using the bindings — on macOS the SwiftUI menubar app launches it for
13
+ * you; on Linux start it manually or via systemd.
10
14
  *
11
- * - **`daemon` (default)** talks to the singleton car-server daemon over
12
- * WebSocket JSON-RPC. Methods that touch shared resources (inference,
13
- * embeddings, state, verification) proxy to the daemon preserving the
14
- * process-wide admission semaphore so two Node consumers don't double-load
15
- * models or oversubscribe the GPU.
16
- * - **`embedded`** — explicit opt-in via `CAR_FFI_MODE=embedded`. Every
17
- * method runs in-process. Useful for notebooks, offline dev, or single-
18
- * process deployments where the caller knows they own the machine.
19
- * Production embedders generally don't want this.
15
+ * The following methods are **not exposed in v0.8** and throw
16
+ * with a clear message connect to the daemon's WebSocket directly
17
+ * for the equivalent flow (see `docs/websocket-protocol.md`):
20
18
  *
21
- * Some methods are **embedded-only by design** because their callback
22
- * surface (`ThreadsafeFunction` for tools, streaming events) doesn't
23
- * survive a network boundary cleanly. Those throw in `daemon` mode with a
24
- * clear message: `executeProposal`, `inferStream`. For streaming inference
25
- * under the singleton-daemon contract, talk to `ws://127.0.0.1:9100/`
26
- * directly.
19
+ * - `executeProposal` use `proposal.submit` JSON-RPC + a `tools.execute`
20
+ * handler on the WS connection
21
+ * - `inferStream`, `transcribeStream`, `dispatchVoiceTurn` daemon
22
+ * streams events over WS notifications
23
+ * - `openSession`, `closeSession`, `registerPolicy(sessionId)` use
24
+ * `session.open` / `session.close` JSON-RPC methods
25
+ * - `stateSnapshot`, `stateKeys` — daemon-side endpoints pending
26
+ * - `removeModel`, `registerModel` — daemon owns models_dir / models.json
27
27
  *
28
28
  * Daemon URL override: `CAR_DAEMON_URL=ws://...` (default
29
29
  * `ws://127.0.0.1:9100`).
@@ -35,11 +35,23 @@ export class CarRuntime {
35
35
 
36
36
  // --- Memory persistence ---
37
37
 
38
- /** Load memory graph from a JSON file. Returns the number of facts loaded. */
39
- loadMemory(path: string): number;
38
+ /**
39
+ * Load memory graph from a JSON file. Returns the number of facts loaded.
40
+ *
41
+ * Daemon-side read: `path` is sandboxed under `~/.car/memory/` (the
42
+ * 2026-05 audit boundary). Relative paths land under the base;
43
+ * absolute paths must already be under the base; `..` segments and
44
+ * symlinks pointing out of the sandbox are rejected.
45
+ */
46
+ loadMemory(path: string): Promise<number>;
40
47
 
41
- /** Persist memory graph to a JSON file (backward-compatible flat format). */
42
- persistMemory(path: string): void;
48
+ /**
49
+ * Persist memory graph to a JSON file (backward-compatible flat format).
50
+ * Returns the number of records written.
51
+ *
52
+ * Daemon-side write: same `~/.car/memory/` sandbox as `loadMemory`.
53
+ */
54
+ persistMemory(path: string): Promise<number>;
43
55
 
44
56
  // --- Tools & policies ---
45
57
 
@@ -952,13 +964,16 @@ export function stopMeeting(
952
964
  summarize?: boolean | null,
953
965
  ): Promise<string>;
954
966
 
955
- export function listMeetings(rt: CarRuntime, rootOverride?: string | null): string;
967
+ export function listMeetings(
968
+ rt: CarRuntime,
969
+ rootOverride?: string | null,
970
+ ): Promise<string>;
956
971
 
957
972
  export function getMeeting(
958
973
  rt: CarRuntime,
959
974
  meetingId: string,
960
975
  rootOverride?: string | null,
961
- ): string;
976
+ ): Promise<string>;
962
977
 
963
978
  // --- Agent registry (file-based, no daemon) ---
964
979
 
@@ -1242,6 +1257,17 @@ export function listShortcuts(argsJson: string): Promise<string>;
1242
1257
  */
1243
1258
  export function runShortcut(argsJson: string): Promise<string>;
1244
1259
 
1260
+ // --- Local notifications ---
1261
+
1262
+ /**
1263
+ * Deliver a user-visible local notification.
1264
+ *
1265
+ * `argsJson` shape: `{ title: string, body: string, subtitle?: string, sound?: string }`.
1266
+ * Returns JSON `{delivered, platform, backend}`. iOS delivery is owned
1267
+ * by the signed host app via UserNotifications.
1268
+ */
1269
+ export function localNotification(argsJson: string): Promise<string>;
1270
+
1245
1271
  // --- Vision OCR (car-vision) ---
1246
1272
 
1247
1273
  /**
@@ -1264,3 +1290,294 @@ export function runShortcut(argsJson: string): Promise<string>;
1264
1290
  * array rather than an error.
1265
1291
  */
1266
1292
  export function visionOcr(argsJson: string): Promise<string>;
1293
+
1294
+ // --- In-process A2A dispatcher (car_a2a) ---
1295
+
1296
+ /**
1297
+ * Dispatch one A2A v1.0 method against the in-process singleton
1298
+ * dispatcher. `method` is the spec method name (`"message/send"`,
1299
+ * `"tasks/get"`, etc., or PascalCase aliases like `"SendMessage"`);
1300
+ * `paramsJson` is the per-method `params` payload. Returns the
1301
+ * JSON-stringified result.
1302
+ *
1303
+ * Streaming methods (`message/stream`, `tasks/resubscribe`) return
1304
+ * a `MethodNotFound` error from the dispatcher's transport-neutral
1305
+ * surface. HTTP+SSE is the supported transport for streaming and
1306
+ * lives outside this FFI wrapper.
1307
+ *
1308
+ * Distinct from the daemon's WS A2A surface — both are valid; using
1309
+ * both in one process gives you two task stores (task ids are
1310
+ * unique per dispatcher).
1311
+ */
1312
+ export function a2aDispatch(method: string, paramsJson: string): Promise<string>;
1313
+
1314
+ // --- Lifecycle-managed agents (car_registry::supervisor) ---
1315
+
1316
+ /**
1317
+ * List every managed agent in `~/.car/agents.json` along with its
1318
+ * runtime status. Returns JSON `[ManagedAgent]`.
1319
+ *
1320
+ * Wire shape matches the daemon's `agents.list` JSON-RPC method
1321
+ * exactly so a host can swap between in-process and WS transports
1322
+ * without reshaping payloads.
1323
+ */
1324
+ export function agentsList(): Promise<string>;
1325
+
1326
+ /**
1327
+ * Re-validate every managed agent's `command` against the
1328
+ * supervisor's `validate_command` rules — used to surface specs that
1329
+ * broke after a system upgrade (Node moved versions, Homebrew pruned
1330
+ * a symlink). Returns JSON `[{ id, command, ok, reason? }]`.
1331
+ *
1332
+ * Pairs with the `interpreter` sugar on `agentsUpsert`: hosts that
1333
+ * resolve at upsert can re-resolve when health fires `ok: false`.
1334
+ */
1335
+ export function agentsHealth(): Promise<string>;
1336
+
1337
+ /**
1338
+ * Add or replace an agent's spec. Persists `~/.car/agents.json`.
1339
+ * The agent is NOT auto-started — call `agentsStart` (or
1340
+ * `auto_start: true` will pick it up on the next boot via the
1341
+ * supervisor's `start_all`).
1342
+ *
1343
+ * `specJson`:
1344
+ * ```jsonc
1345
+ * {
1346
+ * "id": "trader", // filename-safe
1347
+ * "name": "Trader",
1348
+ * "command": "/opt/homebrew/bin/node", // absolute path required
1349
+ * // OR: omit `command` and pass `interpreter: "node" | "python" | ...`
1350
+ * // and the supervisor resolves the interpreter against $PATH
1351
+ * // once at upsert and stores the absolute path in `command`.
1352
+ * "args": ["server.js"],
1353
+ * "cwd": "/path/to/project", // optional
1354
+ * "env": { "K": "V" }, // merged on top of parent's env
1355
+ * "restart": "on_failure", // never | on_failure | always
1356
+ * "max_restarts": 10,
1357
+ * "backoff_secs": 5,
1358
+ * "auto_start": true // included by start_all on boot
1359
+ * }
1360
+ * ```
1361
+ *
1362
+ * Returns JSON `ManagedAgent`.
1363
+ */
1364
+ export function agentsUpsert(specJson: string): Promise<string>;
1365
+
1366
+ /**
1367
+ * Remove an agent's spec. Stops the running child first if it's up.
1368
+ * Idempotent — `{removed: false}` when nothing matched.
1369
+ * Returns JSON `{removed: boolean}`.
1370
+ */
1371
+ export function agentsRemove(id: string): Promise<string>;
1372
+
1373
+ /**
1374
+ * Spawn the agent's child if not already running. No-op when
1375
+ * already `Running` or `Starting`. Resets `restart_count`.
1376
+ * Returns JSON `ManagedAgent`.
1377
+ */
1378
+ export function agentsStart(id: string): Promise<string>;
1379
+
1380
+ /**
1381
+ * Stop the agent and prevent the supervisor from respawning it.
1382
+ * `signal` is `"term"` (SIGTERM with grace, default) or `"kill"`
1383
+ * (SIGKILL immediately). Returns JSON `ManagedAgent`.
1384
+ */
1385
+ export function agentsStop(id: string, signal?: string | null): Promise<string>;
1386
+
1387
+ /**
1388
+ * Stop then start. Equivalent to `agentsStop` followed by
1389
+ * `agentsStart`. Returns JSON `ManagedAgent`.
1390
+ */
1391
+ export function agentsRestart(id: string): Promise<string>;
1392
+
1393
+ /**
1394
+ * Read the last `n` lines from the agent's combined stdout +
1395
+ * stderr log under `~/.car/logs/<id>.{stdout,stderr}.log`.
1396
+ * Defaults to 100 lines. Returns JSON `{lines: string[]}`.
1397
+ */
1398
+ export function agentsTailLog(id: string, n?: number | null): Promise<string>;
1399
+
1400
+ // --- External-agent detection (car-external-agents) ---
1401
+ //
1402
+ // Phase 1 of docs/proposals/external-agent-detection.md — discover
1403
+ // installed agentic CLIs (Claude Code, Codex, Gemini) and report
1404
+ // version + auth-kind heuristic. Per-task invocation lands in Phase 2
1405
+ // alongside `agents.invoke_external`. Wire shape:
1406
+ //
1407
+ // {
1408
+ // "id": "claude-code" | "codex" | "gemini",
1409
+ // "displayName": "Claude Code" | ...,
1410
+ // "binaryPath": "/usr/local/bin/claude",
1411
+ // "version": "1.0.51" | null,
1412
+ // "authKind": "subscription" | "api_key" | "unknown" | "unauthenticated",
1413
+ // "capabilities": { toolUse, mcp, hooks, sessions, streaming },
1414
+ // "detectedAt": <unix-secs>
1415
+ // }
1416
+
1417
+ /**
1418
+ * Cached snapshot of installed external agents. First call triggers
1419
+ * a detection pass; subsequent calls return the cached list. Pass
1420
+ * `includeHealth: true` to also populate each spec's `health` field
1421
+ * via the tool's auth-status command — slower (one subprocess
1422
+ * spawn per detected adapter) but gives a one-stop "what's
1423
+ * installed AND ready to use" answer. Returns JSON
1424
+ * `[ExternalAgentSpec]` (empty array when nothing installed).
1425
+ *
1426
+ * `ExternalAgentSpec.health` shape (when populated):
1427
+ * { id, status, details, reason?, checked_at }
1428
+ * status: "ready" | "not_configured" | "expired" | "network_error" | "unknown"
1429
+ *
1430
+ * The `auth_kind` field is **deprecated** (Phase 2 stage 1) — modern
1431
+ * builds keep credentials in OS keystores so the heuristic falls
1432
+ * through to "unknown" for the most common installs. Prefer `health`.
1433
+ */
1434
+ export function agentsListExternal(
1435
+ includeHealth?: boolean | null,
1436
+ ): Promise<string>;
1437
+
1438
+ /**
1439
+ * Force re-detection of installed external agents. Updates the
1440
+ * presence cache and returns the new snapshot. Pass
1441
+ * `includeHealth: true` to also run ground-truth health checks
1442
+ * (force-refreshing the per-tool 30s TTL cache). Returns JSON
1443
+ * `[ExternalAgentSpec]`.
1444
+ */
1445
+ export function agentsDetectExternal(
1446
+ includeHealth?: boolean | null,
1447
+ ): Promise<string>;
1448
+
1449
+ /**
1450
+ * Ground-truth health check via each tool's own auth-status command
1451
+ * (`claude auth status`, `codex login status`). Replaces the Phase 1
1452
+ * credential-file shape heuristic as the primary signal for "is this
1453
+ * tool ready to invoke." Pass an `id` to check one adapter; omit it
1454
+ * to check every detected adapter. `force: true` bypasses the 30s
1455
+ * per-tool TTL cache.
1456
+ *
1457
+ * Wire shape: `[ExternalAgentHealth]` (when `id` omitted) or
1458
+ * `ExternalAgentHealth` (when `id` supplied), where:
1459
+ *
1460
+ * {
1461
+ * "id": "claude-code" | "codex" | "gemini",
1462
+ * "status": "ready" | "not_configured" | "expired" |
1463
+ * "network_error" | "unknown",
1464
+ * "details": <tool-specific JSON object>,
1465
+ * "reason": <human-readable string when not Ready>,
1466
+ * "checked_at": <unix-secs>
1467
+ * }
1468
+ */
1469
+ export function agentsHealthExternal(
1470
+ id?: string | null,
1471
+ force?: boolean | null,
1472
+ ): Promise<string>;
1473
+
1474
+ /**
1475
+ * Per-task invocation of an external CLI agent (Phase 2 stage 3).
1476
+ * `id` selects the adapter (`"claude-code"` today; `codex` and
1477
+ * `gemini` ship in follow-up PRs). `task` is the prompt. `optionsJson`
1478
+ * is a JSON-encoded `InvokeOptions` — pass `"{}"` or `null` to accept
1479
+ * defaults.
1480
+ *
1481
+ * `InvokeOptions` shape:
1482
+ *
1483
+ * {
1484
+ * "cwd"?: string, // working directory
1485
+ * "allowed_tools"?: string[], // tool allowlist; [] denies all
1486
+ * "max_turns"?: number, // turn cap
1487
+ * "timeout_secs"?: number, // hard deadline (default 300s)
1488
+ * "mcp_endpoint"?: string // MCP server URL passed via
1489
+ * // --mcp-config; daemon callers
1490
+ * // auto-fill from car-server's
1491
+ * // bound /mcp URL. "" opts out.
1492
+ * }
1493
+ *
1494
+ * Returns JSON `InvokeResult`:
1495
+ *
1496
+ * {
1497
+ * "answer": string, // final agent response
1498
+ * "session_id"?: string,
1499
+ * "turns": number,
1500
+ * "tool_calls": number, // tool_use blocks observed
1501
+ * "duration_ms": number,
1502
+ * "total_cost_usd"?: number, // would-be API cost (subscription users don't pay)
1503
+ * "is_error": boolean,
1504
+ * "error"?: string
1505
+ * }
1506
+ *
1507
+ * Cost note: each invocation burns subscription quota. The runner
1508
+ * doesn't gate cost; callers are responsible for rate limiting.
1509
+ */
1510
+ export function agentsInvokeExternal(
1511
+ id: string,
1512
+ task: string,
1513
+ optionsJson?: string | null,
1514
+ ): Promise<string>;
1515
+
1516
+ // --- A2UI surface store (car-a2ui) ---
1517
+ //
1518
+ // NOTE: NAPI-rs converts `a2ui_*` snake-case Rust names to `a2Ui*`
1519
+ // camelCase at the JS boundary because the digit→letter transition
1520
+ // after `a2` is treated as a word boundary by heck's casing rules.
1521
+ // Names below match the actual runtime exports.
1522
+
1523
+ /**
1524
+ * Process-singleton in-process A2UI v0.9 surface store. Wire shapes
1525
+ * match the daemon's WebSocket `a2ui.*` methods exactly, so a host
1526
+ * can move between transports without reshaping payloads.
1527
+ *
1528
+ * Embedded callers share one store across all calls in the process.
1529
+ * Daemon-shared state (across processes) flows over the WebSocket
1530
+ * surface — these functions do NOT proxy to the daemon.
1531
+ *
1532
+ * Returns JSON `A2uiCapabilities` (version, mimeType, catalogs,
1533
+ * components, limits).
1534
+ */
1535
+ export function a2UiCapabilities(): string;
1536
+
1537
+ /**
1538
+ * Apply a single A2UI envelope (`createSurface` | `updateComponents`
1539
+ * | `updateDataModel` | `deleteSurface`). `envelopeJson` is the
1540
+ * direct envelope shape (one message field set). Returns JSON
1541
+ * `A2uiApplyResult` `{surfaceId, deleted, surface?}`.
1542
+ */
1543
+ export function a2UiApply(envelopeJson: string): Promise<string>;
1544
+
1545
+ /**
1546
+ * Extract A2UI envelopes from a carrier payload (`{a2ui: {...}}`,
1547
+ * A2A `DataPart`, artifact `parts`, etc.) and apply each in order.
1548
+ * Owner is auto-extracted from A2A `taskId`/`contextId` shapes.
1549
+ * Returns JSON `{applied: [A2uiApplyResult]}`.
1550
+ */
1551
+ export function a2UiIngest(payloadJson: string): Promise<string>;
1552
+
1553
+ /**
1554
+ * List all live A2UI surfaces in the in-process store. Returns
1555
+ * JSON `[A2uiSurface]`.
1556
+ */
1557
+ export function a2UiSurfaces(): Promise<string>;
1558
+
1559
+ /**
1560
+ * Fetch a surface by id. Returns JSON `A2uiSurface` or `null` if
1561
+ * the surface doesn't exist.
1562
+ */
1563
+ export function a2UiGet(surfaceId: string): Promise<string>;
1564
+
1565
+ /**
1566
+ * Reap surfaces older than `limits.maxSurfaceAgeSecs`. Returns JSON
1567
+ * `{removed: [surfaceId]}` — empty array when nothing was due.
1568
+ */
1569
+ export function a2UiReap(): Promise<string>;
1570
+
1571
+ /**
1572
+ * Submit an A2UI user action (e.g. button click) back to the action
1573
+ * handler. `actionJson` is the action payload. Returns JSON
1574
+ * `{accepted: boolean, result?: any}`.
1575
+ */
1576
+ export function a2UiAction(actionJson: string): Promise<string>;
1577
+
1578
+ /**
1579
+ * Validate a JSON payload against the store's size limits. Returns
1580
+ * JSON `null` on success; rejects with a limit-exceeded error
1581
+ * message otherwise.
1582
+ */
1583
+ export function a2UiValidatePayload(valueJson: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "car-runtime",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Common Agent Runtime — a deterministic execution layer for AI agents",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",