@relayflows/sdk 2.0.36 → 2.0.37
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/authored-flow-executor.d.ts +2 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +2 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +3 -2
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +2 -0
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.js +6 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +2 -1
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +2 -0
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-preflight.d.ts +1 -1
- package/dist/authored-preflight.d.ts.map +1 -1
- package/dist/authored-preflight.js +2 -2
- package/dist/authored-preflight.js.map +1 -1
- package/dist/authored-root.d.ts +2 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +5 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-verdict.d.ts +2 -0
- package/dist/authored-verdict.d.ts.map +1 -1
- package/dist/authored-verdict.js +2 -1
- package/dist/authored-verdict.js.map +1 -1
- package/dist/authored-worker-step.d.ts +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +2 -2
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +3 -3
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +2 -2
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +8 -7
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +7 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +15 -9
- package/dist/cli/run.js.map +1 -1
- package/dist/communication/local.d.ts +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -2
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +4 -4
- package/dist/communication/worker.js.map +1 -1
- package/dist/decimal-dollars.d.ts +12 -0
- package/dist/decimal-dollars.d.ts.map +1 -0
- package/dist/decimal-dollars.js +39 -0
- package/dist/decimal-dollars.js.map +1 -0
- package/dist/effect-channel.d.ts.map +1 -1
- package/dist/effect-channel.js +2 -0
- package/dist/effect-channel.js.map +1 -1
- package/dist/helper-instruction.d.ts +3 -0
- package/dist/helper-instruction.d.ts.map +1 -0
- package/dist/helper-instruction.js +3 -0
- package/dist/helper-instruction.js.map +1 -0
- package/dist/journal-client.d.ts +5 -2
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +12 -4
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts +2 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +7 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent-environment.d.ts +13 -0
- package/dist/local-agent-environment.d.ts.map +1 -0
- package/dist/local-agent-environment.js +104 -0
- package/dist/local-agent-environment.js.map +1 -0
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +2 -2
- package/dist/local-agent.js.map +1 -1
- package/dist/protocol.d.ts +21 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/reported-cost-total.d.ts +41 -0
- package/dist/reported-cost-total.d.ts.map +1 -0
- package/dist/reported-cost-total.js +63 -0
- package/dist/reported-cost-total.js.map +1 -0
- package/dist/reported-cost.d.ts +24 -0
- package/dist/reported-cost.d.ts.map +1 -0
- package/dist/reported-cost.js +49 -0
- package/dist/reported-cost.js.map +1 -0
- package/dist/run-state.d.ts +9 -11
- package/dist/run-state.d.ts.map +1 -1
- package/dist/run-state.js +15 -38
- package/dist/run-state.js.map +1 -1
- package/dist/worker-cli.d.ts +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +3 -3
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +5 -2
- package/dist/worker.js.map +1 -1
- package/dist/yaml-helper-effect.d.ts.map +1 -1
- package/dist/yaml-helper-effect.js +2 -1
- package/dist/yaml-helper-effect.js.map +1 -1
- package/dist/yaml-helpers.d.ts.map +1 -1
- package/dist/yaml-helpers.js +2 -1
- package/dist/yaml-helpers.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-flow-executor.ts +4 -1
- package/src/authored-helper-effect.ts +3 -2
- package/src/authored-mcp.ts +2 -0
- package/src/authored-node-entry.ts +8 -1
- package/src/authored-node-runner.ts +2 -1
- package/src/authored-plugin-effect.ts +2 -0
- package/src/authored-preflight.ts +2 -2
- package/src/authored-root.ts +7 -0
- package/src/authored-verdict.ts +2 -1
- package/src/authored-worker-step.ts +2 -1
- package/src/cli/check.ts +6 -3
- package/src/cli/cli-probe.ts +8 -6
- package/src/cli/direct-run.ts +7 -1
- package/src/cli/run.ts +15 -7
- package/src/communication/local.ts +3 -2
- package/src/communication/worker.ts +7 -4
- package/src/decimal-dollars.ts +38 -0
- package/src/effect-channel.ts +2 -0
- package/src/helper-instruction.ts +2 -0
- package/src/journal-client.ts +14 -5
- package/src/llm-worker.ts +6 -1
- package/src/local-agent-environment.ts +92 -0
- package/src/local-agent.ts +2 -1
- package/src/protocol.ts +22 -0
- package/src/reported-cost-total.ts +82 -0
- package/src/reported-cost.ts +51 -0
- package/src/run-state.ts +25 -37
- package/src/worker-cli.ts +3 -2
- package/src/worker.ts +9 -2
- package/src/yaml-helper-effect.ts +2 -1
- package/src/yaml-helpers.ts +2 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Exact decimal-dollar arithmetic shared by the journal folds.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sum two decimal dollar strings exactly, at whatever scale they carry — the
|
|
5
|
+
* kernel's budget fold (`relayflowd-core/src/state/budget.rs`) aligns every
|
|
6
|
+
* fractional digit before adding, and it accepts arbitrary precision. Scaling
|
|
7
|
+
* to a fixed six fractional digits dropped a charge of `0.0000001` to zero, so
|
|
8
|
+
* `flows status` underreported spend the kernel had totalled exactly.
|
|
9
|
+
*
|
|
10
|
+
* A value that is not a decimal number is left out of the sum rather than
|
|
11
|
+
* counted as zero, and the caller is told by `malformed`.
|
|
12
|
+
*/
|
|
13
|
+
export function addDollars(left: string, right: string): string {
|
|
14
|
+
const parsed = [left, right].map((value) => /^\d+(?:\.\d+)?$/.test(value) ? value : null);
|
|
15
|
+
const usable = parsed.filter((value): value is string => value !== null);
|
|
16
|
+
if (usable.length === 0) return '0';
|
|
17
|
+
if (usable.length === 1) return normalizeDollars(usable[0]!);
|
|
18
|
+
const scale = Math.max(...usable.map((value) => (value.split('.')[1] ?? '').length));
|
|
19
|
+
const scaled = usable.map((value) => {
|
|
20
|
+
const [whole, fraction = ''] = value.split('.');
|
|
21
|
+
return BigInt(`${whole}${fraction.padEnd(scale, '0')}`);
|
|
22
|
+
});
|
|
23
|
+
return fromScaled(scaled[0]! + scaled[1]!, scale);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `12.3400` -> `12.34`, `0.000` -> `0`; the journal's own spelling otherwise. */
|
|
27
|
+
function normalizeDollars(value: string): string {
|
|
28
|
+
const [whole, fraction = ''] = value.split('.');
|
|
29
|
+
return fromScaled(BigInt(`${whole}${fraction}`), fraction.length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fromScaled(total: bigint, scale: number): string {
|
|
33
|
+
if (scale === 0) return String(total);
|
|
34
|
+
const digits = String(total).padStart(scale + 1, '0');
|
|
35
|
+
const whole = digits.slice(0, digits.length - scale);
|
|
36
|
+
const fraction = digits.slice(digits.length - scale).replace(/0+$/, '');
|
|
37
|
+
return fraction.length === 0 ? whole : `${whole}.${fraction}`;
|
|
38
|
+
}
|
package/src/effect-channel.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { JournalClient } from './journal-client.js';
|
|
|
4
4
|
import type { StepDispatchEvent } from './protocol.js';
|
|
5
5
|
import { SPEC_SCHEMA_VERSION } from './spec.js';
|
|
6
6
|
import { withWorkerLease } from './worker-lease.js';
|
|
7
|
+
import { NO_MODEL_COST } from './reported-cost.js';
|
|
7
8
|
import { snapshotJsonValue } from './json-value.js';
|
|
8
9
|
|
|
9
10
|
/** Structural subset of Agent Relay's messages.dm; credentials stay in the client. */
|
|
@@ -93,5 +94,6 @@ export async function completeChannelPost(
|
|
|
93
94
|
messageId, channel: call.channel, to: call.to, text: call.text },
|
|
94
95
|
started_pins: dispatch.pins, end_pins: dispatch.pins,
|
|
95
96
|
effects: [{ surface_path: surfacePath, idempotency_key: dispatch.idempotency_key }],
|
|
97
|
+
reported_cost: NO_MODEL_COST,
|
|
96
98
|
});
|
|
97
99
|
}
|
package/src/journal-client.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { EventEmitter } from 'node:events';
|
|
|
13
13
|
export { walkJournal, JournalReadError, type JournalEvent, type JournalReadFailure } from './journal-reader.js';
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { createConnection, type Socket } from 'node:net';
|
|
16
|
-
import type { VerbContract, EventEmitParams, EventSubmitParams } from './protocol.js';
|
|
16
|
+
import type { VerbContract, EventEmitParams, EventSubmitParams, ReportedCost } from './protocol.js';
|
|
17
17
|
import {
|
|
18
18
|
PROTOCOL_VERSION,
|
|
19
19
|
type CompletionReason,
|
|
@@ -61,6 +61,8 @@ export class JournalProtocolError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export class JournalClient extends EventEmitter {
|
|
64
|
+
/** Additive request capabilities the daemon advertised at `hello`; none until then. */
|
|
65
|
+
private features: ReadonlySet<string> = new Set();
|
|
64
66
|
private socket: Socket | null = null;
|
|
65
67
|
/** Why the connection ended, so a later "not connected" names its cause rather than hiding it. */
|
|
66
68
|
private disconnectCause: Error | undefined;
|
|
@@ -215,9 +217,11 @@ export class JournalClient extends EventEmitter {
|
|
|
215
217
|
|
|
216
218
|
// --- Typed verb methods (gate 1 minimal set, kernel DESIGN.md §5) --------
|
|
217
219
|
|
|
218
|
-
/** Handshake; version mismatch is a hard error. */
|
|
219
|
-
hello(client: string): Promise<VerbContract['hello']['result']> {
|
|
220
|
-
|
|
220
|
+
/** Handshake; version mismatch is a hard error. Records the daemon's additive features. */
|
|
221
|
+
async hello(client: string): Promise<VerbContract['hello']['result']> {
|
|
222
|
+
const result = await this.request('hello', { protocol: PROTOCOL_VERSION, client });
|
|
223
|
+
this.features = new Set(Array.isArray(result.features) ? result.features.filter(f => typeof f === 'string') : []);
|
|
224
|
+
return result;
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
/**
|
|
@@ -403,15 +407,20 @@ export class JournalClient extends EventEmitter {
|
|
|
403
407
|
effects?: EffectRef[];
|
|
404
408
|
trajectory_tail?: unknown;
|
|
405
409
|
human_intervention?: boolean;
|
|
410
|
+
reported_cost?: ReportedCost;
|
|
406
411
|
} = {},
|
|
407
412
|
): Promise<VerbContract['step.complete']['result']> {
|
|
413
|
+
// `reported_cost` is display-only, and a daemon that predates it refuses
|
|
414
|
+
// unknown completion fields: drop it rather than fail the completion.
|
|
415
|
+
const { reported_cost: reportedCost, ...rest } = extra;
|
|
408
416
|
return this.request('step.complete', {
|
|
409
417
|
run_id: runId,
|
|
410
418
|
step_id: stepId,
|
|
411
419
|
attempt,
|
|
412
420
|
idempotency_key: idempotencyKey,
|
|
413
421
|
completionReason,
|
|
414
|
-
...
|
|
422
|
+
...rest,
|
|
423
|
+
...(reportedCost !== undefined && this.features.has('reported_cost') ? { reported_cost: reportedCost } : {}),
|
|
415
424
|
}, null).catch(error => {
|
|
416
425
|
if (error instanceof JournalProtocolError) error.verb = 'step.complete';
|
|
417
426
|
throw error;
|
package/src/llm-worker.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { workerSpend } from './worker-spend.js';
|
|
2
|
+
import { reportedCost } from './reported-cost.js';
|
|
2
3
|
import { DEFAULT_LOCAL_AGENT_CAPACITY } from './worker-slots.js';
|
|
3
4
|
import type { WorkerCliResult } from './worker-cli.js';
|
|
4
5
|
import { EventEmitter } from 'node:events';
|
|
@@ -22,6 +23,7 @@ export class LlmWorker extends EventEmitter {
|
|
|
22
23
|
private readonly client: JournalClient,
|
|
23
24
|
private readonly workerId: string,
|
|
24
25
|
private readonly capacity: number = DEFAULT_LOCAL_AGENT_CAPACITY,
|
|
26
|
+
private readonly environment?: NodeJS.ProcessEnv,
|
|
25
27
|
) {
|
|
26
28
|
super();
|
|
27
29
|
}
|
|
@@ -59,9 +61,11 @@ export class LlmWorker extends EventEmitter {
|
|
|
59
61
|
const effectiveModel = typeof spec.cli === 'string' ? resolveCliModel(spec.cli, spec.model) : spec.model;
|
|
60
62
|
const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
|
|
61
63
|
typeof spec.cli === 'string' && typeof spec.prompt === 'string'
|
|
62
|
-
? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'llm'
|
|
64
|
+
? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'llm',
|
|
65
|
+
undefined, undefined, 'direct', undefined, this.environment)
|
|
63
66
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'llm step has no declared CLI' }));
|
|
64
67
|
const { result, usage } = workerSpend(completed, effectiveModel);
|
|
68
|
+
const cost = reportedCost(completed, effectiveModel);
|
|
65
69
|
let reason: CompletionReason = result.exit_code === 0 ? 'success' : 'worker_error';
|
|
66
70
|
let output: unknown = result.stdout_tail;
|
|
67
71
|
let detail = result.stderr_tail;
|
|
@@ -94,6 +98,7 @@ export class LlmWorker extends EventEmitter {
|
|
|
94
98
|
dispatch.idempotency_key, reason, {
|
|
95
99
|
output,
|
|
96
100
|
...(usage !== undefined ? { usage } : {}),
|
|
101
|
+
...(cost === undefined ? {} : { reported_cost: cost }),
|
|
97
102
|
...(Object.keys(trajectoryTail).length === 0 ? {} : { trajectory_tail: trajectoryTail }),
|
|
98
103
|
});
|
|
99
104
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { closeSync, readSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inherited descriptor carrying environment values that only local agent/LLM
|
|
5
|
+
* subprocesses may receive. The flows process consumes and closes it before
|
|
6
|
+
* loading authored code, so that code cannot inherit or read the credentials.
|
|
7
|
+
*/
|
|
8
|
+
export const LOCAL_AGENT_ENV_FD = 'FLOWS_LOCAL_AGENT_ENV_FD';
|
|
9
|
+
|
|
10
|
+
const MAX_BYTES = 64 * 1024;
|
|
11
|
+
const AGENT_CREDENTIAL_NAMES = new Set([
|
|
12
|
+
'ANTHROPIC_API_KEY',
|
|
13
|
+
'ANTHROPIC_BASE_URL',
|
|
14
|
+
'OPENAI_API_KEY',
|
|
15
|
+
'OPENAI_BASE_URL',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function localAgentEnvironment(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv | undefined {
|
|
19
|
+
const environment = { ...base };
|
|
20
|
+
const rawFd = environment[LOCAL_AGENT_ENV_FD];
|
|
21
|
+
delete environment[LOCAL_AGENT_ENV_FD];
|
|
22
|
+
// Without the descriptor, provider launches keep their historical live
|
|
23
|
+
// process.env behavior instead of freezing an early snapshot.
|
|
24
|
+
if (rawFd === undefined) return undefined;
|
|
25
|
+
// Every later child — especially the authored runtime — inherits process.env.
|
|
26
|
+
// Remove the capability marker before any authored module can be imported.
|
|
27
|
+
if (base === process.env) delete process.env[LOCAL_AGENT_ENV_FD];
|
|
28
|
+
|
|
29
|
+
if (!/^[0-9]+$/.test(rawFd)) throw new Error(`${LOCAL_AGENT_ENV_FD} must name an inherited file descriptor`);
|
|
30
|
+
const fd = Number(rawFd);
|
|
31
|
+
if (!Number.isSafeInteger(fd) || fd < 3 || fd > 255) {
|
|
32
|
+
throw new Error(`${LOCAL_AGENT_ENV_FD} must name an inherited file descriptor`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const chunks: Buffer[] = [];
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
try {
|
|
38
|
+
for (;;) {
|
|
39
|
+
const chunk = Buffer.allocUnsafe(Math.min(8 * 1024, MAX_BYTES + 1 - bytes));
|
|
40
|
+
const read = readSync(fd, chunk, 0, chunk.length, null);
|
|
41
|
+
if (read === 0) break;
|
|
42
|
+
bytes += read;
|
|
43
|
+
if (bytes > MAX_BYTES) throw new Error('local agent environment payload exceeds 64 KiB');
|
|
44
|
+
chunks.push(chunk.subarray(0, read));
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof Error && error.message === 'local agent environment payload exceeds 64 KiB') throw error;
|
|
48
|
+
throw new Error('local agent environment payload could not be read');
|
|
49
|
+
} finally {
|
|
50
|
+
try { closeSync(fd); } catch { /* The descriptor is unusable either way. */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let parsed: unknown;
|
|
54
|
+
try { parsed = JSON.parse(Buffer.concat(chunks, bytes).toString('utf8')); }
|
|
55
|
+
catch { throw new Error('local agent environment payload is not valid JSON'); }
|
|
56
|
+
if (!plainObject(parsed)) throw new Error('local agent environment payload must be an object of strings');
|
|
57
|
+
const agentEnvironment: NodeJS.ProcessEnv = {};
|
|
58
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
59
|
+
if (!AGENT_CREDENTIAL_NAMES.has(key) || typeof value !== 'string' || value.includes('\0')) {
|
|
60
|
+
throw new Error('local agent environment payload must be an object of valid environment strings');
|
|
61
|
+
}
|
|
62
|
+
if (key !== LOCAL_AGENT_ENV_FD) agentEnvironment[key] = value;
|
|
63
|
+
}
|
|
64
|
+
return { ...environment, ...agentEnvironment };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reduce a composed provider environment to the credential overlay that is
|
|
68
|
+
* safe to serialize across the authored Node runtime boundary. */
|
|
69
|
+
export function localAgentCredentialEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
70
|
+
return Object.fromEntries([...AGENT_CREDENTIAL_NAMES]
|
|
71
|
+
.flatMap(key => environment[key] === undefined ? [] : [[key, environment[key]!]]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Validate the parent-to-authored-runtime handoff without touching a descriptor. */
|
|
75
|
+
export function validatedLocalAgentEnvironment(value: unknown): NodeJS.ProcessEnv | undefined {
|
|
76
|
+
if (value === undefined) return undefined;
|
|
77
|
+
if (!plainObject(value)) throw new Error('local agent environment payload must be an object of strings');
|
|
78
|
+
const environment: NodeJS.ProcessEnv = {};
|
|
79
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
80
|
+
if (!AGENT_CREDENTIAL_NAMES.has(key) || typeof entry !== 'string' || entry.includes('\0')) {
|
|
81
|
+
throw new Error('local agent environment payload must be an object of valid environment strings');
|
|
82
|
+
}
|
|
83
|
+
environment[key] = entry;
|
|
84
|
+
}
|
|
85
|
+
return environment;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function plainObject(value: unknown): value is Record<string, unknown> {
|
|
89
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
90
|
+
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
91
|
+
return prototype === Object.prototype || prototype === null;
|
|
92
|
+
}
|
package/src/local-agent.ts
CHANGED
|
@@ -15,6 +15,7 @@ export async function attachLocalAgent(
|
|
|
15
15
|
capacity: number = DEFAULT_LOCAL_AGENT_CAPACITY,
|
|
16
16
|
runRoot?: string,
|
|
17
17
|
declaredStreams: readonly string[] = [],
|
|
18
|
+
environment?: NodeJS.ProcessEnv,
|
|
18
19
|
): Promise<{
|
|
19
20
|
stream: string;
|
|
20
21
|
readonly failure: unknown;
|
|
@@ -31,7 +32,7 @@ export async function attachLocalAgent(
|
|
|
31
32
|
// second parking behind the first. Authored bodies size their admission to
|
|
32
33
|
// this same number (worker-slots.ts), so they never ask for more.
|
|
33
34
|
capacity,
|
|
34
|
-
dataDir, onPtyReady, runRoot,
|
|
35
|
+
dataDir, onPtyReady, runRoot, environment,
|
|
35
36
|
pins: { workspace: [], streams: [...new Set([stream, ...declaredStreams])]
|
|
36
37
|
.map(stream => ({ stream, read_offset: 0 })) },
|
|
37
38
|
});
|
package/src/protocol.ts
CHANGED
|
@@ -79,6 +79,11 @@ export interface HelloParams {
|
|
|
79
79
|
export interface HelloResult {
|
|
80
80
|
protocol: 0;
|
|
81
81
|
server: string;
|
|
82
|
+
/**
|
|
83
|
+
* Additive request capabilities within protocol 0. Absent from daemons that
|
|
84
|
+
* predate it, which then accept none of the optional fields it gates.
|
|
85
|
+
*/
|
|
86
|
+
features?: string[];
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
export interface StepSpend {
|
|
@@ -97,6 +102,21 @@ export interface StepSpend {
|
|
|
97
102
|
* `dollars_unmetered: true` and no dollar amount, so unknown cost is never
|
|
98
103
|
* journaled as a measured $0. See `workerSpend` for the full contract.
|
|
99
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* A step attempt's actual cost (`reported-cost.ts`), journaled beside the
|
|
107
|
+
* metered `usage` as `reported_cost`. Display only: the kernel never charges it
|
|
108
|
+
* against `maxDollars`.
|
|
109
|
+
*/
|
|
110
|
+
export interface ReportedCost {
|
|
111
|
+
/** Non-negative decimal string, six places. */
|
|
112
|
+
dollars: string;
|
|
113
|
+
/**
|
|
114
|
+
* `cli`: the CLI's own reported total. `priced`: estimated from full token
|
|
115
|
+
* usage. `no_model`: an internal effect worker completed the step; $0.
|
|
116
|
+
*/
|
|
117
|
+
source: 'cli' | 'priced' | 'no_model';
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
export type StepUsage =
|
|
101
121
|
| { tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: never }
|
|
102
122
|
| { tokens_in: number; tokens_out: number; dollars_unmetered: true; dollars?: never };
|
|
@@ -342,6 +362,8 @@ export interface StepCompleteParams {
|
|
|
342
362
|
effects?: EffectRef[];
|
|
343
363
|
/** `inspect` evidence for the next attempt. Rejected over 16 KiB of JSON. */
|
|
344
364
|
trajectory_tail?: unknown;
|
|
365
|
+
/** Display-only actual cost; never charged to the budget. */
|
|
366
|
+
reported_cost?: ReportedCost;
|
|
345
367
|
}
|
|
346
368
|
export type StepCompleteResult = RunOutcome;
|
|
347
369
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The display-only actual-cost fold beside run-state's metered spend.
|
|
2
|
+
// Nothing here is enforcement: the kernel charges `budget`, never `reported_cost`.
|
|
3
|
+
|
|
4
|
+
import { declaresAuthoredRoot } from './authored-verdict.js';
|
|
5
|
+
import { addDollars } from './decimal-dollars.js';
|
|
6
|
+
import { HELPER_INSTRUCTION_PREFIX } from './helper-instruction.js';
|
|
7
|
+
import type { JournalEvent } from './journal-reader.js';
|
|
8
|
+
|
|
9
|
+
type Payload = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What steps actually cost, summed from each attempt's journaled
|
|
13
|
+
* `reported_cost` (the CLI's own total, or a full-usage estimate). Display
|
|
14
|
+
* only: the kernel charges `spend` against `maxDollars`, never this. `complete`
|
|
15
|
+
* is false when some completed model attempt reported no cost (e.g. Codex, or
|
|
16
|
+
* an older runtime), so `dollars` is then a lower bound. Segment rollover keeps
|
|
17
|
+
* earlier completions in the journal, so an epoch summary does not lose any.
|
|
18
|
+
*/
|
|
19
|
+
export interface ReportedCostTotal {
|
|
20
|
+
/** Decimal string. */
|
|
21
|
+
dollars: string;
|
|
22
|
+
complete: boolean;
|
|
23
|
+
/** Where the summed figures came from; `mixed` when attempts differ. */
|
|
24
|
+
source: 'cli' | 'priced' | 'mixed' | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Steps known to run no model, read from the spawned spec. Deliberately
|
|
29
|
+
* narrow: a crash-recovered or cancelled model attempt journals a default zero
|
|
30
|
+
* budget although the model ran, so metering nothing proves nothing. A step
|
|
31
|
+
* not listed here that reports no cost stays unknown, and the total reads as a
|
|
32
|
+
* lower bound rather than a false complete figure.
|
|
33
|
+
* - a deterministic step runs a command;
|
|
34
|
+
* - a provider helper (`relayflows:helper:v1`) performs a relayfile write;
|
|
35
|
+
* - the authored root runs the flow body, whose model calls are child runs with
|
|
36
|
+
* journals of their own. Only a spec carrying the authored-root discriminator
|
|
37
|
+
* counts: an ordinary flow may name a step `authored-root`.
|
|
38
|
+
* A memoized reuse is free too, per completion (`reused_from`): the source run
|
|
39
|
+
* paid for the model.
|
|
40
|
+
*/
|
|
41
|
+
export function modelFreeSteps(spawned: JournalEvent): Set<string> {
|
|
42
|
+
const payload = spawned.payload !== null && typeof spawned.payload === 'object' ? spawned.payload as Payload : {};
|
|
43
|
+
const spec = payload['spec'] !== null && typeof payload['spec'] === 'object' ? payload['spec'] as Payload : {};
|
|
44
|
+
const steps = Array.isArray(spec['steps']) ? spec['steps'] as unknown[] : [];
|
|
45
|
+
const authoredRoot = declaresAuthoredRoot(spawned);
|
|
46
|
+
const free = new Set<string>();
|
|
47
|
+
for (const entry of steps) {
|
|
48
|
+
const step = entry !== null && typeof entry === 'object' ? entry as Payload : {};
|
|
49
|
+
if (typeof step['id'] !== 'string') continue;
|
|
50
|
+
const helper = step['type'] === 'agent' && typeof step['instruction'] === 'string'
|
|
51
|
+
&& step['instruction'].startsWith(HELPER_INSTRUCTION_PREFIX);
|
|
52
|
+
if (step['type'] === 'deterministic' || helper || (authoredRoot && step['id'] === 'authored-root')) free.add(step['id']);
|
|
53
|
+
}
|
|
54
|
+
return free;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const NO_REPORTED_COST: ReportedCostTotal = { dollars: '0', complete: true, source: null };
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add one completed attempt's journaled `reported_cost` to a total. A model
|
|
62
|
+
* attempt without one (older runtime, Codex, unpriced) leaves the sum unchanged
|
|
63
|
+
* and marks it incomplete rather than counting an unknown cost as zero.
|
|
64
|
+
*/
|
|
65
|
+
export function addReportedCost(total: ReportedCostTotal, completion: Payload, modelFree: boolean): ReportedCostTotal {
|
|
66
|
+
const charge = completion['reported_cost'];
|
|
67
|
+
const cost = charge !== null && typeof charge === 'object' && !Array.isArray(charge) ? charge as Payload : null;
|
|
68
|
+
const reused = completion['reused_from'] !== undefined && completion['reused_from'] !== null;
|
|
69
|
+
if (cost === null && (modelFree || reused)) return total;
|
|
70
|
+
const dollars = cost === null ? null : cost['dollars'];
|
|
71
|
+
const source = cost === null ? null : cost['source'];
|
|
72
|
+
// An internal effect worker ran no model: a known $0 that names no cost source.
|
|
73
|
+
if (source === 'no_model') return total;
|
|
74
|
+
if (typeof dollars !== 'string' || !/^\d+(?:\.\d+)?$/.test(dollars) || (source !== 'cli' && source !== 'priced')) {
|
|
75
|
+
return { ...total, complete: false };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
dollars: addDollars(total.dollars, dollars),
|
|
79
|
+
complete: total.complete,
|
|
80
|
+
source: total.source === null || total.source === source ? source : 'mixed',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MODEL_PRICING } from './model-pricing.js';
|
|
2
|
+
import type { ReportedCost } from './protocol.js';
|
|
3
|
+
import type { WorkerCliResult } from './worker-cli.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a step attempt actually cost, for display. Never enforcement.
|
|
7
|
+
*
|
|
8
|
+
* The metered `usage` (`workerSpend`) is what the kernel charges against
|
|
9
|
+
* `maxDollars`, and it prices only plain input and output tokens. That
|
|
10
|
+
* undercounts cache-heavy agents badly. This is the real figure a reader wants
|
|
11
|
+
* to see, and it rides beside the metered charge as `reported_cost`: the kernel
|
|
12
|
+
* journals it and never folds it into the budget. Changing which charges count
|
|
13
|
+
* toward a cap is a separate decision, and this module takes no part in it.
|
|
14
|
+
*
|
|
15
|
+
* Sources, in order:
|
|
16
|
+
* - `cli`: the CLI's own reported total (Claude `total_cost_usd`);
|
|
17
|
+
* - `priced`: an estimate from the digest's full token usage at the frozen
|
|
18
|
+
* `MODEL_PRICING` rates, with cache writes at 1.25x and cache reads at 0.1x
|
|
19
|
+
* the input rate (Codex's input total already includes its cached tokens, so
|
|
20
|
+
* they are split out, not added). Only for a model that already has a price.
|
|
21
|
+
* Otherwise undefined: an unknown cost is left unknown, never shown as $0.
|
|
22
|
+
*/
|
|
23
|
+
export function reportedCost(result: WorkerCliResult, model: string | undefined): ReportedCost | undefined {
|
|
24
|
+
const digest = result.transcript?.result;
|
|
25
|
+
const cli = digest?.total_cost_usd;
|
|
26
|
+
if (typeof cli === 'number' && Number.isFinite(cli) && cli >= 0) {
|
|
27
|
+
return { dollars: microDollars(BigInt(Math.round(cli * 1_000_000))), source: 'cli' };
|
|
28
|
+
}
|
|
29
|
+
const usage = digest?.usage;
|
|
30
|
+
const priceModel = digest?.model ?? model;
|
|
31
|
+
if (usage === undefined || priceModel === undefined || !Object.hasOwn(MODEL_PRICING, priceModel)) return undefined;
|
|
32
|
+
const counts = [usage.input, usage.output, usage.cache_read, usage.cache_creation].map(n => n ?? 0);
|
|
33
|
+
if (!counts.every(n => Number.isSafeInteger(n) && n >= 0)) return undefined;
|
|
34
|
+
const [reportedInput, output, cacheRead, cacheCreation] = counts.map(BigInt) as [bigint, bigint, bigint, bigint];
|
|
35
|
+
// Claude reports cache tokens beside `input_tokens`; Codex's `input_tokens`
|
|
36
|
+
// already includes its `cached_input_tokens`. Charge each token once.
|
|
37
|
+
const input = digest?.provider === 'codex'
|
|
38
|
+
? (reportedInput > cacheRead ? reportedInput - cacheRead : 0n) : reportedInput;
|
|
39
|
+
const price = MODEL_PRICING[priceModel]!;
|
|
40
|
+
const inRate = BigInt(price.input);
|
|
41
|
+
const micro = input * inRate + output * BigInt(price.output)
|
|
42
|
+
+ (cacheCreation * inRate * 125n) / 100n + (cacheRead * inRate * 10n) / 100n;
|
|
43
|
+
return { dollars: microDollars(micro), source: 'priced' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What an internal effect worker (provider helper, plugin, MCP) reports: no model ran. */
|
|
47
|
+
export const NO_MODEL_COST: ReportedCost = Object.freeze({ dollars: '0.000000', source: 'no_model' });
|
|
48
|
+
|
|
49
|
+
function microDollars(micro: bigint): string {
|
|
50
|
+
return `${micro / 1_000_000n}.${String(micro % 1_000_000n).padStart(6, '0')}`;
|
|
51
|
+
}
|
package/src/run-state.ts
CHANGED
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
// No I/O, no clock: the caller passes `now_ms`, so a test folds a hand-built
|
|
11
11
|
// journal against a fixed instant and asserts exact elapsed and overdue values.
|
|
12
12
|
|
|
13
|
+
import { addDollars } from './decimal-dollars.js';
|
|
14
|
+
import { addReportedCost, modelFreeSteps, NO_REPORTED_COST, type ReportedCostTotal } from './reported-cost-total.js';
|
|
13
15
|
import type { JournalEvent } from './journal-reader.js';
|
|
14
16
|
|
|
17
|
+
export { addDollars } from './decimal-dollars.js';
|
|
18
|
+
export type { ReportedCostTotal } from './reported-cost-total.js';
|
|
19
|
+
|
|
15
20
|
export type RunStatus = 'running' | 'parked' | 'completed' | 'failed' | 'cancelling' | 'cancelled';
|
|
16
21
|
|
|
17
22
|
/** `StepState` variant names from state.rs, snake_case. */
|
|
@@ -25,6 +30,7 @@ export interface Spend {
|
|
|
25
30
|
dollars_unmetered: boolean;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
|
|
28
34
|
export interface StepCounts {
|
|
29
35
|
total: number;
|
|
30
36
|
done: number;
|
|
@@ -91,6 +97,10 @@ export interface StepView {
|
|
|
91
97
|
completion_reason: string | null;
|
|
92
98
|
/** Paths the worker journaled for the last attempt, when its output carried them. */
|
|
93
99
|
artifacts: { paths: string[]; journaled: boolean };
|
|
100
|
+
/** Metered charges of this step's attempts: what counted toward the budget. */
|
|
101
|
+
spend: Spend;
|
|
102
|
+
/** Actual cost of this step's attempts. Null until an attempt completes. */
|
|
103
|
+
reported_cost: ReportedCostTotal | null;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
export interface RunView {
|
|
@@ -101,6 +111,8 @@ export interface RunView {
|
|
|
101
111
|
spawned_at_ms: number;
|
|
102
112
|
now_ms: number;
|
|
103
113
|
spend: Spend;
|
|
114
|
+
/** Actual cost of this run's own attempts; see {@link ReportedCostTotal}. */
|
|
115
|
+
reported_cost: ReportedCostTotal;
|
|
104
116
|
counts: StepCounts;
|
|
105
117
|
steps: StepView[];
|
|
106
118
|
}
|
|
@@ -127,42 +139,6 @@ function text(value: unknown, fallback = ''): string {
|
|
|
127
139
|
return typeof value === 'string' ? value : fallback;
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
/**
|
|
131
|
-
* Sum two decimal dollar strings exactly, at whatever scale they carry — the
|
|
132
|
-
* kernel's budget fold (`relayflowd-core/src/state/budget.rs`) aligns every
|
|
133
|
-
* fractional digit before adding, and it accepts arbitrary precision. Scaling
|
|
134
|
-
* to a fixed six fractional digits dropped a charge of `0.0000001` to zero, so
|
|
135
|
-
* `flows status` underreported spend the kernel had totalled exactly.
|
|
136
|
-
*
|
|
137
|
-
* A value that is not a decimal number is left out of the sum rather than
|
|
138
|
-
* counted as zero, and the caller is told by `malformed`.
|
|
139
|
-
*/
|
|
140
|
-
export function addDollars(left: string, right: string): string {
|
|
141
|
-
const parsed = [left, right].map((value) => /^\d+(?:\.\d+)?$/.test(value) ? value : null);
|
|
142
|
-
const usable = parsed.filter((value): value is string => value !== null);
|
|
143
|
-
if (usable.length === 0) return '0';
|
|
144
|
-
if (usable.length === 1) return normalizeDollars(usable[0]!);
|
|
145
|
-
const scale = Math.max(...usable.map((value) => (value.split('.')[1] ?? '').length));
|
|
146
|
-
const scaled = usable.map((value) => {
|
|
147
|
-
const [whole, fraction = ''] = value.split('.');
|
|
148
|
-
return BigInt(`${whole}${fraction.padEnd(scale, '0')}`);
|
|
149
|
-
});
|
|
150
|
-
return fromScaled(scaled[0]! + scaled[1]!, scale);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** `12.3400` -> `12.34`, `0.000` -> `0`; the journal's own spelling otherwise. */
|
|
154
|
-
function normalizeDollars(value: string): string {
|
|
155
|
-
const [whole, fraction = ''] = value.split('.');
|
|
156
|
-
return fromScaled(BigInt(`${whole}${fraction}`), fraction.length);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function fromScaled(total: bigint, scale: number): string {
|
|
160
|
-
if (scale === 0) return String(total);
|
|
161
|
-
const digits = String(total).padStart(scale + 1, '0');
|
|
162
|
-
const whole = digits.slice(0, digits.length - scale);
|
|
163
|
-
const fraction = digits.slice(digits.length - scale).replace(/0+$/, '');
|
|
164
|
-
return fraction.length === 0 ? whole : `${whole}.${fraction}`;
|
|
165
|
-
}
|
|
166
142
|
|
|
167
143
|
/**
|
|
168
144
|
* Reduce `trajectory_tail.transcript` (the digest flows#491 journals) to the
|
|
@@ -209,6 +185,7 @@ function addSpend(total: Spend, charge: unknown): Spend {
|
|
|
209
185
|
|
|
210
186
|
const ZERO_SPEND: Spend = { tokens_in: 0, tokens_out: 0, dollars: '0', dollars_unmetered: false };
|
|
211
187
|
|
|
188
|
+
|
|
212
189
|
interface StepFold {
|
|
213
190
|
view: StepView;
|
|
214
191
|
depends_on: string[];
|
|
@@ -221,6 +198,7 @@ function freshStep(id: string, type: string, maxIterations: number | null, depen
|
|
|
221
198
|
id, type, state: 'pending', attempt: 0, max_iterations: maxIterations,
|
|
222
199
|
started_at_ms: null, elapsed_ms: null, lease: null, backoff_until_ms: null, wait: null,
|
|
223
200
|
last_attempt: null, completion_reason: null, artifacts: { paths: [], journaled: false },
|
|
201
|
+
spend: ZERO_SPEND, reported_cost: null,
|
|
224
202
|
},
|
|
225
203
|
};
|
|
226
204
|
}
|
|
@@ -268,6 +246,8 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
|
|
|
268
246
|
// An authored child run carries its parent's totals so ceilings hold across
|
|
269
247
|
// the family (state.rs `prior_spend`); the view reports the same running total.
|
|
270
248
|
let spend = addSpend(ZERO_SPEND, budget['prior_spend']);
|
|
249
|
+
let reportedCost = NO_REPORTED_COST;
|
|
250
|
+
const modelFree = modelFreeSteps(first);
|
|
271
251
|
let completion: string | null = null;
|
|
272
252
|
let cancelRequested = false;
|
|
273
253
|
|
|
@@ -297,6 +277,9 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
|
|
|
297
277
|
const attempt = event.attempt ?? view.attempt;
|
|
298
278
|
view.attempt = Math.max(view.attempt, attempt);
|
|
299
279
|
spend = addSpend(spend, payload['budget']);
|
|
280
|
+
view.spend = addSpend(view.spend, payload['budget']);
|
|
281
|
+
reportedCost = addReportedCost(reportedCost, payload, modelFree.has(view.id));
|
|
282
|
+
view.reported_cost = addReportedCost(view.reported_cost ?? NO_REPORTED_COST, payload, modelFree.has(view.id));
|
|
300
283
|
const verification = payload['verification'] !== null && typeof payload['verification'] === 'object'
|
|
301
284
|
? payload['verification'] as Payload : undefined;
|
|
302
285
|
view.last_attempt = {
|
|
@@ -362,8 +345,12 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
|
|
|
362
345
|
// A new epoch restarts every step (state.rs `apply_epoch`); the
|
|
363
346
|
// summary then restores the ones it carries forward.
|
|
364
347
|
for (const step of steps.values()) {
|
|
365
|
-
const { id, type, max_iterations } = step.view;
|
|
348
|
+
const { id, type, max_iterations, spend: stepSpend, reported_cost: stepCost } = step.view;
|
|
366
349
|
step.view = freshStep(id, type, max_iterations, step.depends_on).view;
|
|
350
|
+
// Rollover keeps the earlier segments' completions in the journal,
|
|
351
|
+
// and this fold has already read them: what the step cost stays.
|
|
352
|
+
step.view.spend = stepSpend;
|
|
353
|
+
step.view.reported_cost = stepCost;
|
|
367
354
|
}
|
|
368
355
|
spend = addSpend(ZERO_SPEND, payload['budget_spent']);
|
|
369
356
|
const done = payload['steps_done'] !== null && typeof payload['steps_done'] === 'object' ? payload['steps_done'] as Payload : {};
|
|
@@ -462,6 +449,7 @@ export function foldRunState(events: readonly JournalEvent[], now_ms: number): R
|
|
|
462
449
|
spawned_at_ms: first.at_ms,
|
|
463
450
|
now_ms,
|
|
464
451
|
spend,
|
|
452
|
+
reported_cost: reportedCost,
|
|
465
453
|
counts,
|
|
466
454
|
steps: views,
|
|
467
455
|
};
|
package/src/worker-cli.ts
CHANGED
|
@@ -91,6 +91,7 @@ export async function runAgentCli(
|
|
|
91
91
|
cwd?: string,
|
|
92
92
|
transport: AgentTransport = 'direct',
|
|
93
93
|
relayContext?: AgentRelayContext,
|
|
94
|
+
processEnvironment: NodeJS.ProcessEnv = process.env,
|
|
94
95
|
): Promise<WorkerCliResult> {
|
|
95
96
|
signal?.throwIfAborted();
|
|
96
97
|
if (signal !== undefined && process.platform === 'win32') {
|
|
@@ -134,7 +135,7 @@ export async function runAgentCli(
|
|
|
134
135
|
if (kind === 'relayflows-wrapper-v1') {
|
|
135
136
|
// The closed allowlist admits no ambient RELAYFLOW_* value; the four
|
|
136
137
|
// discovery names are set from this dispatch, exactly as for a direct spawn.
|
|
137
|
-
const wrapperEnv = wrapperEnvironment(
|
|
138
|
+
const wrapperEnv = wrapperEnvironment(processEnvironment);
|
|
138
139
|
applyStepEnvironment(wrapperEnv, sidechannel);
|
|
139
140
|
return requirePricedUsage(decodeWrapperResult(await runWrapperSession(
|
|
140
141
|
cli,
|
|
@@ -148,7 +149,7 @@ export async function runAgentCli(
|
|
|
148
149
|
)), effectiveModel);
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
const env: NodeJS.ProcessEnv = { ...
|
|
152
|
+
const env: NodeJS.ProcessEnv = { ...processEnvironment };
|
|
152
153
|
delete env[WAKE_CONTEXT_ENV];
|
|
153
154
|
delete env[MODEL_ENV];
|
|
154
155
|
// Where this attempt's journal is, so the agent can run `flows status` on
|
package/src/worker.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { communicationInstruction } from './communication/spec.js';
|
|
2
2
|
import { workerSpend } from './worker-spend.js';
|
|
3
|
+
import { reportedCost } from './reported-cost.js';
|
|
3
4
|
import type { WorkerCliResult } from './worker-cli.js';
|
|
4
5
|
import { EventEmitter } from 'node:events';
|
|
5
6
|
import type { JournalClient } from './journal-client.js';
|
|
@@ -31,6 +32,8 @@ export interface AgentWorkerOptions {
|
|
|
31
32
|
* the root a dispatch is measured against is a value, not an assumption.
|
|
32
33
|
*/
|
|
33
34
|
runRoot?: string;
|
|
35
|
+
/** Environment inherited by provider CLIs, including agent-only secrets. */
|
|
36
|
+
environment?: NodeJS.ProcessEnv;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -122,7 +125,8 @@ export class AgentWorker extends EventEmitter {
|
|
|
122
125
|
if (communication) {
|
|
123
126
|
if (!this.options.dataDir) throw new Error('Agent communication requires a worker data directory');
|
|
124
127
|
const { completeCommunicationDispatch } = await import('./communication/worker.js');
|
|
125
|
-
await completeCommunicationDispatch(this.client, dispatch, communication, this.options.dataDir,
|
|
128
|
+
await completeCommunicationDispatch(this.client, dispatch, communication, this.options.dataDir,
|
|
129
|
+
this.options.runRoot, this.options.environment);
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
132
|
let humanIntervention = false;
|
|
@@ -142,9 +146,11 @@ export class AgentWorker extends EventEmitter {
|
|
|
142
146
|
}, cwd.directory ?? (spec.transport === 'relay' ? undefined : this.options.runRoot),
|
|
143
147
|
spec.transport === 'relay' ? 'relay' : 'direct',
|
|
144
148
|
{ runId: dispatch.run_id, stepId: dispatch.step_id, idempotencyKey: dispatch.idempotency_key,
|
|
145
|
-
dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema }
|
|
149
|
+
dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema },
|
|
150
|
+
this.options.environment)
|
|
146
151
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'agent step has no declared CLI' }));
|
|
147
152
|
const { result, usage } = workerSpend(completed, effectiveModel);
|
|
153
|
+
const cost = reportedCost(completed, effectiveModel);
|
|
148
154
|
const completionReason = result.exit_code === 0 ? 'success' : 'worker_error';
|
|
149
155
|
|
|
150
156
|
// Output shape: if the CLI's stdout parses as JSON, promote THAT
|
|
@@ -195,6 +201,7 @@ export class AgentWorker extends EventEmitter {
|
|
|
195
201
|
...(Object.keys(trajectoryTail).length === 0 ? {} : { trajectory_tail: trajectoryTail }),
|
|
196
202
|
...(humanIntervention ? { human_intervention: true } : {}),
|
|
197
203
|
...(usage !== undefined ? { usage } : {}),
|
|
204
|
+
...(cost === undefined ? {} : { reported_cost: cost }),
|
|
198
205
|
started_pins: dispatch.pins,
|
|
199
206
|
end_pins: dispatch.pins,
|
|
200
207
|
},
|