car-runtime 0.53.0 → 0.55.0-nightly.20260920

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 CHANGED
@@ -21,6 +21,46 @@ Both the `.node` module **and** the `car-server` daemon binary are
21
21
  included. Supported platforms: macOS (14+) arm64/x64, Linux
22
22
  x64/arm64 glibc, Windows x64.
23
23
 
24
+ ## Daemon/package compatibility
25
+
26
+ The npm package and the daemon use a versioned JSON-RPC wire protocol. Read
27
+ `carProtocolVersion` from `car-runtime/package.json` when compatibility must be
28
+ checked before the native module loads; `protocolVersion()` returns the same
29
+ number at runtime:
30
+
31
+ ```javascript
32
+ const pkg = require('car-runtime/package.json');
33
+ const { protocolVersion } = require('car-runtime');
34
+
35
+ if (protocolVersion() !== pkg.carProtocolVersion) {
36
+ throw new Error('car-runtime package metadata does not match its native binding');
37
+ }
38
+ ```
39
+
40
+ The daemon handshake fails closed and names both wire versions when the client
41
+ and daemon are incompatible. `CarHost.app` can update its bundled daemon
42
+ independently of project dependencies, so update this package in the project
43
+ that loads it; while CAR is pre-1.0, pinning the package to the daemon's CAR
44
+ release is the safest choice (`npm install car-runtime@<version>`). A caret
45
+ range does not cross a `0.x` minor release.
46
+
47
+ ## Call the daemon
48
+
49
+ `CarRuntime` connects lazily. Params and results are JSON strings; generated typed
50
+ wrappers such as `agentsList` share the same governed connection.
51
+
52
+ ```javascript
53
+ async function daemonCallExample() {
54
+ // BEGIN CALL_DAEMON_FIVE_LINES
55
+ const { CarRuntime } = (await import('car-runtime')).default;
56
+ const car = new CarRuntime();
57
+ car.registerDaemonHandler('tools.execute', async () => JSON.stringify({ ok: true }));
58
+ const methods = JSON.parse(await car.daemonCall('capabilities.list', '{}'));
59
+ return { car, methods, agents: JSON.parse(await car.agentsList('{}')) };
60
+ // END CALL_DAEMON_FIVE_LINES
61
+ }
62
+ ```
63
+
24
64
  ## Start the daemon
25
65
 
26
66
  ```bash
@@ -95,13 +135,18 @@ main({
95
135
  });
96
136
  ```
97
137
 
98
- `runAgent(config, goal)` is also exported for embedding and tests. `main(config)`
99
- provides `--task`, `--json`, and `--serve`. The loop adds `finish`, statically
100
- verifies every proposal before execution, preserves tool-call/result IDs,
101
- honors per-schema `timeoutMs`, traces task and chat runs, and uses request-shaped
102
- inference with prompt-cache breakpoints. It loads the native binding only when a
103
- loop runs, so `require('car-runtime/agent-loop')` is safe for package discovery.
104
- Type declarations ship as `agent-loop.d.ts` and shared config/outcome types in
138
+ `runAgent(config, goal)` is also exported for embedding and tests. Its
139
+ `AgentOutcome.run_id` is the durable key accepted by `runsGetTrace`; it is
140
+ `null` when an older daemon has no `runs.*` surface or did not acknowledge the
141
+ start. `runChatTurn` keeps returning bare text by default; pass
142
+ `{ includeRunId: true }` as its final options argument to receive
143
+ `{ text, run_id }`. `main(config)` provides `--task`, `--json`, and `--serve`.
144
+ The loop adds `finish`, statically verifies every proposal before execution,
145
+ preserves tool-call/result IDs, honors per-schema `timeoutMs`, traces task and
146
+ chat runs, and uses request-shaped inference with prompt-cache breakpoints. It
147
+ loads the native binding only when a loop runs, so
148
+ `require('car-runtime/agent-loop')` is safe for package discovery. Type
149
+ declarations ship as `agent-loop.d.ts` and shared config/outcome types in
105
150
  `index.d.ts`.
106
151
 
107
152
  Full API reference lives in [`index.d.ts`](./index.d.ts). The package also
