@relayflows/sdk 2.0.35 → 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/adapters/base.d.ts +16 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +15 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +4 -0
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/usage-limit.d.ts +2 -0
- package/dist/adapters/usage-limit.d.ts.map +1 -0
- package/dist/adapters/usage-limit.js +17 -0
- package/dist/adapters/usage-limit.js.map +1 -0
- package/dist/authored-activity.d.ts +17 -0
- package/dist/authored-activity.d.ts.map +1 -0
- package/dist/authored-activity.js +218 -0
- package/dist/authored-activity.js.map +1 -0
- package/dist/authored-child-run.d.ts +34 -0
- package/dist/authored-child-run.d.ts.map +1 -0
- package/dist/authored-child-run.js +128 -0
- package/dist/authored-child-run.js.map +1 -0
- package/dist/authored-flow-error.d.ts +32 -3
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +3 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +11 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +32 -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 +4 -2
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.js +9 -1
- 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 +42 -9
- 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 +4 -2
- 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 +12 -7
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +24 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +24 -8
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +81 -15
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-subscription-park.d.ts +5 -0
- package/dist/authored-subscription-park.d.ts.map +1 -0
- package/dist/authored-subscription-park.js +47 -0
- package/dist/authored-subscription-park.js.map +1 -0
- 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 +9 -4
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-activities.d.ts +8 -0
- package/dist/cli/check-activities.d.ts.map +1 -0
- package/dist/cli/check-activities.js +155 -0
- package/dist/cli/check-activities.js.map +1 -0
- 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 +22 -9
- 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 +17 -4
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +18 -5
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +37 -12
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/subscription-report.d.ts +5 -0
- package/dist/cli/subscription-report.d.ts.map +1 -0
- package/dist/cli/subscription-report.js +48 -0
- package/dist/cli/subscription-report.js.map +1 -0
- package/dist/cli-adapter.d.ts +4 -2
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +4 -0
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -5
- package/dist/cli.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/failure-kinds.d.ts +2 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +21 -3
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +45 -6
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-reader.d.ts.map +1 -1
- package/dist/journal-reader.js +2 -0
- package/dist/journal-reader.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 +4 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +15 -5
- package/dist/local-agent.js.map +1 -1
- package/dist/preflight.d.ts +6 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +36 -10
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +187 -1
- 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/step-settling.d.ts +11 -0
- package/dist/step-settling.d.ts.map +1 -0
- package/dist/step-settling.js +22 -0
- package/dist/step-settling.js.map +1 -0
- 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/adapters/base.ts +19 -0
- package/src/adapters/claude.ts +15 -0
- package/src/adapters/codex.ts +6 -0
- package/src/adapters/usage-limit.ts +17 -0
- package/src/authored-activity.ts +251 -0
- package/src/authored-child-run.ts +138 -0
- package/src/authored-flow-error.ts +33 -0
- package/src/authored-flow-executor.ts +44 -1
- package/src/authored-helper-effect.ts +3 -2
- package/src/authored-mcp.ts +4 -2
- package/src/authored-node-entry.ts +11 -2
- package/src/authored-node-runner.ts +43 -9
- package/src/authored-plugin-effect.ts +4 -2
- package/src/authored-preflight.ts +2 -2
- package/src/authored-root.ts +34 -4
- package/src/authored-step-output.ts +88 -15
- package/src/authored-subscription-park.ts +46 -0
- package/src/authored-verdict.ts +2 -1
- package/src/authored-worker-step.ts +9 -3
- package/src/cli/check-activities.ts +144 -0
- package/src/cli/check.ts +6 -3
- package/src/cli/cli-probe.ts +23 -9
- package/src/cli/direct-run.ts +16 -1
- package/src/cli/run.ts +59 -14
- package/src/cli/subscription-report.ts +51 -0
- package/src/cli-adapter.ts +8 -2
- package/src/cli.ts +13 -6
- 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/failure-kinds.ts +2 -0
- package/src/helper-instruction.ts +2 -0
- package/src/index.ts +6 -0
- package/src/journal-client.ts +60 -7
- package/src/journal-reader.ts +2 -0
- package/src/llm-worker.ts +6 -1
- package/src/local-agent-environment.ts +92 -0
- package/src/local-agent.ts +19 -4
- package/src/preflight.ts +46 -10
- package/src/protocol.ts +151 -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/step-settling.ts +19 -0
- 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,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
|
@@ -2,6 +2,8 @@ import { onWorkerFailure } from './worker-lease.js';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import type { JournalClient } from './journal-client.js';
|
|
4
4
|
import { AgentWorker } from './worker.js';
|
|
5
|
+
import type { KernelRunSpec } from './spec.js';
|
|
6
|
+
import { communicationInstruction } from './communication/spec.js';
|
|
5
7
|
import { DEFAULT_LOCAL_AGENT_CAPACITY } from './worker-slots.js';
|
|
6
8
|
|
|
7
9
|
/** A local worker for stream-only steps; no workspace recovery is claimed. */
|
|
@@ -12,13 +14,17 @@ export async function attachLocalAgent(
|
|
|
12
14
|
requestedStream?: string,
|
|
13
15
|
capacity: number = DEFAULT_LOCAL_AGENT_CAPACITY,
|
|
14
16
|
runRoot?: string,
|
|
17
|
+
declaredStreams: readonly string[] = [],
|
|
18
|
+
environment?: NodeJS.ProcessEnv,
|
|
15
19
|
): Promise<{
|
|
16
20
|
stream: string;
|
|
17
21
|
readonly failure: unknown;
|
|
18
22
|
close(): Promise<void>;
|
|
19
23
|
}> {
|
|
20
|
-
//
|
|
21
|
-
//
|
|
24
|
+
// This worker has consumed no messages: its read offsets are zero, including
|
|
25
|
+
// named declarative streams. The kernel retains ownership of dispatch pins
|
|
26
|
+
// and rejects a resume whose required offsets this worker does not hold.
|
|
27
|
+
// No worktree revision is invented.
|
|
22
28
|
const stream = requestedStream ?? `local-agent-${randomUUID()}`;
|
|
23
29
|
const worker = new AgentWorker(client, {
|
|
24
30
|
workerId: stream,
|
|
@@ -26,8 +32,9 @@ export async function attachLocalAgent(
|
|
|
26
32
|
// second parking behind the first. Authored bodies size their admission to
|
|
27
33
|
// this same number (worker-slots.ts), so they never ask for more.
|
|
28
34
|
capacity,
|
|
29
|
-
dataDir, onPtyReady, runRoot,
|
|
30
|
-
pins: { workspace: [], streams: [
|
|
35
|
+
dataDir, onPtyReady, runRoot, environment,
|
|
36
|
+
pins: { workspace: [], streams: [...new Set([stream, ...declaredStreams])]
|
|
37
|
+
.map(stream => ({ stream, read_offset: 0 })) },
|
|
31
38
|
});
|
|
32
39
|
let failure: unknown;
|
|
33
40
|
// The cause travels with the close, so the flow's next request names it.
|
|
@@ -41,3 +48,11 @@ export async function attachLocalAgent(
|
|
|
41
48
|
},
|
|
42
49
|
};
|
|
43
50
|
}
|
|
51
|
+
|
|
52
|
+
/** Conversation workers own their streams and registration; do not steal their dispatches. */
|
|
53
|
+
export function declaredLocalAgentStreams(spec: KernelRunSpec): string[] {
|
|
54
|
+
return [...new Set(spec.steps.flatMap(step =>
|
|
55
|
+
step.type === 'agent' && !communicationInstruction(step.instruction)
|
|
56
|
+
? step.surfaces?.streams?.map(({ stream }) => stream) ?? []
|
|
57
|
+
: []))];
|
|
58
|
+
}
|
package/src/preflight.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface CliProbeResult {
|
|
|
52
52
|
* unauthenticated CLI.
|
|
53
53
|
*/
|
|
54
54
|
authFailureDetail?: string;
|
|
55
|
+
/** Exit code of a FAILED model-scoped readiness probe. */
|
|
56
|
+
modelExitCode?: number | null;
|
|
57
|
+
/** Cause the adapter read from a failed readiness probe's output, if any. */
|
|
58
|
+
modelFailure?: import('./adapters/base.js').ModelProbeFailure;
|
|
59
|
+
/** Redacted output of a FAILED model-scoped readiness probe. */
|
|
60
|
+
modelFailureDetail?: string;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export type CliProbeFailureDetail =
|
|
@@ -603,17 +609,47 @@ function probeResolvedCli(
|
|
|
603
609
|
message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}`,
|
|
604
610
|
});
|
|
605
611
|
} else if (resolution.model !== undefined && result.modelAvailable !== true) {
|
|
606
|
-
diagnostics.push({
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
612
|
+
diagnostics.push(modelProbeRefusal(resolution as CliResolution & { model: string }, result));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Only a probe whose output says the model is unknown (or the adapter gave no
|
|
617
|
+
// output to read) is told to check the model name. An outdated CLI and a
|
|
618
|
+
// usage-limited credential name a different remedy, and "verify the model
|
|
619
|
+
// name" sends the author after a typo that is not there.
|
|
620
|
+
function modelProbeRefusal(
|
|
621
|
+
resolution: CliResolution & { model: string },
|
|
622
|
+
result: CliProbeResult,
|
|
623
|
+
): PreflightDiagnostic {
|
|
624
|
+
const { stepId, cli, model } = resolution;
|
|
625
|
+
const effective = describeEffectiveModel(stepId, model, cli, resolution.modelSource);
|
|
626
|
+
const command = `"${result.modelCommand ?? `${cli} auth status`}"`;
|
|
627
|
+
const exit = result.modelExitCode === undefined || result.modelExitCode === null ? '' : ` (exit ${result.modelExitCode})`;
|
|
628
|
+
const reported = result.modelFailureDetail === undefined ? '' : ` It reported: ${result.modelFailureDetail}`;
|
|
629
|
+
const base = { severity: 'refusal' as const, stepId, cli, model };
|
|
630
|
+
const cause = result.modelFailure?.cause;
|
|
631
|
+
if (cause === 'cli_outdated') {
|
|
632
|
+
const required = result.modelFailure?.requiredVersion;
|
|
633
|
+
return { ...base, kind: 'cli_outdated',
|
|
634
|
+
message: `${effective}, but this "${cli}" is too old to use that model`
|
|
635
|
+
+ (required === undefined ? '' : `; version ${required} or newer is required`)
|
|
636
|
+
+ `. Upgrade the CLI in the environment that runs this flow; the model name is not the problem.${reported}` };
|
|
637
|
+
}
|
|
638
|
+
if (cause === 'provider_usage_limited') {
|
|
639
|
+
return { ...base, kind: 'provider_usage_limited',
|
|
640
|
+
message: `${effective}, but the provider refused the model-scoped ${command} probe${exit} because this credential`
|
|
641
|
+
+ ` has reached a usage or rate limit. Retry after the limit resets or use a credential with capacity;`
|
|
642
|
+
+ ` the model name is not the problem.${reported}` };
|
|
616
643
|
}
|
|
644
|
+
// A probe that printed nothing is judged on its exit code alone, which is
|
|
645
|
+
// how a wrapper refuses a model it cannot serve.
|
|
646
|
+
const unknownModel = cause === 'model_unknown' || result.modelFailureDetail === undefined;
|
|
647
|
+
return { ...base, kind: 'model_unavailable',
|
|
648
|
+
message: `${effective}, but its model-scoped ${command} probe exited non-zero${exit}`
|
|
649
|
+
+ (unknownModel
|
|
650
|
+
? `; verify the model name and this credential's access.`
|
|
651
|
+
: ` for a reason Relayflows does not recognise.`)
|
|
652
|
+
+ reported };
|
|
617
653
|
}
|
|
618
654
|
|
|
619
655
|
function probeFailedMessage(
|
package/src/protocol.ts
CHANGED
|
@@ -53,8 +53,16 @@ export type Verb =
|
|
|
53
53
|
| 'effect.confirm'
|
|
54
54
|
| 'step.complete'
|
|
55
55
|
| 'step.wait'
|
|
56
|
+
| 'subscription.park'
|
|
56
57
|
| 'event.emit'
|
|
57
58
|
| 'event.submit'
|
|
59
|
+
| 'subscription.open'
|
|
60
|
+
| 'subscription.activate'
|
|
61
|
+
| 'subscription.deliver'
|
|
62
|
+
| 'subscription.inspect'
|
|
63
|
+
| 'subscription.fence_overflow'
|
|
64
|
+
| 'subscription.next'
|
|
65
|
+
| 'subscription.close'
|
|
58
66
|
| 'channel.append'
|
|
59
67
|
| 'channel.receive'
|
|
60
68
|
| 'channel.ack'
|
|
@@ -71,6 +79,11 @@ export interface HelloParams {
|
|
|
71
79
|
export interface HelloResult {
|
|
72
80
|
protocol: 0;
|
|
73
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[];
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
export interface StepSpend {
|
|
@@ -89,6 +102,21 @@ export interface StepSpend {
|
|
|
89
102
|
* `dollars_unmetered: true` and no dollar amount, so unknown cost is never
|
|
90
103
|
* journaled as a measured $0. See `workerSpend` for the full contract.
|
|
91
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
|
+
|
|
92
120
|
export type StepUsage =
|
|
93
121
|
| { tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: never }
|
|
94
122
|
| { tokens_in: number; tokens_out: number; dollars_unmetered: true; dollars?: never };
|
|
@@ -334,6 +362,8 @@ export interface StepCompleteParams {
|
|
|
334
362
|
effects?: EffectRef[];
|
|
335
363
|
/** `inspect` evidence for the next attempt. Rejected over 16 KiB of JSON. */
|
|
336
364
|
trajectory_tail?: unknown;
|
|
365
|
+
/** Display-only actual cost; never charged to the budget. */
|
|
366
|
+
reported_cost?: ReportedCost;
|
|
337
367
|
}
|
|
338
368
|
export type StepCompleteResult = RunOutcome;
|
|
339
369
|
|
|
@@ -362,6 +392,10 @@ export interface EventEmitParams {
|
|
|
362
392
|
run_id: string;
|
|
363
393
|
event_key: string;
|
|
364
394
|
payload: unknown;
|
|
395
|
+
/** Provider delivery id for body activities. Required by Cloud; optional for legacy exact waits. */
|
|
396
|
+
delivery_id?: string;
|
|
397
|
+
/** Provider actor identity, used by the local router adapter for self filtering. */
|
|
398
|
+
actor?: string;
|
|
365
399
|
}
|
|
366
400
|
export interface EventEmitResult {
|
|
367
401
|
matched: number;
|
|
@@ -388,6 +422,111 @@ export interface EventSubmitResult {
|
|
|
388
422
|
run?: unknown;
|
|
389
423
|
}
|
|
390
424
|
|
|
425
|
+
/** Body-level activity open; the server acknowledges only after its fenced binding is durable. */
|
|
426
|
+
export interface SubscriptionOpenParams {
|
|
427
|
+
run_id: string;
|
|
428
|
+
subscription_id: string;
|
|
429
|
+
event_types: string[];
|
|
430
|
+
pattern?: Record<string, unknown>;
|
|
431
|
+
settle_ms: number;
|
|
432
|
+
idle_ms: number;
|
|
433
|
+
deadline_ms: number;
|
|
434
|
+
include_self: boolean;
|
|
435
|
+
}
|
|
436
|
+
export type SubscriptionOpenResult =
|
|
437
|
+
| {
|
|
438
|
+
/** Immutable snapshot from the durable `subscription.prepared` entry. */
|
|
439
|
+
state: 'prepared';
|
|
440
|
+
subscription_id: string;
|
|
441
|
+
event_types: string[];
|
|
442
|
+
pattern?: Record<string, unknown>;
|
|
443
|
+
stream: string;
|
|
444
|
+
settle_ms: number;
|
|
445
|
+
idle_ms: number;
|
|
446
|
+
deadline_at_ms: number;
|
|
447
|
+
include_self: boolean;
|
|
448
|
+
}
|
|
449
|
+
| {
|
|
450
|
+
state: 'active';
|
|
451
|
+
subscription_id: string;
|
|
452
|
+
stream: string;
|
|
453
|
+
deadline_at_ms: number;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
/** Cloud supplies this receipt only after it has durably fenced its binding. */
|
|
457
|
+
export interface SubscriptionActivateParams {
|
|
458
|
+
run_id: string;
|
|
459
|
+
subscription_id: string;
|
|
460
|
+
ingress_offset: number;
|
|
461
|
+
router_binding: Record<string, unknown>;
|
|
462
|
+
}
|
|
463
|
+
export interface SubscriptionActivateResult {
|
|
464
|
+
state: 'active';
|
|
465
|
+
subscription_id: string;
|
|
466
|
+
stream: string;
|
|
467
|
+
deadline_at_ms: number;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Targeted Cloud ingress; receipt must equal the journaled activation receipt. */
|
|
471
|
+
export interface SubscriptionDeliverParams {
|
|
472
|
+
run_id: string;
|
|
473
|
+
subscription_id: string;
|
|
474
|
+
router_binding: Record<string, unknown>;
|
|
475
|
+
delivery_id: string;
|
|
476
|
+
frame: unknown;
|
|
477
|
+
}
|
|
478
|
+
export interface SubscriptionDeliverResult {
|
|
479
|
+
/** False for an idempotent duplicate or a frame that closes on overflow. */
|
|
480
|
+
appended: boolean;
|
|
481
|
+
reason?: 'duplicate' | 'overflow';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Read-only projection of the kernel's durable subscription records. */
|
|
485
|
+
export interface SubscriptionSnapshot {
|
|
486
|
+
subscriptionId: string;
|
|
487
|
+
state: 'prepared' | 'active' | 'closed';
|
|
488
|
+
completionReason?: 'closed' | 'run_completed' | 'canceled' | 'deadline' | 'overflow';
|
|
489
|
+
routerBinding?: Record<string, unknown>;
|
|
490
|
+
ingressOffset?: number;
|
|
491
|
+
unreadFrames: number;
|
|
492
|
+
unreadBytes: number;
|
|
493
|
+
settleMs: number;
|
|
494
|
+
idleAtMs?: number;
|
|
495
|
+
deadlineAtMs: number;
|
|
496
|
+
}
|
|
497
|
+
export interface SubscriptionInspectParams { run_id: string }
|
|
498
|
+
export interface SubscriptionInspectResult { subscriptions: SubscriptionSnapshot[] }
|
|
499
|
+
export interface SubscriptionFenceOverflowParams {
|
|
500
|
+
run_id: string;
|
|
501
|
+
subscription_id: string;
|
|
502
|
+
router_binding: Record<string, unknown>;
|
|
503
|
+
}
|
|
504
|
+
export interface SubscriptionFenceOverflowResult { fenced: true }
|
|
505
|
+
|
|
506
|
+
export interface SubscriptionNextParams {
|
|
507
|
+
run_id: string;
|
|
508
|
+
subscription_id: string;
|
|
509
|
+
/** Internal durable receipt for the prior normal wake, supplied with the next pull. */
|
|
510
|
+
acknowledge_wait_id?: string;
|
|
511
|
+
/** Body call ordinal: replay this pull's durable wake across process restarts. */
|
|
512
|
+
sequence?: number;
|
|
513
|
+
}
|
|
514
|
+
export type SubscriptionNextResult =
|
|
515
|
+
| { kind: 'suspended'; subscription_id: string; stream: string; deadline_at_ms: number }
|
|
516
|
+
| { kind: 'events'; events: unknown[]; offset: number; acknowledge_wait_id?: string }
|
|
517
|
+
| { kind: 'idle'; acknowledge_wait_id?: string }
|
|
518
|
+
| { kind: 'deadline'; pending: { from: number; to: number } | null }
|
|
519
|
+
| { kind: 'overflow'; retained: number; bytes: number; from: number };
|
|
520
|
+
|
|
521
|
+
export interface SubscriptionCloseParams {
|
|
522
|
+
run_id: string;
|
|
523
|
+
subscription_id: string;
|
|
524
|
+
completion_reason: 'closed' | 'run_completed' | 'canceled';
|
|
525
|
+
}
|
|
526
|
+
export interface SubscriptionCloseResult {
|
|
527
|
+
closed: string;
|
|
528
|
+
}
|
|
529
|
+
|
|
391
530
|
export interface StreamAppendParams {
|
|
392
531
|
run_id: string;
|
|
393
532
|
stream: string;
|
|
@@ -441,8 +580,20 @@ export interface VerbContract {
|
|
|
441
580
|
'effect.confirm': { params: EffectConfirmParams; result: EffectConfirmResult };
|
|
442
581
|
'step.complete': { params: StepCompleteParams; result: StepCompleteResult };
|
|
443
582
|
'step.wait': { params: StepWaitParams; result: StepWaitResult };
|
|
583
|
+
'subscription.park': {
|
|
584
|
+
params: { run_id: string; step_id: string; attempt: number; idempotency_key: string;
|
|
585
|
+
subscription_id: string; phase: 'activation' | 'event_wait' };
|
|
586
|
+
result: RunOutcome;
|
|
587
|
+
};
|
|
444
588
|
'event.emit': { params: EventEmitParams; result: EventEmitResult };
|
|
445
589
|
'event.submit': { params: EventSubmitParams; result: EventSubmitResult };
|
|
590
|
+
'subscription.open': { params: SubscriptionOpenParams; result: SubscriptionOpenResult };
|
|
591
|
+
'subscription.activate': { params: SubscriptionActivateParams; result: SubscriptionActivateResult };
|
|
592
|
+
'subscription.deliver': { params: SubscriptionDeliverParams; result: SubscriptionDeliverResult };
|
|
593
|
+
'subscription.inspect': { params: SubscriptionInspectParams; result: SubscriptionInspectResult };
|
|
594
|
+
'subscription.fence_overflow': { params: SubscriptionFenceOverflowParams; result: SubscriptionFenceOverflowResult };
|
|
595
|
+
'subscription.next': { params: SubscriptionNextParams; result: SubscriptionNextResult };
|
|
596
|
+
'subscription.close': { params: SubscriptionCloseParams; result: SubscriptionCloseResult };
|
|
446
597
|
'stream.append': { params: StreamAppendParams; result: StreamAppendResult };
|
|
447
598
|
'stream.read': { params: StreamReadParams; result: StreamReadResult };
|
|
448
599
|
'journal.read': { params: JournalReadParams; result: JournalReadResult };
|
|
@@ -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
|
+
}
|