car-runtime 0.55.0 → 0.56.1-nightly.20261007
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 +10 -2
- package/agent-loop.d.ts +8 -1
- package/agent-loop.js +136 -38
- package/agent-loop.mjs +1 -1
- package/daemon-wrappers.js +528 -8
- package/docs/CLI.md +82 -36
- package/index.d.ts +331 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,7 @@ const result = await executeProposal(rt, proposal, async (callJson) => {
|
|
|
112
112
|
// Dispatch to your tool implementation.
|
|
113
113
|
return JSON.stringify({ stdout: 'ok' });
|
|
114
114
|
});
|
|
115
|
+
await rt.close(); // disconnect is an alias; a later call reconnects lazily
|
|
115
116
|
```
|
|
116
117
|
|
|
117
118
|
## Packaged agent loop
|
|
@@ -135,7 +136,10 @@ main({
|
|
|
135
136
|
});
|
|
136
137
|
```
|
|
137
138
|
|
|
138
|
-
`runAgent(config, goal)` is also exported for embedding and tests.
|
|
139
|
+
`runAgent(config, goal)` is also exported for embedding and tests. Pass one
|
|
140
|
+
long-lived runtime as `config.runtime` to reuse its daemon connection across
|
|
141
|
+
runs; otherwise the loop creates and closes a runtime for each call. `serveChat`
|
|
142
|
+
uses the same option and returns the runtime it is serving on. Its
|
|
139
143
|
`AgentOutcome.run_id` is the durable key accepted by `runsGetTrace`; it is
|
|
140
144
|
`null` when an older daemon has no `runs.*` surface or did not acknowledge the
|
|
141
145
|
start. `runChatTurn` keeps returning bare text by default; pass
|
|
@@ -143,7 +147,11 @@ start. `runChatTurn` keeps returning bare text by default; pass
|
|
|
143
147
|
`{ text, run_id }`. `main(config)` provides `--task`, `--json`, and `--serve`.
|
|
144
148
|
The loop adds `finish`, statically verifies every proposal before execution,
|
|
145
149
|
preserves tool-call/result IDs, honors per-schema `timeoutMs`, traces task and
|
|
146
|
-
chat runs, and uses request-shaped inference with prompt-cache breakpoints.
|
|
150
|
+
chat runs, and uses request-shaped inference with prompt-cache breakpoints.
|
|
151
|
+
Each inference is recorded before its requested actions with assistant text,
|
|
152
|
+
normalized tool-call IDs, model id, token usage, and duration; `runsGetTrace`
|
|
153
|
+
returns those model turns interleaved with action turns. Recording is
|
|
154
|
+
best-effort, so an older daemon without the method still runs the agent. It
|
|
147
155
|
loads the native binding only when a loop runs, so
|
|
148
156
|
`require('car-runtime/agent-loop')` is safe for package discovery. Type
|
|
149
157
|
declarations ship as `agent-loop.d.ts` and shared config/outcome types in
|
package/agent-loop.d.ts
CHANGED
|
@@ -39,7 +39,14 @@ export function runChatTurn(
|
|
|
39
39
|
options?: AgentChatTurnOptions & { includeRunId?: false },
|
|
40
40
|
): Promise<string>;
|
|
41
41
|
|
|
42
|
+
export function serveChat(config: AgentLoopConfig): Promise<CarRuntime>;
|
|
43
|
+
|
|
42
44
|
export function main(config: AgentLoopConfig): Promise<void>;
|
|
43
45
|
|
|
44
|
-
declare const agentLoop: {
|
|
46
|
+
declare const agentLoop: {
|
|
47
|
+
main: typeof main;
|
|
48
|
+
runAgent: typeof runAgent;
|
|
49
|
+
runChatTurn: typeof runChatTurn;
|
|
50
|
+
serveChat: typeof serveChat;
|
|
51
|
+
};
|
|
45
52
|
export default agentLoop;
|
package/agent-loop.js
CHANGED
|
@@ -138,6 +138,40 @@ function makeToolCallback(tools, toolTimeouts = {}, onFinish = null) {
|
|
|
138
138
|
// single full re-write of the prefix, and every surviving turn then reads at
|
|
139
139
|
// ~0.1x.
|
|
140
140
|
//
|
|
141
|
+
// Registration belongs to the daemon connection, not to an individual run.
|
|
142
|
+
// Remember what this package installed so a caller-owned runtime can serve
|
|
143
|
+
// hundreds of sequential runs without appending the same policy hundreds of
|
|
144
|
+
// times. Weak keys ensure this bookkeeping never keeps a runtime/socket alive.
|
|
145
|
+
const RUNTIME_REGISTRATION = new WeakMap();
|
|
146
|
+
|
|
147
|
+
async function prepareRuntime(rt, toolSchemas, policies) {
|
|
148
|
+
let state = RUNTIME_REGISTRATION.get(rt);
|
|
149
|
+
if (!state) {
|
|
150
|
+
state = { basicsAttempted: false, tools: new Set(), policies: new Set() };
|
|
151
|
+
RUNTIME_REGISTRATION.set(rt, state);
|
|
152
|
+
}
|
|
153
|
+
if (!state.basicsAttempted) {
|
|
154
|
+
state.basicsAttempted = true;
|
|
155
|
+
try { await rt.registerAgentBasics(); } catch { /* unsupported on this daemon — fine */ }
|
|
156
|
+
}
|
|
157
|
+
for (const schema of toolSchemas) {
|
|
158
|
+
if (state.tools.has(schema.name)) continue;
|
|
159
|
+
await rt.registerTool(schema.name);
|
|
160
|
+
state.tools.add(schema.name);
|
|
161
|
+
}
|
|
162
|
+
for (const policy of policies || []) {
|
|
163
|
+
const fingerprint = JSON.stringify(policy);
|
|
164
|
+
if (state.policies.has(fingerprint)) continue;
|
|
165
|
+
await rt.registerPolicy(...policy);
|
|
166
|
+
state.policies.add(fingerprint);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function closeRuntime(rt) {
|
|
171
|
+
if (typeof rt.close === 'function') await rt.close();
|
|
172
|
+
else if (typeof rt.disconnect === 'function') await rt.disconnect();
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
/** One request-shaped, cache-aware inference path for task and chat loops. */
|
|
142
176
|
async function inferTurn(rt, { model, maxTokens, toolSchemas, messages, toolChoice = 'auto' }) {
|
|
143
177
|
return JSON.parse(await rt.inferTrackedWithRequest(JSON.stringify({
|
|
@@ -155,6 +189,28 @@ async function inferTurn(rt, { model, maxTokens, toolSchemas, messages, toolChoi
|
|
|
155
189
|
})));
|
|
156
190
|
}
|
|
157
191
|
|
|
192
|
+
/** Record one completed inference before any requested action executes. */
|
|
193
|
+
async function recordModelTurn(rt, runId, turnIndex, tracked, fallbackModel) {
|
|
194
|
+
if (runId === null || typeof rt.runsRecordModelTurn !== 'function') return;
|
|
195
|
+
try {
|
|
196
|
+
await rt.runsRecordModelTurn(JSON.stringify({
|
|
197
|
+
run_id: runId,
|
|
198
|
+
turn_index: turnIndex,
|
|
199
|
+
assistant_text: tracked.text || '',
|
|
200
|
+
requested_tool_calls: (tracked.tool_calls || []).map((call) => ({
|
|
201
|
+
id: call.id || '',
|
|
202
|
+
name: call.name || '',
|
|
203
|
+
arguments: call.arguments ?? {},
|
|
204
|
+
})),
|
|
205
|
+
model_id: tracked.model_used || fallbackModel || 'unknown',
|
|
206
|
+
token_usage: tracked.usage ?? null,
|
|
207
|
+
duration_ms: Number.isSafeInteger(tracked.latency_ms) && tracked.latency_ms >= 0
|
|
208
|
+
? tracked.latency_ms
|
|
209
|
+
: 0,
|
|
210
|
+
}));
|
|
211
|
+
} catch { /* older daemon or transient trace failure — inference still counts */ }
|
|
212
|
+
}
|
|
213
|
+
|
|
158
214
|
/**
|
|
159
215
|
* Run the agent once toward `goal`. Returns an AgentOutcome-shaped object:
|
|
160
216
|
* { status, summary, evidence[], metrics{}, tools_called[], run_id }
|
|
@@ -168,8 +224,19 @@ async function inferTurn(rt, { model, maxTokens, toolSchemas, messages, toolChoi
|
|
|
168
224
|
* `rt.runsComplete`. Both are best-effort and never print, so the daemon traces
|
|
169
225
|
* the run for CarHost while older daemons report `run_id: null` and still run.
|
|
170
226
|
*/
|
|
171
|
-
async function runAgent(config, goal,
|
|
172
|
-
const { CarRuntime
|
|
227
|
+
async function runAgent(config, goal, options = {}) {
|
|
228
|
+
const { CarRuntime } = runtimeApi();
|
|
229
|
+
const ownsRuntime = config.runtime == null;
|
|
230
|
+
const rt = config.runtime ?? new CarRuntime();
|
|
231
|
+
try {
|
|
232
|
+
return await runAgentWithRuntime(rt, config, goal, options);
|
|
233
|
+
} finally {
|
|
234
|
+
if (ownsRuntime) await closeRuntime(rt);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function runAgentWithRuntime(rt, config, goal, { maxTurns } = {}) {
|
|
239
|
+
const { executeProposal } = runtimeApi();
|
|
173
240
|
const turnsCap = maxTurns ?? config.maxTurns ?? 8;
|
|
174
241
|
const toolSchemas = [...(config.toolSchemas || []), FINISH_SCHEMA];
|
|
175
242
|
const tools = { finish: ({ answer }) => ({ answer }), ...(config.tools || {}) };
|
|
@@ -180,15 +247,9 @@ async function runAgent(config, goal, { maxTurns } = {}) {
|
|
|
180
247
|
toolSchemas.filter((s) => s && s.timeoutMs != null).map((s) => [s.name, s.timeoutMs]),
|
|
181
248
|
);
|
|
182
249
|
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
// depend on, so a missing method must not abort the run.
|
|
187
|
-
try { await rt.registerAgentBasics(); } catch { /* unsupported on this daemon — fine */ }
|
|
188
|
-
for (const s of toolSchemas) await rt.registerTool(s.name);
|
|
189
|
-
// Guardrails are declarative policies, enforced in Rust BEFORE the tool fires
|
|
190
|
-
// — not prompt rules. Each entry is the argument list for registerPolicy.
|
|
191
|
-
for (const p of (config.policies || [])) await rt.registerPolicy(...p);
|
|
250
|
+
// Tools and declarative policies live on the daemon connection. Register
|
|
251
|
+
// each exact entry once when the caller reuses a runtime across runs.
|
|
252
|
+
await prepareRuntime(rt, toolSchemas, config.policies);
|
|
192
253
|
|
|
193
254
|
// Run-trace bracket (open). Tell the daemon a run is starting so CarHost can
|
|
194
255
|
// trace it: prompt -> CLI outcome -> verifier verdict -> AgentOutcome. The
|
|
@@ -258,16 +319,16 @@ async function runAgent(config, goal, { maxTurns } = {}) {
|
|
|
258
319
|
}
|
|
259
320
|
|
|
260
321
|
const calls = tracked.tool_calls || [];
|
|
322
|
+
// Normalize ids before tracing so the recorded decision carries the same
|
|
323
|
+
// ids proposal execution and subsequent tool_results use.
|
|
324
|
+
calls.forEach((c, i) => { c.id = c.id || `a${i}`; });
|
|
325
|
+
await recordModelTurn(rt, runId, turns, tracked, config.defaultModel);
|
|
261
326
|
if (calls.length === 0) {
|
|
262
327
|
// Model answered in prose with no tool call — treat as a neutral Done.
|
|
263
328
|
outcome = mkOutcome('done', tracked.text || 'no further actions',
|
|
264
329
|
[{ kind: 'self_assessment', description: tracked.text || '', data: null }], metrics, toolsCalled);
|
|
265
330
|
break;
|
|
266
331
|
}
|
|
267
|
-
|
|
268
|
-
// Normalize ids so assistant tool_calls and tool_results correlate across
|
|
269
|
-
// turns (local models often omit ids).
|
|
270
|
-
calls.forEach((c, i) => { c.id = c.id || `a${i}`; });
|
|
271
332
|
messages.push({ role: 'assistant', content: tracked.text || '', tool_calls: calls });
|
|
272
333
|
const idToTool = Object.fromEntries(calls.map((c) => [c.id, c.name]));
|
|
273
334
|
|
|
@@ -432,13 +493,16 @@ async function runChatTurn(
|
|
|
432
493
|
messages,
|
|
433
494
|
});
|
|
434
495
|
const calls = tracked.tool_calls || [];
|
|
496
|
+
calls.forEach((c, i) => { c.id = c.id || `a${i}`; });
|
|
497
|
+
await recordModelTurn(
|
|
498
|
+
rt, runId, turns + 1, tracked, selectedModel ?? config.defaultModel,
|
|
499
|
+
);
|
|
435
500
|
if (calls.length === 0) {
|
|
436
501
|
finalText = tracked.text || '';
|
|
437
502
|
messages.push({ role: 'assistant', content: finalText });
|
|
438
503
|
terminal = 'done';
|
|
439
504
|
break;
|
|
440
505
|
}
|
|
441
|
-
calls.forEach((c, i) => { c.id = c.id || `a${i}`; });
|
|
442
506
|
messages.push({ role: 'assistant', content: tracked.text || '', tool_calls: calls });
|
|
443
507
|
const idToTool = Object.fromEntries(calls.map((c) => [c.id, c.name]));
|
|
444
508
|
// Surface non-finish tool calls as progress so the host UI can show them.
|
|
@@ -495,15 +559,19 @@ async function runChatTurn(
|
|
|
495
559
|
/** Set up the long-lived chat runtime: register tools/policies + the agent.chat handler. */
|
|
496
560
|
async function serveChat(config) {
|
|
497
561
|
const { CarRuntime, registerChatHandler } = runtimeApi();
|
|
498
|
-
const
|
|
562
|
+
const ownsRuntime = config.runtime == null;
|
|
563
|
+
const rt = config.runtime ?? new CarRuntime();
|
|
499
564
|
const toolSchemas = [...(config.toolSchemas || []), FINISH_SCHEMA];
|
|
500
565
|
const tools = { finish: ({ answer }) => ({ answer }), ...(config.tools || {}) };
|
|
501
566
|
const toolTimeouts = Object.fromEntries(
|
|
502
567
|
toolSchemas.filter((s) => s && s.timeoutMs != null).map((s) => [s.name, s.timeoutMs]),
|
|
503
568
|
);
|
|
504
|
-
try {
|
|
505
|
-
|
|
506
|
-
|
|
569
|
+
try {
|
|
570
|
+
await prepareRuntime(rt, toolSchemas, config.policies);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (ownsRuntime) await closeRuntime(rt);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
507
575
|
|
|
508
576
|
if (typeof registerChatHandler !== 'function') {
|
|
509
577
|
console.error(`[${config.agentName}] car-runtime has no agent.chat support — chat disabled (update car-runtime).`);
|
|
@@ -536,6 +604,21 @@ async function serveChat(config) {
|
|
|
536
604
|
return rt;
|
|
537
605
|
}
|
|
538
606
|
|
|
607
|
+
function shutdownLatch() {
|
|
608
|
+
let resolve;
|
|
609
|
+
const promise = new Promise((r) => { resolve = r; });
|
|
610
|
+
const onSignal = () => resolve();
|
|
611
|
+
process.once('SIGINT', onSignal);
|
|
612
|
+
process.once('SIGTERM', onSignal);
|
|
613
|
+
return {
|
|
614
|
+
promise,
|
|
615
|
+
cleanup() {
|
|
616
|
+
process.removeListener('SIGINT', onSignal);
|
|
617
|
+
process.removeListener('SIGTERM', onSignal);
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
539
622
|
async function main(config) {
|
|
540
623
|
const args = parseArgs(process.argv.slice(2));
|
|
541
624
|
|
|
@@ -544,24 +627,39 @@ async function main(config) {
|
|
|
544
627
|
// "running". Always serve chat (agent.chat); if the agent has a standing
|
|
545
628
|
// goal + interval, ALSO run it on a loop.
|
|
546
629
|
// car_register.py can set these via CAR_STANDING_GOAL / CAR_INTERVAL_SECS.
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
630
|
+
let chatRuntime = null;
|
|
631
|
+
const ownsRuntime = config.runtime == null;
|
|
632
|
+
const shutdown = shutdownLatch();
|
|
633
|
+
try {
|
|
634
|
+
try { chatRuntime = await serveChat(config); } catch (e) { console.error(`[${config.agentName}] chat setup failed:`, e); }
|
|
635
|
+
const goal = config.standingGoal ?? process.env.CAR_STANDING_GOAL ?? null;
|
|
636
|
+
const everyMs = (config.intervalSecs ?? Number(process.env.CAR_INTERVAL_SECS || 0)) * 1000;
|
|
637
|
+
console.error(`[${config.agentName}] serving${goal ? ` — "${goal}" every ${everyMs / 1000}s` : ' (idle; start via dashboard or --task)'}`);
|
|
638
|
+
if (goal && everyMs > 0) {
|
|
639
|
+
for (;;) {
|
|
640
|
+
try {
|
|
641
|
+
// Each iteration is its own run-trace bracket on the same long-lived
|
|
642
|
+
// chat runtime, so standing service does not grow daemon sockets.
|
|
643
|
+
const runConfig = chatRuntime ? { ...config, runtime: chatRuntime } : config;
|
|
644
|
+
const o = await runAgent(runConfig, goal);
|
|
645
|
+
console.error(`[${config.agentName}] ${o.status} — ${o.summary}`);
|
|
646
|
+
} catch (e) { console.error(`[${config.agentName}] error:`, e); }
|
|
647
|
+
const delay = new Promise((resolve) => {
|
|
648
|
+
const timer = setTimeout(() => resolve(false), everyMs);
|
|
649
|
+
timer.unref?.();
|
|
650
|
+
});
|
|
651
|
+
const stopped = await Promise.race([
|
|
652
|
+
shutdown.promise.then(() => true),
|
|
653
|
+
delay,
|
|
654
|
+
]);
|
|
655
|
+
if (stopped) return;
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
await shutdown.promise;
|
|
560
659
|
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
setInterval(() => {}, 1 << 30);
|
|
660
|
+
} finally {
|
|
661
|
+
shutdown.cleanup();
|
|
662
|
+
if (ownsRuntime && chatRuntime) await closeRuntime(chatRuntime);
|
|
565
663
|
}
|
|
566
664
|
return;
|
|
567
665
|
}
|
|
@@ -589,4 +687,4 @@ async function main(config) {
|
|
|
589
687
|
process.exit(outcome.status === 'failure' ? 1 : 0);
|
|
590
688
|
}
|
|
591
689
|
|
|
592
|
-
module.exports = { main, runAgent, runChatTurn };
|
|
690
|
+
module.exports = { main, runAgent, runChatTurn, serveChat };
|
package/agent-loop.mjs
CHANGED