package/agent-loop.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type {
2
+ AgentChatTurnOptions,
3
+ AgentChatTurnResult,
2
4
  AgentLoopConfig,
3
5
  AgentLoopOptions,
4
6
  AgentOutcome,
@@ -13,6 +15,18 @@ export function runAgent(
13
15
  options?: AgentLoopOptions,
14
16
  ): Promise<AgentOutcome>;
15
17
 
18
+ export function runChatTurn(
19
+ runtime: CarRuntime,
20
+ config: AgentLoopConfig,
21
+ toolSchemas: AgentToolSchema[],
22
+ tools: Record<string, AgentTool>,
23
+ toolTimeouts: Record<string, number>,
24
+ sessionId: string,
25
+ messages: Record<string, unknown>[],
26
+ requestedModel: string | null | undefined,
27
+ options: AgentChatTurnOptions & { includeRunId: true },
28
+ ): Promise<AgentChatTurnResult>;
29
+
16
30
  export function runChatTurn(
17
31
  runtime: CarRuntime,
18
32
  config: AgentLoopConfig,
@@ -22,6 +36,7 @@ export function runChatTurn(
22
36
  sessionId: string,
23
37
  messages: Record<string, unknown>[],
24
38
  requestedModel?: string | null,
39
+ options?: AgentChatTurnOptions & { includeRunId?: false },
25
40
  ): Promise<string>;
26
41
 
27
42
  export function main(config: AgentLoopConfig): Promise<void>;
package/agent-loop.js CHANGED
@@ -157,7 +157,7 @@ async function inferTurn(rt, { model, maxTokens, toolSchemas, messages, toolChoi
157
157
 
158
158
  /**
159
159
  * Run the agent once toward `goal`. Returns an AgentOutcome-shaped object:
160
- * { status, summary, evidence[], metrics{}, tools_called[] }
160
+ * { status, summary, evidence[], metrics{}, tools_called[], run_id }
161
161
  * `status` is one of the six OutcomeStatus values (success|partial_success|
162
162
  * done|give_up|timeout|failure). AgentOutcome is caller-built — CAR does not
163
163
  * return it; we assemble it from what the loop observed.
@@ -166,7 +166,7 @@ async function inferTurn(rt, { model, maxTokens, toolSchemas, messages, toolChoi
166
166
  * proposal we bracket the run open with `rt.runsStart` (daemon mints a durable
167
167
  * `run_id`); after the terminal AgentOutcome is assembled we close it with
168
168
  * `rt.runsComplete`. Both are best-effort and never print, so the daemon traces
169
- * the run for CarHost while older daemons (no runs.*) behave exactly as before.
169
+ * the run for CarHost while older daemons report `run_id: null` and still run.
170
170
  */
171
171
  async function runAgent(config, goal, { maxTurns } = {}) {
172
172
  const { CarRuntime, executeProposal } = runtimeApi();
@@ -198,7 +198,7 @@ async function runAgent(config, goal, { maxTurns } = {}) {
198
198
  // CAR_AGENT_ID (the supervisor injects it) when supervised, else falls back to
199
199
  // config.agentName for the unsupervised one-shot / run_scenarios path.
200
200
  // Best-effort, exactly like registerAgentBasics: a daemon without runs.* (an
201
- // older build) makes this throw, and the run must continue unchanged. NEVER
201
+ // older build) makes this throw, and the run must continue with a null id. NEVER
202
202
  // print here — the last stdout line in --json mode must stay the AgentOutcome
203
203
  // (run_scenarios.py parses it).
204
204
  let runId = null;
@@ -209,7 +209,9 @@ async function runAgent(config, goal, { maxTurns } = {}) {
209
209
  agent_name: config.agentName,
210
210
  outcome_description: config.targetOutcome ?? '',
211
211
  })));
212
- runId = started.run_id ?? null;
212
+ runId = typeof started.run_id === 'string' && started.run_id.length > 0
213
+ ? started.run_id
214
+ : null;
213
215
  } catch { /* daemon lacks runs.* — degrade to untraced behavior */ }
214
216
 
215
217
  // Local Qwen3 models default to "thinking" (they emit a long <think> block
@@ -329,13 +331,17 @@ async function runAgent(config, goal, { maxTurns } = {}) {
329
331
  // before returning (the connection may close right after) so a healthy run is
330
332
  // never raced into `Incomplete`. Best-effort + never prints, mirroring the
331
333
  // open bracket: an older daemon without runs.* (or one that never acked the
332
- // start, leaving runId null) just skips this and behaves as before.
334
+ // start, leaving runId null) just skips completion and returns the null id.
333
335
  if (runId !== null) {
334
336
  try {
335
337
  await rt.runsComplete(JSON.stringify({ run_id: runId, outcome }));
336
338
  } catch { /* daemon lacks runs.* — nothing to report to */ }
337
339
  }
338
340
 
341
+ // `run_id` is package return metadata, not part of the daemon's car-ir
342
+ // AgentOutcome. Add it only after runs.complete so the wire shape stays
343
+ // unchanged. Null reports an older daemon or unacknowledged runs.start.
344
+ outcome.run_id = runId;
339
345
  return outcome;
340
346
  }
341
347
 
@@ -371,9 +377,15 @@ function parseArgs(argv) {
371
377
  // declared policies still gate tool execution, so guardrails (draft-only, etc.)
372
378
  // carry into the conversation.
373
379
 
374
- /** Run one chat turn against a persistent `messages` thread; stream via chatEvent. */
380
+ /**
381
+ * Run one chat turn against a persistent `messages` thread; stream via
382
+ * chatEvent. The default return stays the historical bare string. Callers that
383
+ * need to inspect the durable trace opt into `{ includeRunId: true }` and get
384
+ * `{ text, run_id }` instead.
385
+ */
375
386
  async function runChatTurn(
376
387
  rt, config, toolSchemas, tools, toolTimeouts, sessionId, messages, requestedModel = null,
388
+ options = {},
377
389
  ) {
378
390
  const { executeProposal } = runtimeApi();
379
391
  const turnsCap = config.maxTurns ?? 8;
@@ -399,7 +411,9 @@ async function runChatTurn(
399
411
  agent_name: config.agentName,
400
412
  outcome_description: config.targetOutcome ?? '',
401
413
  })));
402
- runId = started.run_id ?? null;
414
+ runId = typeof started.run_id === 'string' && started.run_id.length > 0
415
+ ? started.run_id
416
+ : null;
403
417
  } catch { /* daemon lacks runs.* — degrade to untraced behavior */ }
