@deksden-com/dd-flow-cli 0.9.0-beta.64 → 0.9.0-beta.74
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/dist/build-info.json +3 -3
- package/dist/cli/help.js +2 -0
- package/dist/cli/run-cli.js +15 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +4 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +4 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +4 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +4 -2
- package/dist/harness-runtime/bin/dd-opencode.mjs +4 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/daemon-operations.mjs +108 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +5 -4
- package/dist/harness-runtime/lib/dd-codex.mjs +38 -4
- package/dist/harness-runtime/lib/dd-droid.mjs +55 -20
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/dd-grok.mjs +14 -8
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +43 -8
- package/dist/harness-runtime/lib/dd-opencode.mjs +5 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +182 -16
- package/dist/harness-runtime/lib/dd-zcode.mjs +162 -63
- package/dist/harness-runtime/lib/driver-recovery.mjs +2 -2
- package/dist/harness-runtime/lib/managed-daemon.mjs +27 -5
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/services/code-checks.js +120 -19
- package/dist/services/controller-fanout.js +11 -0
- package/dist/services/engines.js +40 -9
- package/dist/services/eval-snapshots.js +30 -47
- package/dist/services/harness-adapter.js +12 -6
- package/dist/services/lifecycle-invocations.js +12 -8
- package/dist/services/native-session-control.js +6 -0
- package/dist/services/projects.js +6 -2
- package/dist/services/recovery-observation-budget.js +9 -1
- package/dist/services/run-control-worker.js +19 -0
- package/dist/services/run-control.js +87 -13
- package/dist/services/run-controller-adapter.js +134 -7
- package/dist/services/run-controller.js +11 -3
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/runs.js +1 -11
- package/dist/services/schema-validation.js +1 -1
- package/dist/services/stage-lifecycle.js +14 -2
- package/dist/services/usage.js +20 -87
- package/dist/services/vnext-code-review.js +35 -27
- package/dist/services/vnext-code.js +62 -26
- package/dist/services/vnext-merge.js +8 -8
- package/dist/services/vnext-plan-review.js +16 -16
- package/dist/services/vnext-plan.js +51 -11
- package/dist/services/work-registry.js +48 -27
- package/dist/shared/errors.js +12 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { processSnapshot } from "./process-snapshot.mjs";
|
|
2
|
+
import { ToolObservations, droidToolObservations, journalToolObservations } from "./tool-observations.mjs";
|
|
2
3
|
export { processSnapshot } from "./process-snapshot.mjs";
|
|
3
4
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -19,6 +20,7 @@ import { confirmDaemonProcess, finishDaemonProcess, heartbeatDaemonProcess, regi
|
|
|
19
20
|
const run = promisify(execFile);
|
|
20
21
|
export const DROID_CONTRACT = "dd-droid-harness@1";
|
|
21
22
|
export const DROID_PROTOCOL = "1.201.0";
|
|
23
|
+
const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
22
24
|
export class DroidError extends Error {
|
|
23
25
|
constructor(code, message, details) { super(message); this.code = code; if (details) this.details = details; }
|
|
24
26
|
}
|
|
@@ -26,6 +28,8 @@ export const absolute = (value, label) => { if (!value || !path.isAbsolute(value
|
|
|
26
28
|
export const executable = (bin, args = []) => /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args };
|
|
27
29
|
const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
28
30
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
const responseSummary = event => ({ request_id: event?.id ?? null, has_result: event?.result !== undefined,
|
|
32
|
+
result_keys: event?.result && typeof event.result === "object" && !Array.isArray(event.result) ? Object.keys(event.result).slice(0, 32) : [], error: event?.error ?? null });
|
|
29
33
|
export async function readJson(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
30
34
|
export function droidPaths(stateDir) {
|
|
31
35
|
const dir = absolute(stateDir, "state directory"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 24);
|
|
@@ -71,13 +75,10 @@ export function parseDroidTranscript(text, settings, expectedId) {
|
|
|
71
75
|
for (const line of text.split("\n")) { if (!line.trim()) continue; try { records.push(JSON.parse(line)); } catch { complete = false; } }
|
|
72
76
|
const header = records.find(record => record.type === "session_start");
|
|
73
77
|
if (!header || header.id !== expectedId) throw new DroidError("session_identity_mismatch", "Droid transcript has a different or missing native Session ID", { expected_session_id: expectedId, observed_session_id: header?.id ?? null });
|
|
74
|
-
const
|
|
75
|
-
for (const record of records) for (const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
const byTool = {}; for (const name of calls.values()) byTool[name] = (byTool[name] ?? 0) + 1;
|
|
80
|
-
return { header, records, outcomes: records.filter(record => record.type === "agent_turn_outcome"), usage: physicalUsage(settings?.tokenUsage), settings, complete, tool_calls: { total: calls.size, failures: [...failures].filter(id => calls.has(id)).length, by_tool: byTool } };
|
|
78
|
+
const tools = new ToolObservations();
|
|
79
|
+
for (const record of records) for (const event of droidToolObservations(record, expectedId)) tools.add(event);
|
|
80
|
+
if (!complete) tools.gaps.add("transcript_incomplete");
|
|
81
|
+
return { header, records, outcomes: records.filter(record => record.type === "agent_turn_outcome"), usage: physicalUsage(settings?.tokenUsage), settings, complete, tool_calls: tools.summary() };
|
|
81
82
|
}
|
|
82
83
|
export async function readDroidSession(factory, id) {
|
|
83
84
|
const file = await metadataReader(factory).locate(id);
|
|
@@ -95,7 +96,7 @@ function descendants(rows, rootPid) { const ids = new Set([rootPid]); for (;;) {
|
|
|
95
96
|
|
|
96
97
|
export class DroidRuntime {
|
|
97
98
|
constructor(config, state, persist) {
|
|
98
|
-
this.config = config; this.state = state; this.paths = droidPaths(config.stateDir); this.persist = persist; this.pending = new Map(); this.active = null; this.child = null; this.closed = true; this.loaded = false; this.ownedProcesses = state.owned_processes ?? []; this.closing = false; this.workingState = "idle"; this.protocol = null; this.rootId = state.root_session_id ?? null; this.topology = new Map(state.descendants?.map(child => [child.provider_session_id, child]) ?? []); this.draining = Promise.resolve(); this.journalling = Promise.resolve(); this.activity = 0; this.providerProcess = null; this.cancelled = false; this.lastResult = state.last_result ?? null; this.metadata = metadataReader(this.paths.factory); this.fatalError = state.fatal_error ? reportedError(state.fatal_error) : null;
|
|
99
|
+
this.config = config; this.state = state; this.paths = droidPaths(config.stateDir); this.persist = persist; this.pending = new Map(); this.lateResponses = new Map(); this.active = null; this.child = null; this.closed = true; this.loaded = false; this.ownedProcesses = state.owned_processes ?? []; this.closing = false; this.workingState = "idle"; this.protocol = null; this.rootId = state.root_session_id ?? null; this.topology = new Map(state.descendants?.map(child => [child.provider_session_id, child]) ?? []); this.draining = Promise.resolve(); this.journalling = Promise.resolve(); this.activity = 0; this.providerProcess = null; this.cancelled = false; this.lastResult = state.last_result ?? null; this.metadata = metadataReader(this.paths.factory); this.fatalError = state.fatal_error ? reportedError(state.fatal_error) : null;
|
|
99
100
|
}
|
|
100
101
|
async journal(kind, payload) { const line = JSON.stringify({ schema_id: "dd-droid/event@1", observed_at: new Date().toISOString(), kind, ...payload }); this.journalling = this.journalling.then(() => appendFile(this.config.journal, `${line}\n`, { mode: 0o600 })); return await this.journalling; }
|
|
101
102
|
async save(patch = {}) { return await this.persist({ root_session_id: this.rootId, descendants: [...this.topology.values()], working_state: this.workingState, last_activity: this.activity, ...patch }); }
|
|
@@ -129,13 +130,14 @@ export class DroidRuntime {
|
|
|
129
130
|
await this.journal("native", { event });
|
|
130
131
|
if (event.type === "response") {
|
|
131
132
|
this.protocol ??= event.factoryProtocolVersion;
|
|
132
|
-
const pending = this.pending.get(event.id); if (!pending) return;
|
|
133
|
+
const pending = this.pending.get(event.id); if (!pending) { this.lateResponses.set(event.id, event); while (this.lateResponses.size > 128) this.lateResponses.delete(this.lateResponses.keys().next().value); await this.journal("late_response", responseSummary(event)); return; }
|
|
133
134
|
this.pending.delete(event.id); clearTimeout(pending.timer);
|
|
135
|
+
if (pending.timedOut) await this.journal("late_response", { ...responseSummary(event), method: pending.method, grace_ms: LATE_RESPONSE_GRACE_MS });
|
|
134
136
|
event.error ? pending.reject(new DroidError("droid_rpc_error", event.error.message ?? "Droid rejected request", event.error)) : pending.resolve(event.result ?? {}); return;
|
|
135
137
|
}
|
|
136
138
|
if (event.method === "droid.request_permission" || event.method === "droid.ask_user") {
|
|
137
139
|
const result = event.method === "droid.request_permission" ? { selectedOption: "cancel" } : { cancelled: true, answers: [] };
|
|
138
|
-
this.
|
|
140
|
+
this.send({ jsonrpc: "2.0", type: "response", factoryApiVersion: "1.0.0", factoryProtocolVersion: DROID_PROTOCOL, id: event.id, result });
|
|
139
141
|
await this.journal("unexpected_input_rejected", { method: event.method }); return;
|
|
140
142
|
}
|
|
141
143
|
if (event.method !== "droid.session_notification") return;
|
|
@@ -162,11 +164,28 @@ export class DroidRuntime {
|
|
|
162
164
|
request(method, params, id = randomUUID(), timeoutMs = 30_000) {
|
|
163
165
|
if (!this.child || this.closed) return Promise.reject(new DroidError("operation_observation_lost", "Droid runtime is not connected"));
|
|
164
166
|
return new Promise((resolve, reject) => {
|
|
165
|
-
const timer = observedTimeout(() => {
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
const timer = observedTimeout(() => {
|
|
168
|
+
const pending = this.pending.get(id); if (!pending || pending.timedOut) return;
|
|
169
|
+
pending.timedOut = true;
|
|
170
|
+
const timeoutError = new DroidError("rpc_timeout", `${method} has no observed reply`, { request_id: id, timeout_ms: timeoutMs, grace_ms: LATE_RESPONSE_GRACE_MS });
|
|
171
|
+
void this.journal("request_deadline", { method, request_id: id, timeout_ms: timeoutMs, grace_ms: LATE_RESPONSE_GRACE_MS });
|
|
172
|
+
pending.timer = observedTimeout(() => { if (this.pending.get(id) !== pending) return; this.pending.delete(id); reject(timeoutError); }, LATE_RESPONSE_GRACE_MS);
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
this.pending.set(id, { resolve, reject, timer, method });
|
|
175
|
+
try { this.send({ jsonrpc: "2.0", type: "request", factoryApiVersion: "1.0.0", factoryProtocolVersion: DROID_PROTOCOL, id, method, params }, error => { if (error) { clearTimeout(timer); this.pending.delete(id); reject(error); } }); }
|
|
176
|
+
catch (error) { clearTimeout(timer); this.pending.delete(id); reject(error); }
|
|
168
177
|
});
|
|
169
178
|
}
|
|
179
|
+
send(value, callback) {
|
|
180
|
+
const stdin = this.child?.stdin;
|
|
181
|
+
if (!stdin || stdin.destroyed) {
|
|
182
|
+
const error = new DroidError("bridge_pipe_broken", "Droid runtime stdin is closed");
|
|
183
|
+
this.rejectPending(error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
try { stdin.write(`${JSON.stringify(value)}\n`, callback); }
|
|
187
|
+
catch (error) { this.rejectPending(error); throw error; }
|
|
188
|
+
}
|
|
170
189
|
profile(settings) {
|
|
171
190
|
const model = settings?.modelId ?? settings?.model ?? null;
|
|
172
191
|
const nativeModel = this.models?.find(item => (item.id ?? item.modelId) === model);
|
|
@@ -279,8 +298,15 @@ export class DroidRuntime {
|
|
|
279
298
|
assertDispatch();
|
|
280
299
|
await this.request("droid.add_user_message", { text: prompt, messageId: turnId }, requestId);
|
|
281
300
|
const outcome = await completed; await this.draining;
|
|
282
|
-
|
|
283
|
-
|
|
301
|
+
try {
|
|
302
|
+
const receipt = await this.inspect(id, { ignoreActive: true });
|
|
303
|
+
return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
|
|
304
|
+
} catch (error) {
|
|
305
|
+
// A terminal native outcome is durable independently of a later
|
|
306
|
+
// filesystem/topology observation. Preserve it and let settlement
|
|
307
|
+
// reconcile; do not convert it into a failed prompt.
|
|
308
|
+
return { provider_session_id: id, adapter_session_id: id, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId, settled: false, settlement_observation_error: errorRecord(error) };
|
|
309
|
+
}
|
|
284
310
|
} catch (error) {
|
|
285
311
|
await this.journal("prompt_error", { operation_id: operationId, error: errorRecord(error) }); throw error;
|
|
286
312
|
} finally { clearInterval(timer); if (this.active === current) this.active = null; await this.save({ active_tree: this.workingState !== "idle" }); }
|
|
@@ -292,16 +318,23 @@ export class DroidRuntime {
|
|
|
292
318
|
const native = await readDroidSession(this.paths.factory, binding.provider_session_id);
|
|
293
319
|
const outcome = native?.outcomes.find(item => item.turnId === binding.turn_id);
|
|
294
320
|
if (!outcome || !terminal(outcome.reason)) return null;
|
|
295
|
-
|
|
296
|
-
|
|
321
|
+
let receipt;
|
|
322
|
+
try { receipt = await this.inspect(binding.provider_session_id, { ignoreActive: true }); }
|
|
323
|
+
catch (error) {
|
|
324
|
+
receipt = { provider_session_id: binding.provider_session_id, adapter_session_id: binding.provider_session_id, settled: false, settlement_observation_error: errorRecord(error) };
|
|
325
|
+
}
|
|
326
|
+
// A terminal root turn is not enough to recover the operation while a
|
|
327
|
+
// child Task or owned provider process is still active. Leave the
|
|
328
|
+
// durable binding for the normal settlement observer instead of exposing
|
|
329
|
+
// a misleading successful receipt.
|
|
330
|
+
if (receipt.settled !== true) return null;
|
|
297
331
|
const current = this.active;
|
|
298
332
|
if (current?.operationId === operationId && current.turnId === binding.turn_id) {
|
|
299
333
|
current.outcome = outcome; this.lastResult = outcome;
|
|
300
334
|
await this.saveBinding(current, { native_outcome: outcome });
|
|
301
335
|
current.resolve(outcome);
|
|
302
|
-
// Let the original dispatch persist its one terminal reply
|
|
303
|
-
//
|
|
304
|
-
// with a second writer or expose success while prompt still owns busy.
|
|
336
|
+
// Let the original dispatch persist its one terminal reply. Settlement
|
|
337
|
+
// is reconciled separately; do not race a second writer or replay it.
|
|
305
338
|
return null;
|
|
306
339
|
}
|
|
307
340
|
return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: binding.assistant_text ?? "", turn_id: binding.turn_id, recovered_from: "native.agent_turn_outcome" };
|
|
@@ -343,6 +376,8 @@ export class DroidRuntime {
|
|
|
343
376
|
}
|
|
344
377
|
async snapshotUsage(id) {
|
|
345
378
|
const native = await readDroidSession(this.paths.factory, id); if (!native) return { status: "unavailable", provider_session_id: id };
|
|
379
|
+
this.journaledTools ??= new Map();
|
|
380
|
+
journalToolObservations(this.config.journal, [...native.records.flatMap(record => droidToolObservations(record, id)), ...native.tool_calls.reasons.map(reason => ({ reason }))], this.journaledTools);
|
|
346
381
|
const measured = native.usage;
|
|
347
382
|
const payload = { provider_session_id: id, daemon_id: this.config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: native.complete && measured ? "complete" : "partial", usage: measured, tool_calls: native.tool_calls, source: { path: native.transcript_path, sha256: native.source_sha256 } };
|
|
348
383
|
if (measured && this.config.ddFlowBin) await this.flow("usage", "ingest", payload);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
2
|
+
import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
|
|
2
3
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
@@ -198,7 +199,7 @@ export class Runtime {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
topology(root) { return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
|
|
201
|
-
observeToolCall(message) {
|
|
202
|
+
observeToolCall(message) { this.toolObservations ??= new ToolObservations(); this.toolObservations.add(acpToolObservation(message, { harness: "grok-acp" })); const sessionId = message?.params?.sessionId; if (sessionId) this.toolUsage.set(sessionId, this.toolObservations.summary(sessionId)); }
|
|
202
203
|
running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
|
|
203
204
|
treeRunning(result) {
|
|
204
205
|
const evidence = result?.evidence ?? result;
|
|
@@ -232,7 +233,7 @@ export class Runtime {
|
|
|
232
233
|
return running;
|
|
233
234
|
}
|
|
234
235
|
async requireSettled() { const running = await this.refreshTree(); if (running.length) throw new DaemonError("tree_not_settled", "daemon still owns a running Session tree", false, { sessions: running }); }
|
|
235
|
-
async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree:
|
|
236
|
+
async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const running = this.treeRunning(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree: running, active_operation: null }); return { ...result, descendants: this.topology(result.provider_session_id), settled: !running }; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; } finally { this.active = null; } }
|
|
236
237
|
async dispatch(operation, params) {
|
|
237
238
|
const assertDispatch = captureDispatchGuard(this);
|
|
238
239
|
if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { observation_error: observationError ?? null, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config }; }
|
|
@@ -29,13 +29,13 @@ function unwrap(value) { return value?.result ?? value ?? {}; }
|
|
|
29
29
|
function extension(method) { return method.startsWith("_") ? method : `_${method}`; }
|
|
30
30
|
function usageScope(options, providerSessionId) { return providerSessionId === options.rootProviderSessionId ? "execution_tree_inclusive" : "physical_session"; }
|
|
31
31
|
function emptyToolCalls() { return { total: 0, failures: 0, by_tool: {} }; }
|
|
32
|
-
function sessionToolCalls(options, bridge, sessionId) { return options.toolUsage ? (options.toolUsage.get(sessionId) ?? emptyToolCalls()) : bridge.toolSummary(); }
|
|
32
|
+
function sessionToolCalls(options, bridge, sessionId) { return options.toolUsage ? (options.toolUsage.get(sessionId) ?? emptyToolCalls()) : bridge.toolSummary(sessionId); }
|
|
33
33
|
function recordToolCalls(options, bridge, sessionId, cursor) {
|
|
34
34
|
const result = options.toolUsage
|
|
35
35
|
? sessionToolCalls(options, bridge, sessionId)
|
|
36
36
|
: typeof bridge.toolSummarySince === "function"
|
|
37
|
-
? bridge.toolSummarySince(cursor)
|
|
38
|
-
: bridge.toolSummary();
|
|
37
|
+
? bridge.toolSummarySince(cursor, sessionId)
|
|
38
|
+
: bridge.toolSummary(sessionId);
|
|
39
39
|
options.toolUsage?.set(sessionId, result);
|
|
40
40
|
return result;
|
|
41
41
|
}
|
|
@@ -45,6 +45,7 @@ function bridgeOptions(options) {
|
|
|
45
45
|
const grokHome = options.grokHome ? absolute(options.grokHome, "--grok-home") : null;
|
|
46
46
|
return {
|
|
47
47
|
...options,
|
|
48
|
+
observationHarness: "grok-acp",
|
|
48
49
|
// AcpBridge defaults to zcode-acp; Grok must always name its own executable.
|
|
49
50
|
bin: options.bin ?? process.env.DD_GROK_BIN ?? "grok",
|
|
50
51
|
commandArgs: ["agent", "--no-leader", "--always-approve", "--model", requested.model ?? "grok-4.6", "--reasoning-effort", requested.reasoning ?? "high", "stdio"],
|
|
@@ -91,6 +92,11 @@ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
|
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
async function forwardUsageBestEffort(options, providerSessionId, usage, toolCalls) {
|
|
96
|
+
try { await forwardUsage(options, providerSessionId, usage, toolCalls); return null; }
|
|
97
|
+
catch (error) { return { code: error.code ?? "usage_ingest_failed", message: error.message ?? String(error) }; }
|
|
98
|
+
}
|
|
99
|
+
|
|
94
100
|
async function identity(bridge, options) {
|
|
95
101
|
const sessionId = text(options.sessionId, "--session-id");
|
|
96
102
|
if (!options.liveSession) await bridge.request("session/load", { sessionId, cwd: absolute(options.cwd, "--cwd"), mcpServers: [] });
|
|
@@ -136,24 +142,24 @@ export async function createSessionWithBridge(bridge, options, initialized) {
|
|
|
136
142
|
await observeModel({ journal: options.journal, harness: "grok-acp", sessionId: providerSessionId, requested, observed: observedProfile(initialized, requested), source: "acp.initialize.modelState" });
|
|
137
143
|
const profileReceipt = assertProfile(requested, observedProfile(initialized, requested));
|
|
138
144
|
await options.onSessionCreated?.({ provider_session_id: providerSessionId, adapter_session_id: providerSessionId, cwd });
|
|
139
|
-
const local = { ...options, initialized, rootProviderSessionId: providerSessionId }; await
|
|
140
|
-
await bridge.flush(); const evidence = await inspect(bridge, providerSessionId, local); evidence.tool_calls = sessionToolCalls(local, bridge, providerSessionId); await
|
|
145
|
+
const local = { ...options, initialized, rootProviderSessionId: providerSessionId }; const initial_usage_error = await forwardUsageBestEffort(local, providerSessionId, await sessionUsage(bridge, providerSessionId), sessionToolCalls(local, bridge, providerSessionId));
|
|
146
|
+
await bridge.flush(); const evidence = await inspect(bridge, providerSessionId, local); evidence.tool_calls = sessionToolCalls(local, bridge, providerSessionId); const usage_ingest_error = await forwardUsageBestEffort(local, providerSessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
|
|
141
147
|
return receipt(options, { harness: "grok-acp", runtime_family: "grok", provider_session_id: providerSessionId, adapter_session_id: providerSessionId, cwd, initialized, profile: profileReceipt, evidence });
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
export async function promptSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await promptSessionWithBridge(bridge, { ...options, initialized })); }
|
|
145
151
|
export async function promptSessionWithBridge(bridge, options) {
|
|
146
152
|
requireJournal(options); const sessionId = await identity(bridge, options); const prompt = text(options.prompt, "prompt"); const requested = requiredProfile(options); const profileReceipt = assertProfile(requested, observedProfile(options.initialized, requested));
|
|
147
|
-
const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; await
|
|
153
|
+
const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; const initial_usage_error = await forwardUsageBestEffort(local, sessionId, await sessionUsage(bridge, sessionId), sessionToolCalls(local, bridge, sessionId));
|
|
148
154
|
options.assertDispatch?.();
|
|
149
155
|
const cursor = bridge.toolCursor(); const textCursor = bridge.assistantTextCursor(sessionId); const turn = await bridge.request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, null, { idleSessionId: sessionId, idleTimeoutMs: options.timeoutMs ?? 600_000 }); await bridge.flush();
|
|
150
|
-
const evidence = await inspect(bridge, sessionId, local); evidence.tool_calls = recordToolCalls(local, bridge, sessionId, cursor); await
|
|
156
|
+
const evidence = await inspect(bridge, sessionId, local); evidence.tool_calls = recordToolCalls(local, bridge, sessionId, cursor); const usage_ingest_error = await forwardUsageBestEffort(local, sessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
|
|
151
157
|
if (!options.allowBackground && (evidence.subagents?.subagents ?? []).length) { await cancelTree(bridge, sessionId); fail("background Grok Build subagents cannot outlive the one-shot dd-grok connection"); }
|
|
152
158
|
return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, assistant_text: bridge.assistantTextSince(sessionId, textCursor), turn, profile: profileReceipt, evidence });
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
export async function inspectSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await inspectSessionWithBridge(bridge, { ...options, initialized })); }
|
|
156
|
-
export async function inspectSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); const evidence = await inspect(bridge, sessionId, options); evidence.tool_calls = sessionToolCalls(options, bridge, sessionId); await
|
|
162
|
+
export async function inspectSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); const evidence = await inspect(bridge, sessionId, options); evidence.tool_calls = sessionToolCalls(options, bridge, sessionId); const usage_ingest_error = await forwardUsageBestEffort(options, sessionId, evidence.usage, evidence.tool_calls); if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error; return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...evidence }); }
|
|
157
163
|
export async function cancelSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await cancelSessionWithBridge(bridge, options)); }
|
|
158
164
|
export async function cancelSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...await cancelTree(bridge, sessionId) }); }
|
|
159
165
|
export async function forkSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await forkSessionWithBridge(bridge, options)); }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
2
|
+
import { openCodeToolObservation, journalToolObservations } from "./tool-observations.mjs";
|
|
2
3
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
3
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
5
|
import { appendFile, chmod, copyFile, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
@@ -68,7 +69,7 @@ export class Runtime {
|
|
|
68
69
|
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(async () => { await writeJson(this.paths.state, snapshot); await heartbeatDaemonProcess(snapshot.config, snapshot.resource_process).catch(() => {}); await heartbeatDaemonProcess(snapshot.config, this.providerProcess).catch(() => {}); }); return this.persisting; }
|
|
69
70
|
async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
70
71
|
async childTree(id, seen = new Set()) { if (seen.has(id)) return []; seen.add(id); const direct = await this.client.children(id) ?? []; const nested = []; for (const child of direct) nested.push(child, ...await this.childTree(child.id, seen)); return nested; }
|
|
71
|
-
async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, children, usage: usageSnapshot(messages), history_sha256: historyDigest(messages), message_count: messages.length }; }
|
|
72
|
+
async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); this.journaledTools ??= new Map(); journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, children, usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length }; }
|
|
72
73
|
async observeModels(id, messages, parent = null) {
|
|
73
74
|
this.modelMessages ??= new Set();
|
|
74
75
|
for (const message of messages ?? []) {
|
|
@@ -82,7 +83,7 @@ export class Runtime {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
async requireSettled(id) { const value = await this.describe(id); if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status }); return value; }
|
|
85
|
-
async reconcileUsage(observed) { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) await this.reconcileUsage(await this.describe(child.id)); }
|
|
86
|
+
async reconcileUsage(observed) { const errors = []; try { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); } catch (error) { errors.push({ session_id: observed.provider_session_id, phase: "usage", ...errorPayload(error) }); } for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) { try { errors.push(...await this.reconcileUsage(await this.describe(child.id))); } catch (error) { errors.push({ session_id: child.id, phase: "usage_child", ...errorPayload(error) }); } } return errors; }
|
|
86
87
|
async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); await this.persist({ active_tree: !result?.settled, active_operation: null }); await this.journal(name, result); return result; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); await this.journal(`${name}.failed`, null); throw error; } finally { this.active = null; } }
|
|
87
88
|
async cancelTree(id, seen = new Set()) {
|
|
88
89
|
if (seen.has(id)) return [];
|
|
@@ -102,18 +103,52 @@ export class Runtime {
|
|
|
102
103
|
const assertDispatch = captureDispatchGuard(this);
|
|
103
104
|
if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
|
|
104
105
|
if (operation === "daemon.doctor") { const doc = await this.client.request("GET", "/doc", undefined, { directory: null, timeoutMs: 5_000 }); const apiFingerprint = createHash("sha256").update(typeof doc === "string" ? doc : JSON.stringify(doc)).digest("hex"); if (apiFingerprint !== OPENCODE_BASELINE.api_fingerprint) throw new DaemonError("api_fingerprint_mismatch", "OpenCode API differs from the qualified baseline", false, { expected: OPENCODE_BASELINE.api_fingerprint, actual: apiFingerprint }); return { compatible: true, versions: this.state.versions, api_fingerprint: apiFingerprint, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status }; }
|
|
105
|
-
if (operation === "session.create") return this.productive(operation, async () => {
|
|
106
|
+
if (operation === "session.create") return this.productive(operation, async () => {
|
|
107
|
+
const info = await this.client.create(params.title);
|
|
108
|
+
const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? null, root_provider_session_id: info.id, cwd: info.directory };
|
|
109
|
+
this.sessions.set(info.id, created);
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.describe(info.id);
|
|
112
|
+
result.usage_ingest_errors = await this.reconcileUsage(result);
|
|
113
|
+
return result;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// The Session exists even if its optional post-create observation is
|
|
116
|
+
// temporarily unavailable. Preserve the identity; a later inspect
|
|
117
|
+
// owns any profile/topology proof required before a prompt.
|
|
118
|
+
return { ...created, settled: false, settlement_observation_error: errorPayload(error) };
|
|
119
|
+
}
|
|
120
|
+
});
|
|
106
121
|
if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
|
|
107
122
|
if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => { assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, provider: this.state.config.provider, model: this.state.config.model, variant: this.state.config.variant, agent: this.state.config.agent,
|
|
108
123
|
assertDispatch,
|
|
109
124
|
onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
|
|
110
125
|
onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
|
|
111
|
-
}); const
|
|
112
|
-
|
|
126
|
+
}); const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
|
|
127
|
+
const reply_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: this.state.config.agent ?? null };
|
|
128
|
+
checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, reply_profile);
|
|
129
|
+
let result;
|
|
130
|
+
try { result = await this.describe(params.sessionId); }
|
|
131
|
+
catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
|
|
132
|
+
result.assistant_text = assistant_text;
|
|
113
133
|
result.response_message_id = reply.info.id;
|
|
114
|
-
result.observed_profile = {
|
|
115
|
-
checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, result.observed_profile); await this.reconcileUsage(result); return result; }); }
|
|
116
|
-
if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
134
|
+
result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
|
|
135
|
+
checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
|
|
136
|
+
if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
137
|
+
const info = await this.client.fork(params.sessionId, params.messageId);
|
|
138
|
+
const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? params.sessionId, root_provider_session_id: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
|
|
139
|
+
this.sessions.set(info.id, created);
|
|
140
|
+
try {
|
|
141
|
+
const fork = await this.describe(info.id);
|
|
142
|
+
const sourceMessages = await this.client.messages(params.sessionId), forkMessages = await this.client.messages(info.id);
|
|
143
|
+
const prefix = JSON.stringify(canonicalHistory(forkMessages)) === JSON.stringify(canonicalHistory(sourceMessages).slice(0, forkMessages.length));
|
|
144
|
+
if (!prefix) throw new DaemonError("fork_continuity_failed", "fork history is not a source prefix");
|
|
145
|
+
return { ...fork, fork_receipt: { source_provider_session_id: params.sessionId, source_history_sha256: source.history_sha256, fork_history_sha256: fork.history_sha256, history_prefix_proven: true } };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// A native fork is materialized already. Do not lose its identity or
|
|
148
|
+
// retry it, but never let an unproven history prefix become usable.
|
|
149
|
+
return { ...created, settled: false, fork_receipt: { source_provider_session_id: params.sessionId, source_history_sha256: source.history_sha256, history_prefix_proven: false }, fork_continuity_error: errorPayload(error) };
|
|
150
|
+
}
|
|
151
|
+
}); }
|
|
117
152
|
if (operation === "session.cancel") {
|
|
118
153
|
if (!this.sessions.has(params.sessionId)) throw new DaemonError("foreign_session", "cannot cancel a Session without daemon ownership");
|
|
119
154
|
cancelPendingDispatch(this);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { ToolObservations, openCodeToolObservation } from "./tool-observations.mjs";
|
|
2
3
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
3
4
|
|
|
4
5
|
export class OpenCodeError extends Error {
|
|
@@ -89,9 +90,9 @@ export function canonicalHistory(messages) {
|
|
|
89
90
|
return messages.map(({ info, parts }) => ({ role: info.role, ...(info.role === "assistant" ? { provider: info.providerID, model: info.modelID, mode: info.mode, finish: info.finish ?? null, error: info.error?.name ?? null } : {}), parts: parts.map(stablePart) }));
|
|
90
91
|
}
|
|
91
92
|
export function historyDigest(messages) { return createHash("sha256").update(JSON.stringify(canonicalHistory(messages))).digest("hex"); }
|
|
92
|
-
export function usageSnapshot(messages) {
|
|
93
|
+
export function usageSnapshot(messages, sessionId) {
|
|
93
94
|
const assistants = messages.filter((item) => item.info?.role === "assistant"); const tokens = assistants.reduce((sum, item) => { const value = item.info.tokens ?? {}; const cache = value.cache ?? {}; sum.input += value.input ?? 0; sum.output += value.output ?? 0; sum.reasoning += value.reasoning ?? 0; sum.cache_read += cache.read ?? 0; sum.cache_write += cache.write ?? 0; return sum; }, { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 });
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
return { input_tokens: tokens.input, output_tokens: tokens.output, reasoning_tokens: tokens.reasoning, cached_input_tokens: tokens.cache_read, cache_write_tokens: tokens.cache_write, total_tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache_read + tokens.cache_write, tool_calls:
|
|
95
|
+
const tools = new ToolObservations();
|
|
96
|
+
for (const item of messages) for (const part of item.parts ?? []) tools.add(openCodeToolObservation(part, item.info?.sessionID ?? sessionId, item.info?.id));
|
|
97
|
+
return { input_tokens: tokens.input, output_tokens: tokens.output, reasoning_tokens: tokens.reasoning, cached_input_tokens: tokens.cache_read, cache_write_tokens: tokens.cache_write, total_tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache_read + tokens.cache_write, tool_calls: tools.summary(), cost_usd: assistants.reduce((sum, item) => sum + (item.info.cost ?? 0), 0) };
|
|
97
98
|
}
|