@relayflows/sdk 2.0.36 → 2.0.38
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/agent-reaper.d.ts +1 -1
- package/dist/agent-reaper.d.ts.map +1 -1
- package/dist/agent-reaper.js +6 -4
- package/dist/agent-reaper.js.map +1 -1
- package/dist/authored-budget.d.ts +26 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +72 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +7 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- 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 +5 -3
- 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 +146 -6
- 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 +3 -2
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/child-stop.d.ts +3 -1
- package/dist/child-stop.d.ts.map +1 -1
- package/dist/child-stop.js +42 -2
- package/dist/child-stop.js.map +1 -1
- package/dist/cli/add-extension.js +1 -1
- package/dist/cli/add-extension.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 +28 -10
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +3 -2
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +79 -42
- 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 +11 -3
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/pinned-cli-alias.d.ts +13 -0
- package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
- package/dist/cli/pinned-cli-alias.js +58 -0
- package/dist/cli/pinned-cli-alias.js.map +1 -0
- package/dist/cli/run.d.ts +8 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +106 -17
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +18 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +9 -2
- package/dist/cloud-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 +27 -9
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +16 -7
- package/dist/compile.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/failure-kinds.d.ts +9 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +6 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +1 -1
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +143 -14
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-extension-manifest.d.ts +1 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -1
- package/dist/flow-extension-manifest.js +12 -3
- package/dist/flow-extension-manifest.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/hosted-extension-manifest.d.ts.map +1 -1
- package/dist/hosted-extension-manifest.js +25 -4
- package/dist/hosted-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/hosted-extension-sandbox.js +2 -2
- 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 +10 -3
- 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/mcp-stdio.d.ts +2 -1
- package/dist/mcp-stdio.d.ts.map +1 -1
- package/dist/mcp-stdio.js +38 -21
- package/dist/mcp-stdio.js.map +1 -1
- package/dist/preflight.d.ts +2 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +30 -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/resolved-cli-identity.d.ts +14 -0
- package/dist/resolved-cli-identity.d.ts.map +1 -0
- package/dist/resolved-cli-identity.js +19 -0
- package/dist/resolved-cli-identity.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 +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +118 -42
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts +3 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +47 -9
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +8 -3
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +3 -1
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +93 -53
- package/dist/wrapper-session.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/agent-reaper.ts +7 -4
- package/src/authored-budget.ts +73 -1
- package/src/authored-flow-error.ts +6 -0
- 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 +5 -3
- package/src/authored-plugin-effect.ts +2 -0
- package/src/authored-preflight.ts +2 -2
- package/src/authored-root.ts +157 -7
- package/src/authored-verdict.ts +2 -1
- package/src/authored-worker-step.ts +3 -1
- package/src/child-stop.ts +40 -1
- package/src/cli/add-extension.ts +1 -1
- package/src/cli/check.ts +31 -7
- package/src/cli/cli-probe.ts +80 -42
- package/src/cli/direct-run.ts +11 -2
- package/src/cli/pinned-cli-alias.ts +74 -0
- package/src/cli/run.ts +118 -14
- package/src/cli/step-failure.ts +18 -0
- package/src/cloud-run.ts +9 -2
- package/src/communication/local.ts +3 -2
- package/src/communication/worker.ts +29 -11
- package/src/compile.ts +28 -13
- package/src/decimal-dollars.ts +38 -0
- package/src/effect-channel.ts +2 -0
- package/src/failure-kinds.ts +14 -0
- package/src/flow-extension-loader.ts +154 -14
- package/src/flow-extension-manifest.ts +6 -3
- package/src/helper-instruction.ts +2 -0
- package/src/hosted-extension-manifest.ts +28 -5
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/hosted-extension-sandbox.ts +2 -2
- package/src/journal-client.ts +14 -5
- package/src/llm-worker.ts +11 -4
- package/src/local-agent-environment.ts +92 -0
- package/src/local-agent.ts +2 -1
- package/src/mcp-stdio.ts +34 -20
- package/src/preflight.ts +2 -0
- package/src/protocol.ts +31 -0
- package/src/reported-cost-total.ts +82 -0
- package/src/reported-cost.ts +51 -0
- package/src/resolved-cli-identity.ts +42 -0
- package/src/run-state.ts +25 -37
- package/src/worker-cli.ts +126 -39
- package/src/worker-lease.ts +50 -9
- package/src/worker.ts +14 -5
- package/src/wrapper-session.ts +89 -56
- package/src/yaml-helper-effect.ts +2 -1
- package/src/yaml-helpers.ts +2 -1
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/failure-kinds.ts
CHANGED
|
@@ -144,6 +144,12 @@ export const RUN_FAILURE_KINDS = [
|
|
|
144
144
|
* attaches and before the resume touches the journal.
|
|
145
145
|
*/
|
|
146
146
|
'local_agent_unavailable',
|
|
147
|
+
/**
|
|
148
|
+
* The authored root's worker lease was lost and the kernel's retry did not
|
|
149
|
+
* reach this process. The run is NOT failed (`status: running`): its
|
|
150
|
+
* completed steps are journaled and `flows resume <rootRunId>` continues it.
|
|
151
|
+
*/
|
|
152
|
+
'root_lease_lost',
|
|
147
153
|
] as const;
|
|
148
154
|
|
|
149
155
|
/**
|
|
@@ -271,6 +277,14 @@ export interface StepFailedDetails {
|
|
|
271
277
|
* `retries_exhausted`.
|
|
272
278
|
*/
|
|
273
279
|
maxIterations?: number;
|
|
280
|
+
/**
|
|
281
|
+
* Attempts before this one that died without a result -- the kernel
|
|
282
|
+
* journaled them `crashed` or `lease_expired` itself -- and so, by design,
|
|
283
|
+
* consumed none of `maxIterations` (relayflowd-core recovery.rs). Present
|
|
284
|
+
* only when non-zero. Without it `attempt` can exceed `maxIterations`
|
|
285
|
+
* (`attempt=2/1`) and read as the kernel overrunning its own budget.
|
|
286
|
+
*/
|
|
287
|
+
unchargedAttempts?: number;
|
|
274
288
|
exitCode?: number;
|
|
275
289
|
/**
|
|
276
290
|
* Terminal-safe UTF-8 excerpt of the captured stdout, at most
|
|
@@ -13,7 +13,18 @@ import { pluginStoreDirectory, readStoredPluginFiles } from './plugin-store.js';
|
|
|
13
13
|
const JSON_PARSE = JSON.parse;
|
|
14
14
|
const ARRAY_IS_ARRAY = Array.isArray;
|
|
15
15
|
const JSON_STRINGIFY = JSON.stringify;
|
|
16
|
+
const NUMBER = Number;
|
|
17
|
+
const NUMBER_IS_FINITE = Number.isFinite.bind(Number);
|
|
18
|
+
const NUMBER_IS_SAFE_INTEGER = Number.isSafeInteger.bind(Number);
|
|
19
|
+
const NUMBER_TO_STRING = Function.prototype.call.bind(Number.prototype.toString) as (
|
|
20
|
+
value: number,
|
|
21
|
+
) => string;
|
|
22
|
+
const OBJECT_GET_OWN_PROPERTY_DESCRIPTOR = Object.getOwnPropertyDescriptor;
|
|
16
23
|
const OBJECT_FREEZE = Object.freeze;
|
|
24
|
+
const POSITIVE_INFINITY = Number.POSITIVE_INFINITY;
|
|
25
|
+
const REGEXP_EXEC = Function.prototype.call.bind(RegExp.prototype.exec) as (
|
|
26
|
+
regexp: RegExp, value: string,
|
|
27
|
+
) => RegExpExecArray | null;
|
|
17
28
|
const REGEXP_TEST = Function.prototype.call.bind(RegExp.prototype.test) as (
|
|
18
29
|
regexp: RegExp, value: string,
|
|
19
30
|
) => boolean;
|
|
@@ -74,12 +85,111 @@ export interface LoadFlowExtensionsOptions<Authority> {
|
|
|
74
85
|
|
|
75
86
|
const EXTENSION_HEADER_FIELDS = new Set(['budget', 'tools']);
|
|
76
87
|
const WALLCLOCK_MS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 } as const;
|
|
88
|
+
type StructuredFlowBudget = Exclude<NonNullable<AuthoredFlowDefinition['header']['budget']>, string>;
|
|
89
|
+
|
|
90
|
+
function ownBudgetField<K extends keyof StructuredFlowBudget>(
|
|
91
|
+
budget: StructuredFlowBudget,
|
|
92
|
+
field: K,
|
|
93
|
+
): StructuredFlowBudget[K] | undefined {
|
|
94
|
+
const descriptor = OBJECT_GET_OWN_PROPERTY_DESCRIPTOR(budget, field);
|
|
95
|
+
return descriptor !== undefined && 'value' in descriptor
|
|
96
|
+
? descriptor.value as StructuredFlowBudget[K]
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
77
99
|
|
|
78
100
|
function wallclockMs(value: string): number | undefined {
|
|
79
|
-
const match = /^(\d+)(ms|s|m|h|d)
|
|
101
|
+
const match = REGEXP_EXEC(/^(\d+)(ms|s|m|h|d)$/, value);
|
|
80
102
|
if (!match) return undefined;
|
|
81
103
|
const unit = match[2] as keyof typeof WALLCLOCK_MS;
|
|
82
|
-
|
|
104
|
+
const milliseconds = NUMBER(match[1]) * WALLCLOCK_MS[unit];
|
|
105
|
+
return NUMBER_IS_FINITE(milliseconds) ? milliseconds : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasBudgetCeiling(budget: StructuredFlowBudget): boolean {
|
|
109
|
+
return ownBudgetField(budget, 'tokens') !== undefined
|
|
110
|
+
|| ownBudgetField(budget, 'dollars') !== undefined
|
|
111
|
+
|| ownBudgetField(budget, 'wallclock') !== undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateStructuredBudget(budget: StructuredFlowBudget): void {
|
|
115
|
+
const tokens = ownBudgetField(budget, 'tokens');
|
|
116
|
+
if (tokens !== undefined && (!NUMBER_IS_SAFE_INTEGER(tokens) || tokens < 0)) {
|
|
117
|
+
throw new PluginError('plugin_incompatible', `Malformed token budget ceiling ${JSON_STRINGIFY(tokens)}.`);
|
|
118
|
+
}
|
|
119
|
+
const dollars = ownBudgetField(budget, 'dollars');
|
|
120
|
+
if (dollars !== undefined && (!NUMBER_IS_FINITE(dollars) || dollars < 0
|
|
121
|
+
|| REGEXP_EXEC(/^\d+(?:\.\d{1,6})?$/, NUMBER_TO_STRING(dollars)) === null)) {
|
|
122
|
+
throw new PluginError('plugin_incompatible', `Malformed dollar budget ceiling ${JSON_STRINGIFY(dollars)}.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function composeBudget(
|
|
127
|
+
base: AuthoredFlowDefinition['header']['budget'],
|
|
128
|
+
extensions: readonly LoadedFlowExtension[],
|
|
129
|
+
): AuthoredFlowDefinition['header']['budget'] {
|
|
130
|
+
type Budget = NonNullable<AuthoredFlowDefinition['header']['budget']>;
|
|
131
|
+
type StructuredBudget = Exclude<Budget, string>;
|
|
132
|
+
const effectiveBase = typeof base === 'object' && !hasBudgetCeiling(base)
|
|
133
|
+
? undefined
|
|
134
|
+
: base;
|
|
135
|
+
const ceilings: Budget[] = [];
|
|
136
|
+
for (let index = 0; index < extensions.length; index += 1) {
|
|
137
|
+
const extension = extensions[index]!;
|
|
138
|
+
const manifestBudget = extension.manifest.permissions.budget;
|
|
139
|
+
const entryBudget = extension.getDefinition(extension.handle).header.budget;
|
|
140
|
+
if (manifestBudget !== undefined
|
|
141
|
+
&& (typeof manifestBudget === 'string' || hasBudgetCeiling(manifestBudget))) {
|
|
142
|
+
appendIntrinsicArray(ceilings, manifestBudget);
|
|
143
|
+
}
|
|
144
|
+
if (entryBudget !== undefined
|
|
145
|
+
&& (typeof entryBudget === 'string' || hasBudgetCeiling(entryBudget))) {
|
|
146
|
+
appendIntrinsicArray(ceilings, entryBudget);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (ceilings.length === 0) return base;
|
|
150
|
+
if (effectiveBase === undefined && ceilings.length === 1 && typeof ceilings[0] === 'string') {
|
|
151
|
+
return ceilings[0];
|
|
152
|
+
}
|
|
153
|
+
let shorthand = typeof effectiveBase === 'string';
|
|
154
|
+
for (let index = 0; index < ceilings.length; index += 1) {
|
|
155
|
+
if (typeof ceilings[index] === 'string') shorthand = true;
|
|
156
|
+
}
|
|
157
|
+
if (shorthand) {
|
|
158
|
+
throw new PluginError('plugin_incompatible', 'A shorthand budget cannot compose with structured base-flow or extension budget ceilings.');
|
|
159
|
+
}
|
|
160
|
+
const budgets: StructuredBudget[] = [];
|
|
161
|
+
if (effectiveBase !== undefined) appendIntrinsicArray(budgets, effectiveBase as StructuredBudget);
|
|
162
|
+
for (let index = 0; index < ceilings.length; index += 1) {
|
|
163
|
+
appendIntrinsicArray(budgets, ceilings[index] as StructuredBudget);
|
|
164
|
+
}
|
|
165
|
+
let tokens: number | undefined;
|
|
166
|
+
let dollars: number | undefined;
|
|
167
|
+
let wallclock: string | undefined;
|
|
168
|
+
let wallclockLimit = POSITIVE_INFINITY;
|
|
169
|
+
for (let index = 0; index < budgets.length; index += 1) {
|
|
170
|
+
const budget = budgets[index]!;
|
|
171
|
+
const budgetTokens = ownBudgetField(budget, 'tokens');
|
|
172
|
+
const budgetDollars = ownBudgetField(budget, 'dollars');
|
|
173
|
+
const budgetWallclock = ownBudgetField(budget, 'wallclock');
|
|
174
|
+
validateStructuredBudget(budget);
|
|
175
|
+
if (budgetTokens !== undefined && (tokens === undefined || budgetTokens < tokens)) tokens = budgetTokens;
|
|
176
|
+
if (budgetDollars !== undefined && (dollars === undefined || budgetDollars < dollars)) dollars = budgetDollars;
|
|
177
|
+
if (budgetWallclock !== undefined) {
|
|
178
|
+
const candidate = wallclockMs(budgetWallclock);
|
|
179
|
+
if (candidate === undefined) {
|
|
180
|
+
throw new PluginError('plugin_incompatible', `Malformed wallclock budget ceiling ${JSON_STRINGIFY(budgetWallclock)}.`);
|
|
181
|
+
}
|
|
182
|
+
if (wallclock === undefined || candidate < wallclockLimit) {
|
|
183
|
+
wallclock = budgetWallclock;
|
|
184
|
+
wallclockLimit = candidate;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return OBJECT_FREEZE({
|
|
189
|
+
...(tokens === undefined ? {} : { tokens }),
|
|
190
|
+
...(dollars === undefined ? {} : { dollars }),
|
|
191
|
+
...(wallclock === undefined ? {} : { wallclock }),
|
|
192
|
+
});
|
|
83
193
|
}
|
|
84
194
|
|
|
85
195
|
/** Credentials and servers declared on a flow-extension, probed before the base body starts. */
|
|
@@ -253,14 +363,26 @@ async function loadOne<Authority>(
|
|
|
253
363
|
assertBaseCompatible(manifest, { name: base.definition.name, version: base.definition.header.version });
|
|
254
364
|
const baseBudget = base.definition.header.budget;
|
|
255
365
|
const ceiling = manifest.permissions.budget;
|
|
256
|
-
if (ceiling
|
|
257
|
-
throw new PluginError('plugin_incompatible', `${manifest.name} declares a
|
|
366
|
+
if (ceiling !== undefined && hasBudgetCeiling(ceiling) && typeof baseBudget === 'string') {
|
|
367
|
+
throw new PluginError('plugin_incompatible', `${manifest.name} declares a structured budget ceiling that cannot compose with the base flow's shorthand budget.`);
|
|
258
368
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
369
|
+
const ceilingTokens = ceiling === undefined ? undefined : ownBudgetField(ceiling, 'tokens');
|
|
370
|
+
const ceilingDollars = ceiling === undefined ? undefined : ownBudgetField(ceiling, 'dollars');
|
|
371
|
+
const ceilingWallclock = ceiling === undefined ? undefined : ownBudgetField(ceiling, 'wallclock');
|
|
372
|
+
const baseTokens = typeof baseBudget === 'object' ? ownBudgetField(baseBudget, 'tokens') : undefined;
|
|
373
|
+
const baseDollars = typeof baseBudget === 'object' ? ownBudgetField(baseBudget, 'dollars') : undefined;
|
|
374
|
+
const baseWallclock = typeof baseBudget === 'object' ? ownBudgetField(baseBudget, 'wallclock') : undefined;
|
|
375
|
+
if (ceilingTokens !== undefined && baseTokens !== undefined && ceilingTokens > baseTokens) {
|
|
376
|
+
throw new PluginError('plugin_incompatible', `${manifest.name} declares a ${ceilingTokens}-token budget ceiling above the base flow's ${baseTokens}-token ceiling.`);
|
|
377
|
+
}
|
|
378
|
+
if (ceilingDollars !== undefined && baseDollars !== undefined && ceilingDollars > baseDollars) {
|
|
379
|
+
throw new PluginError('plugin_incompatible', `${manifest.name} declares a $${ceilingDollars} budget ceiling above the base flow's $${baseDollars}.`);
|
|
380
|
+
}
|
|
381
|
+
if (ceilingWallclock !== undefined && baseWallclock !== undefined) {
|
|
382
|
+
const pluginMs = wallclockMs(ceilingWallclock);
|
|
383
|
+
const baseMs = wallclockMs(baseWallclock);
|
|
262
384
|
if (pluginMs !== undefined && baseMs !== undefined && pluginMs > baseMs) {
|
|
263
|
-
throw new PluginError('plugin_incompatible', `${manifest.name} declares a ${
|
|
385
|
+
throw new PluginError('plugin_incompatible', `${manifest.name} declares a ${ceilingWallclock} wallclock ceiling above the base flow's ${baseWallclock}.`);
|
|
264
386
|
}
|
|
265
387
|
}
|
|
266
388
|
const entryPath = join(directory, manifest.entry);
|
|
@@ -279,7 +401,9 @@ async function loadOne<Authority>(
|
|
|
279
401
|
if (!manifest.extends.handlers && definition.handlers.length > 0) {
|
|
280
402
|
throw new PluginError('plugin_manifest_invalid', `${manifest.name}: ${manifest.entry} declares handlers but extends.handlers is false.`);
|
|
281
403
|
}
|
|
282
|
-
|
|
404
|
+
for (let index = 0; index < definition.handlers.length; index += 1) {
|
|
405
|
+
assertDeclaredSubscription(manifest.name, manifest, definition.handlers[index]!, index);
|
|
406
|
+
}
|
|
283
407
|
const hooks = imported.hooks;
|
|
284
408
|
const exported = Object.keys(hooks).sort();
|
|
285
409
|
const declared = [...manifest.extends.hooks].sort();
|
|
@@ -292,9 +416,13 @@ async function loadOne<Authority>(
|
|
|
292
416
|
throw new PluginError('plugin_incompatible', `${manifest.name}: hook ${hook} is not declared by the base flow.`);
|
|
293
417
|
}
|
|
294
418
|
}
|
|
295
|
-
|
|
419
|
+
const handlers: TriggerHandler[] = [];
|
|
420
|
+
for (let index = 0; index < definition.handlers.length; index += 1) {
|
|
421
|
+
appendIntrinsicArray(handlers, definition.handlers[index]!);
|
|
422
|
+
}
|
|
423
|
+
return OBJECT_FREEZE({
|
|
296
424
|
name: manifest.name, version: manifest.version, ref, digest: lock.digest, directory, entryPath, manifest,
|
|
297
|
-
handle: imported.handle, getDefinition: imported.getDefinition, handlers:
|
|
425
|
+
handle: imported.handle, getDefinition: imported.getDefinition, handlers: OBJECT_FREEZE(handlers),
|
|
298
426
|
hooks,
|
|
299
427
|
});
|
|
300
428
|
}
|
|
@@ -329,11 +457,23 @@ export async function loadFlowExtensions<Authority>(
|
|
|
329
457
|
return Object.freeze(loaded);
|
|
330
458
|
}
|
|
331
459
|
|
|
332
|
-
/** The base definition with extension handlers appended
|
|
460
|
+
/** The base definition with extension handlers appended and every declared budget ceiling retained. */
|
|
333
461
|
export function composeDefinition<Input>(base: AuthoredFlowDefinition<Input>, extensions: readonly LoadedFlowExtension[]): AuthoredFlowDefinition<Input> {
|
|
334
462
|
if (extensions.length === 0) return base;
|
|
335
|
-
|
|
463
|
+
const budget = composeBudget(base.header.budget, extensions);
|
|
464
|
+
const handlers: TriggerHandler[] = [];
|
|
465
|
+
for (let index = 0; index < base.handlers.length; index += 1) appendIntrinsicArray(handlers, base.handlers[index]!);
|
|
466
|
+
for (let extensionIndex = 0; extensionIndex < extensions.length; extensionIndex += 1) {
|
|
467
|
+
const extensionHandlers = extensions[extensionIndex]!.handlers;
|
|
468
|
+
for (let handlerIndex = 0; handlerIndex < extensionHandlers.length; handlerIndex += 1) {
|
|
469
|
+
appendIntrinsicArray(handlers, extensionHandlers[handlerIndex]!);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return OBJECT_FREEZE({
|
|
336
473
|
...base,
|
|
337
|
-
|
|
474
|
+
header: budget === base.header.budget
|
|
475
|
+
? base.header
|
|
476
|
+
: OBJECT_FREEZE({ ...base.header, budget }),
|
|
477
|
+
handlers: OBJECT_FREEZE(handlers),
|
|
338
478
|
});
|
|
339
479
|
}
|
|
@@ -6,6 +6,7 @@ import { PluginError, pluginKindOf } from './plugin-manifest.js';
|
|
|
6
6
|
import { safePath } from './bundle.js';
|
|
7
7
|
import { SHA, parsePluginSource, type PluginSourceInput } from './plugin-source.js';
|
|
8
8
|
import { isVersionRange, parseVersion } from './semver-range.js';
|
|
9
|
+
import { parseBudget } from './budget.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Schema 2, `kind: "flow-extension"`: a plugin whose `entry` default-exports
|
|
@@ -29,7 +30,7 @@ export interface FlowExtensionPermissions {
|
|
|
29
30
|
readonly mcp: readonly string[];
|
|
30
31
|
/** Declared effect classes, shown for review; not enforced by this runtime (gate 8 / #442). */
|
|
31
32
|
readonly writes: readonly string[];
|
|
32
|
-
readonly budget?: { readonly dollars?: number; readonly wallclock?: string };
|
|
33
|
+
readonly budget?: { readonly tokens?: number; readonly dollars?: number; readonly wallclock?: string };
|
|
33
34
|
}
|
|
34
35
|
export interface FlowExtensionManifest {
|
|
35
36
|
readonly schema: 2;
|
|
@@ -117,10 +118,12 @@ function permissions(value: unknown): FlowExtensionPermissions {
|
|
|
117
118
|
for (const harness of harnesses) if (!(FLOW_HARNESSES as readonly string[]).includes(harness)) return invalid(`permissions.harnesses: unknown harness ${harness}.`);
|
|
118
119
|
let budget: FlowExtensionPermissions['budget'];
|
|
119
120
|
if (value.budget !== undefined) {
|
|
120
|
-
if (!object(value.budget) || Object.keys(value.budget).some(k => !['dollars', 'wallclock'].includes(k))) return invalid('permissions.budget expects dollars and/or wallclock.');
|
|
121
|
+
if (!object(value.budget) || Object.keys(value.budget).some(k => !['tokens', 'dollars', 'wallclock'].includes(k))) return invalid('permissions.budget expects tokens, dollars, and/or wallclock.');
|
|
122
|
+
if (value.budget.tokens !== undefined && (typeof value.budget.tokens !== 'number' || !Number.isSafeInteger(value.budget.tokens) || value.budget.tokens < 0)) return invalid('permissions.budget.tokens must be a non-negative safe integer.');
|
|
121
123
|
if (value.budget.dollars !== undefined && (typeof value.budget.dollars !== 'number' || !(value.budget.dollars > 0) || !Number.isFinite(value.budget.dollars))) return invalid('permissions.budget.dollars must be a positive number.');
|
|
122
124
|
if (value.budget.wallclock !== undefined && (typeof value.budget.wallclock !== 'string' || !WALLCLOCK.test(value.budget.wallclock))) return invalid('permissions.budget.wallclock must be a duration such as 45m.');
|
|
123
|
-
|
|
125
|
+
try { parseBudget(value.budget); } catch { return invalid('permissions.budget must use runtime budget syntax.'); }
|
|
126
|
+
budget = Object.freeze({ ...(value.budget.tokens === undefined ? {} : { tokens: value.budget.tokens }), ...(value.budget.dollars === undefined ? {} : { dollars: value.budget.dollars }), ...(value.budget.wallclock === undefined ? {} : { wallclock: value.budget.wallclock }) });
|
|
124
127
|
}
|
|
125
128
|
return Object.freeze({
|
|
126
129
|
integrations: stringList(value.integrations, 'permissions.integrations', PROVIDER),
|
|
@@ -12,6 +12,7 @@ import { PluginError } from './plugin-manifest.js';
|
|
|
12
12
|
|
|
13
13
|
const ARRAY_IS_ARRAY = Array.isArray;
|
|
14
14
|
const NUMBER_IS_FINITE = Number.isFinite;
|
|
15
|
+
const NUMBER_IS_SAFE_INTEGER = Number.isSafeInteger;
|
|
15
16
|
const OBJECT_CREATE = Object.create;
|
|
16
17
|
const OBJECT_FREEZE = Object.freeze;
|
|
17
18
|
const OBJECT_HAS_OWN = Object.hasOwn;
|
|
@@ -20,6 +21,10 @@ const REGEXP_TEST = Function.prototype.call.bind(RegExp.prototype.test) as (
|
|
|
20
21
|
regexp: RegExp,
|
|
21
22
|
value: string,
|
|
22
23
|
) => boolean;
|
|
24
|
+
const REGEXP_EXEC = Function.prototype.call.bind(RegExp.prototype.exec) as (
|
|
25
|
+
regexp: RegExp,
|
|
26
|
+
value: string,
|
|
27
|
+
) => RegExpExecArray | null;
|
|
23
28
|
const STRING_ENDS_WITH = Function.prototype.call.bind(String.prototype.endsWith) as (
|
|
24
29
|
value: string,
|
|
25
30
|
search: string,
|
|
@@ -38,7 +43,8 @@ const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
38
43
|
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
39
44
|
const PROVIDER = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
40
45
|
const WRITE_CLASS = /^[a-z0-9-]+(?::[a-z0-9_-]+)+$/;
|
|
41
|
-
const
|
|
46
|
+
const DOLLARS = /^\d+(?:\.\d{1,6})?$/;
|
|
47
|
+
const WALLCLOCK = /^(\d+)(ms|s|m|h|d)$/;
|
|
42
48
|
const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
43
49
|
const VERSION_RANGE = /^(?:\*|(?:[\^~]|>=)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?: <\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)?)$/;
|
|
44
50
|
const MAX_DESCRIPTION = 500;
|
|
@@ -168,19 +174,36 @@ function permissionsShape(value: unknown): FlowExtensionPermissions {
|
|
|
168
174
|
}
|
|
169
175
|
let budget: FlowExtensionPermissions['budget'];
|
|
170
176
|
if (value.budget !== undefined) {
|
|
171
|
-
if (!record(value.budget) || !hasOnlyKeys(value.budget, ['dollars', 'wallclock'])) {
|
|
172
|
-
return invalid('permissions.budget expects dollars and/or wallclock.');
|
|
177
|
+
if (!record(value.budget) || !hasOnlyKeys(value.budget, ['tokens', 'dollars', 'wallclock'])) {
|
|
178
|
+
return invalid('permissions.budget expects tokens, dollars, and/or wallclock.');
|
|
179
|
+
}
|
|
180
|
+
if (value.budget.tokens !== undefined
|
|
181
|
+
&& (typeof value.budget.tokens !== 'number'
|
|
182
|
+
|| !NUMBER_IS_SAFE_INTEGER(value.budget.tokens) || value.budget.tokens < 0)) {
|
|
183
|
+
return invalid('permissions.budget.tokens must be a non-negative safe integer.');
|
|
173
184
|
}
|
|
174
185
|
if (value.budget.dollars !== undefined
|
|
175
186
|
&& (typeof value.budget.dollars !== 'number' || value.budget.dollars <= 0
|
|
176
|
-
|| !NUMBER_IS_FINITE(value.budget.dollars)
|
|
187
|
+
|| !NUMBER_IS_FINITE(value.budget.dollars)
|
|
188
|
+
|| !matches(DOLLARS, `${value.budget.dollars}`))) {
|
|
177
189
|
return invalid('permissions.budget.dollars must be a positive number.');
|
|
178
190
|
}
|
|
179
191
|
if (value.budget.wallclock !== undefined
|
|
180
192
|
&& (typeof value.budget.wallclock !== 'string' || !matches(WALLCLOCK, value.budget.wallclock))) {
|
|
181
193
|
return invalid('permissions.budget.wallclock must be a duration such as 45m.');
|
|
182
194
|
}
|
|
183
|
-
|
|
195
|
+
if (value.budget.wallclock !== undefined) {
|
|
196
|
+
const match = REGEXP_EXEC(WALLCLOCK, value.budget.wallclock)!;
|
|
197
|
+
const multiplier = match[2] === 'ms' ? 1
|
|
198
|
+
: match[2] === 's' ? 1_000
|
|
199
|
+
: match[2] === 'm' ? 60_000
|
|
200
|
+
: match[2] === 'h' ? 3_600_000 : 86_400_000;
|
|
201
|
+
if (!NUMBER_IS_SAFE_INTEGER(+match[1]! * multiplier)) {
|
|
202
|
+
return invalid('permissions.budget.wallclock must fit the runtime duration range.');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const projected = OBJECT_CREATE(null) as { tokens?: number; dollars?: number; wallclock?: string };
|
|
206
|
+
if (value.budget.tokens !== undefined) projected.tokens = value.budget.tokens;
|
|
184
207
|
if (value.budget.dollars !== undefined) projected.dollars = value.budget.dollars;
|
|
185
208
|
if (value.budget.wallclock !== undefined) projected.wallclock = value.budget.wallclock;
|
|
186
209
|
budget = OBJECT_FREEZE(projected);
|
|
@@ -26,7 +26,7 @@ const REALPATH = realpath;
|
|
|
26
26
|
const PATH_DIRNAME = dirname;
|
|
27
27
|
const PATH_JOIN = join;
|
|
28
28
|
const PATH_RESOLVE = resolve;
|
|
29
|
-
const SOFTWARE_FACTORY_SHA256 = '
|
|
29
|
+
const SOFTWARE_FACTORY_SHA256 = '4339c0c45a4fc928092a3e32275c061000887ed95acdc2f898966c35aca91a2d';
|
|
30
30
|
const ARRAY_IS_ARRAY = Array.isArray;
|
|
31
31
|
const OBJECT_FREEZE = Object.freeze;
|
|
32
32
|
const WEAK_MAP_GET = Function.prototype.call.bind(WeakMap.prototype.get) as <K extends object, V>(
|
|
@@ -124,11 +124,11 @@ const NODE_EXECUTABLE = captureNodeExecutable();
|
|
|
124
124
|
const ADDRESS_SPACE_BYTES = 16 * 1024 * 1024 * 1024;
|
|
125
125
|
const DATA_BYTES = 3 * 1024 * 1024 * 1024;
|
|
126
126
|
const SURFACE_RUNTIME_SHA256 = OBJECT_FREEZE({
|
|
127
|
-
'flow.js': '
|
|
127
|
+
'flow.js': '2ba972849bf435bba36b918c348efeab71ec34d180cec7a722e535f482687a6a',
|
|
128
128
|
'helpers/providers.js': '4eb06d0d85ca0a3434bb2dbba7407e3d95eef2dfbedaeb0e2de0c0c0ea812457',
|
|
129
129
|
'provider-trigger.js': 'e2664c65397f93fb486eb6f1e756c7cec3f88b3851d79c23567cad986f80f1ff',
|
|
130
130
|
'schedule.js': '8fe72f176a75ec0b5f26e12db7a597575c259a2e2cbb59690f9dc20a5e63940b',
|
|
131
|
-
'triggers.js': '
|
|
131
|
+
'triggers.js': '421609a6ea6d2ea77051dd4826a32e773b7db81dedd1dde91f4187b382cd46e3',
|
|
132
132
|
'triggers/github.js': 'e312994320f82aad0af00d09c504175d929cc6c601dfe1bc522632462ad9a48b',
|
|
133
133
|
});
|
|
134
134
|
const SURFACE_PACKAGE_JSON = '{"name":"@relayflows/surface","type":"module","exports":{".":"./index.js","./runtime":"./runtime.js"}}';
|
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,10 +1,11 @@
|
|
|
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';
|
|
5
6
|
import type { JournalClient } from './journal-client.js';
|
|
6
7
|
import type { CompletionReason, StepDispatchEvent } from './protocol.js';
|
|
7
|
-
import type {
|
|
8
|
+
import type { ResolvedKernelLlmStep } from './resolved-cli-identity.js';
|
|
8
9
|
import { runAgentCli } from './worker-cli.js';
|
|
9
10
|
import { resolveCliModel } from './cli-adapter.js';
|
|
10
11
|
import { withWorkerLease } from './worker-lease.js';
|
|
@@ -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
|
}
|
|
@@ -52,16 +54,20 @@ export class LlmWorker extends EventEmitter {
|
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
private async execute(dispatch: StepDispatchEvent): Promise<void> {
|
|
55
|
-
const spec = dispatch.spec as
|
|
57
|
+
const spec = dispatch.spec as ResolvedKernelLlmStep;
|
|
56
58
|
const schema = spec.verification?.json_schema;
|
|
57
59
|
const prompt = schema === undefined ? spec.prompt
|
|
58
60
|
: `${spec.prompt}\n\nReturn only a JSON value matching this JSON Schema (no Markdown fences):\n${JSON.stringify(schema)}`;
|
|
59
|
-
const effectiveModel = typeof spec.cli === 'string'
|
|
61
|
+
const effectiveModel = typeof spec.cli === 'string'
|
|
62
|
+
? resolveCliModel(spec.cli_identity ?? spec.cli, spec.model)
|
|
63
|
+
: spec.model;
|
|
60
64
|
const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
|
|
61
65
|
typeof spec.cli === 'string' && typeof spec.prompt === 'string'
|
|
62
|
-
? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'llm'
|
|
66
|
+
? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'llm',
|
|
67
|
+
undefined, undefined, 'direct', undefined, this.environment, spec.cli_identity)
|
|
63
68
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'llm step has no declared CLI' }));
|
|
64
69
|
const { result, usage } = workerSpend(completed, effectiveModel);
|
|
70
|
+
const cost = reportedCost(completed, effectiveModel);
|
|
65
71
|
let reason: CompletionReason = result.exit_code === 0 ? 'success' : 'worker_error';
|
|
66
72
|
let output: unknown = result.stdout_tail;
|
|
67
73
|
let detail = result.stderr_tail;
|
|
@@ -94,6 +100,7 @@ export class LlmWorker extends EventEmitter {
|
|
|
94
100
|
dispatch.idempotency_key, reason, {
|
|
95
101
|
output,
|
|
96
102
|
...(usage !== undefined ? { usage } : {}),
|
|
103
|
+
...(cost === undefined ? {} : { reported_cost: cost }),
|
|
97
104
|
...(Object.keys(trajectoryTail).length === 0 ? {} : { trajectory_tail: trajectoryTail }),
|
|
98
105
|
});
|
|
99
106
|
}
|
|
@@ -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
|
});
|