404
418
 
405
419
  const metrics = { turns: 0, tool_calls: 0, actions_succeeded: 0, actions_failed: 0 };
@@ -462,20 +476,20 @@ async function runChatTurn(
462
476
  // Run-trace bracket (close). The outcome is assembled exactly as `runAgent`
463
477
  // assembles it, so a chat-driven run and a --task run are the same shape in
464
478
  // the trace and a host reads one code path, not two.
479
+ const outcome = terminal === 'success'
480
+ ? mkOutcome('success', finalText,
481
+ [{ kind: 'tool_result', description: 'finish called', data: { answer: finalText } }], metrics, toolsCalled)
482
+ : (terminal === 'done'
483
+ ? mkOutcome('done', finalText || 'no further actions',
484
+ [{ kind: 'self_assessment', description: finalText || '', data: null }], metrics, toolsCalled)
485
+ : mkOutcome('timeout', `hit ${turnsCap}-turn cap without finishing`,
486
+ [{ kind: 'stop_reason', description: 'max turns', data: { turnsCap } }], metrics, toolsCalled));
465
487
  if (runId !== null) {
466
- const outcome = terminal === 'success'
467
- ? mkOutcome('success', finalText,
468
- [{ kind: 'tool_result', description: 'finish called', data: { answer: finalText } }], metrics, toolsCalled)
469
- : (terminal === 'done'
470
- ? mkOutcome('done', finalText || 'no further actions',
471
- [{ kind: 'self_assessment', description: finalText || '', data: null }], metrics, toolsCalled)
472
- : mkOutcome('timeout', `hit ${turnsCap}-turn cap without finishing`,
473
- [{ kind: 'stop_reason', description: 'max turns', data: { turnsCap } }], metrics, toolsCalled));
474
488
  try {
475
489
  await rt.runsComplete(JSON.stringify({ run_id: runId, outcome }));
476
490
  } catch { /* daemon lacks runs.* — nothing to report to */ }
477
491
  }
478
- return finalText;
492
+ return options?.includeRunId ? { text: finalText, run_id: runId } : finalText;
479
493
  }
480
494
 
481
495
  /** Set up the long-lived chat runtime: register tools/policies + the agent.chat handler. */