@relayflows/sdk 2.0.11 → 2.0.12

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.
Files changed (156) hide show
  1. package/dist/adapters/base.d.ts +6 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +1 -0
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/authored-admission.d.ts +5 -0
  7. package/dist/authored-admission.d.ts.map +1 -0
  8. package/dist/authored-admission.js +14 -0
  9. package/dist/authored-admission.js.map +1 -0
  10. package/dist/authored-budget.d.ts +1 -1
  11. package/dist/authored-budget.d.ts.map +1 -1
  12. package/dist/authored-budget.js +3 -3
  13. package/dist/authored-budget.js.map +1 -1
  14. package/dist/authored-flow-error.d.ts +1 -1
  15. package/dist/authored-flow-error.d.ts.map +1 -1
  16. package/dist/authored-flow-error.js.map +1 -1
  17. package/dist/authored-flow-executor.d.ts +10 -0
  18. package/dist/authored-flow-executor.d.ts.map +1 -1
  19. package/dist/authored-flow-executor.js +10 -7
  20. package/dist/authored-flow-executor.js.map +1 -1
  21. package/dist/authored-flow-loader.d.ts +20 -0
  22. package/dist/authored-flow-loader.d.ts.map +1 -1
  23. package/dist/authored-flow-loader.js +86 -11
  24. package/dist/authored-flow-loader.js.map +1 -1
  25. package/dist/authored-helper-effect.d.ts +1 -1
  26. package/dist/authored-helper-effect.d.ts.map +1 -1
  27. package/dist/authored-helper-effect.js +5 -3
  28. package/dist/authored-helper-effect.js.map +1 -1
  29. package/dist/authored-mcp.d.ts +1 -1
  30. package/dist/authored-mcp.d.ts.map +1 -1
  31. package/dist/authored-mcp.js +7 -4
  32. package/dist/authored-mcp.js.map +1 -1
  33. package/dist/authored-node-entry.d.ts +2 -0
  34. package/dist/authored-node-entry.d.ts.map +1 -0
  35. package/dist/authored-node-entry.js +111 -0
  36. package/dist/authored-node-entry.js.map +1 -0
  37. package/dist/authored-node-runner.d.ts +11 -0
  38. package/dist/authored-node-runner.d.ts.map +1 -0
  39. package/dist/authored-node-runner.js +246 -0
  40. package/dist/authored-node-runner.js.map +1 -0
  41. package/dist/authored-plugin-effect.d.ts +1 -1
  42. package/dist/authored-plugin-effect.d.ts.map +1 -1
  43. package/dist/authored-plugin-effect.js +7 -4
  44. package/dist/authored-plugin-effect.js.map +1 -1
  45. package/dist/authored-root.d.ts +38 -0
  46. package/dist/authored-root.d.ts.map +1 -0
  47. package/dist/authored-root.js +329 -0
  48. package/dist/authored-root.js.map +1 -0
  49. package/dist/authored-runtime-capability.d.ts +6 -0
  50. package/dist/authored-runtime-capability.d.ts.map +1 -0
  51. package/dist/authored-runtime-capability.js +40 -0
  52. package/dist/authored-runtime-capability.js.map +1 -0
  53. package/dist/authored-source-authority.d.ts +4 -0
  54. package/dist/authored-source-authority.d.ts.map +1 -0
  55. package/dist/authored-source-authority.js +61 -0
  56. package/dist/authored-source-authority.js.map +1 -0
  57. package/dist/authored-worker-step.d.ts +2 -2
  58. package/dist/authored-worker-step.d.ts.map +1 -1
  59. package/dist/authored-worker-step.js +11 -6
  60. package/dist/authored-worker-step.js.map +1 -1
  61. package/dist/budget-preflight.d.ts +17 -3
  62. package/dist/budget-preflight.d.ts.map +1 -1
  63. package/dist/budget-preflight.js +31 -24
  64. package/dist/budget-preflight.js.map +1 -1
  65. package/dist/cli/check.js +3 -1
  66. package/dist/cli/check.js.map +1 -1
  67. package/dist/cli/direct-run.d.ts.map +1 -1
  68. package/dist/cli/direct-run.js +18 -14
  69. package/dist/cli/direct-run.js.map +1 -1
  70. package/dist/cli/run.d.ts +2 -0
  71. package/dist/cli/run.d.ts.map +1 -1
  72. package/dist/cli/run.js +56 -1
  73. package/dist/cli/run.js.map +1 -1
  74. package/dist/cli-adapter.d.ts +16 -0
  75. package/dist/cli-adapter.d.ts.map +1 -1
  76. package/dist/cli-adapter.js +17 -0
  77. package/dist/cli-adapter.js.map +1 -1
  78. package/dist/cli.js +4 -4
  79. package/dist/cli.js.map +1 -1
  80. package/dist/cloud-run.d.ts +11 -1
  81. package/dist/cloud-run.d.ts.map +1 -1
  82. package/dist/cloud-run.js +65 -19
  83. package/dist/cloud-run.js.map +1 -1
  84. package/dist/failure-kinds.d.ts +7 -3
  85. package/dist/failure-kinds.d.ts.map +1 -1
  86. package/dist/failure-kinds.js +5 -1
  87. package/dist/failure-kinds.js.map +1 -1
  88. package/dist/journal-client.d.ts +1 -1
  89. package/dist/journal-client.d.ts.map +1 -1
  90. package/dist/journal-client.js +6 -2
  91. package/dist/journal-client.js.map +1 -1
  92. package/dist/llm-worker.d.ts.map +1 -1
  93. package/dist/llm-worker.js +4 -2
  94. package/dist/llm-worker.js.map +1 -1
  95. package/dist/local-agent.d.ts +1 -1
  96. package/dist/local-agent.d.ts.map +1 -1
  97. package/dist/local-agent.js +2 -2
  98. package/dist/local-agent.js.map +1 -1
  99. package/dist/model-pricing.d.ts +3 -4
  100. package/dist/model-pricing.d.ts.map +1 -1
  101. package/dist/model-pricing.js +4 -4
  102. package/dist/model-pricing.js.map +1 -1
  103. package/dist/preflight.d.ts +4 -1
  104. package/dist/preflight.d.ts.map +1 -1
  105. package/dist/preflight.js +36 -18
  106. package/dist/preflight.js.map +1 -1
  107. package/dist/protocol.d.ts +2 -0
  108. package/dist/protocol.d.ts.map +1 -1
  109. package/dist/spec.d.ts +5 -4
  110. package/dist/spec.d.ts.map +1 -1
  111. package/dist/spec.js.map +1 -1
  112. package/dist/worker-cli.d.ts.map +1 -1
  113. package/dist/worker-cli.js +6 -5
  114. package/dist/worker-cli.js.map +1 -1
  115. package/dist/worker-spend.d.ts +4 -3
  116. package/dist/worker-spend.d.ts.map +1 -1
  117. package/dist/worker-spend.js +13 -4
  118. package/dist/worker-spend.js.map +1 -1
  119. package/dist/worker.d.ts.map +1 -1
  120. package/dist/worker.js +4 -2
  121. package/dist/worker.js.map +1 -1
  122. package/package.json +2 -2
  123. package/src/adapters/base.ts +7 -0
  124. package/src/adapters/claude.ts +1 -0
  125. package/src/authored-admission.ts +17 -0
  126. package/src/authored-budget.ts +4 -3
  127. package/src/authored-flow-error.ts +0 -1
  128. package/src/authored-flow-executor.ts +27 -7
  129. package/src/authored-flow-loader.ts +96 -13
  130. package/src/authored-helper-effect.ts +5 -3
  131. package/src/authored-mcp.ts +6 -4
  132. package/src/authored-node-entry.ts +90 -0
  133. package/src/authored-node-runner.ts +219 -0
  134. package/src/authored-plugin-effect.ts +6 -3
  135. package/src/authored-root.ts +396 -0
  136. package/src/authored-runtime-capability.ts +40 -0
  137. package/src/authored-source-authority.ts +62 -0
  138. package/src/authored-worker-step.ts +15 -4
  139. package/src/budget-preflight.ts +41 -25
  140. package/src/cli/check.ts +3 -1
  141. package/src/cli/direct-run.ts +24 -16
  142. package/src/cli/run.ts +58 -1
  143. package/src/cli-adapter.ts +27 -0
  144. package/src/cli.ts +5 -5
  145. package/src/cloud-run.ts +74 -21
  146. package/src/failure-kinds.ts +5 -1
  147. package/src/journal-client.ts +6 -2
  148. package/src/llm-worker.ts +4 -2
  149. package/src/local-agent.ts +7 -2
  150. package/src/model-pricing.ts +4 -4
  151. package/src/preflight.ts +40 -18
  152. package/src/protocol.ts +2 -0
  153. package/src/spec.ts +5 -4
  154. package/src/worker-cli.ts +7 -5
  155. package/src/worker-spend.ts +15 -4
  156. package/src/worker.ts +4 -2
@@ -1,11 +1,13 @@
1
1
  import { McpStepError } from '../authored-mcp.js';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { authoredLocalAgentStream } from '../authored-admission.js';
2
4
  import { McpPreflightError } from './check-typescript.js';
3
5
  import { attachLocalAgent } from '../local-agent.js';
4
6
  import { LlmWorker } from '../llm-worker.js';
5
7
  import {
6
8
  AuthoredFlowExecutionError,
7
- executeAuthoredFlow,
8
9
  } from '../authored-flow-executor.js';
10
+ import { executeDurableAuthoredFlow } from '../authored-root.js';
9
11
  import { AuthoredFlowLoadError } from '../authored-flow-loader.js';
10
12
  import { DirectInputError, parseDirectInput } from '../direct-input.js';
11
13
  import { JournalClient } from '../journal-client.js';
@@ -61,10 +63,13 @@ export async function runDirectFlow(
61
63
  let localLlm: LlmWorker | undefined;
62
64
  let llmClient: JournalClient | undefined;
63
65
  let llmFailure: unknown;
66
+ const admissionIdentity = options.authoredAdmissionKey
67
+ ?? process.env['RELAYFLOW_AUTHORED_ADMISSION_KEY'] ?? randomUUID();
64
68
  try {
65
- const { handle, getDefinition } = checked.loaded;
66
69
  if (options.localAgent) {
67
- localAgent = await attachLocalAgent(client, dataDir, options.onPtyReady);
70
+ localAgent = await attachLocalAgent(
71
+ client, dataDir, options.onPtyReady, authoredLocalAgentStream(admissionIdentity),
72
+ );
68
73
  // A session owns one worker registration. Keep the workspace-free LLM
69
74
  // worker on its own connection so it cannot replace the agent worker.
70
75
  llmClient = new JournalClient(socketPath);
@@ -74,15 +79,19 @@ export async function runDirectFlow(
74
79
  localLlm.on('error', error => { llmFailure = error; client.close(); });
75
80
  await localLlm.attach();
76
81
  }
77
- const result = await executeAuthoredFlow(handle, client, input, {
78
- getDefinition,
79
- dataDir,
80
- flowPath: path,
81
- onProgress: options.onProgress,
82
- localAgentStream: localAgent?.stream,
83
- ...(options.signal !== undefined ? { signal: options.signal } : {}),
84
- ...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
85
- });
82
+ const result = await executeDurableAuthoredFlow(
83
+ checked.loaded, client, input,
84
+ {
85
+ dataDir,
86
+ admissionKey: admissionIdentity,
87
+ localAgentStream: localAgent?.stream,
88
+ lifecycle: {
89
+ onProgress: options.onProgress,
90
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
91
+ ...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
92
+ },
93
+ },
94
+ );
86
95
  const terminal = result.journalSteps.at(-1);
87
96
  if (terminal === undefined) {
88
97
  return protocolFailure('run', base, socketPath, new Error(
@@ -93,7 +102,7 @@ export async function runDirectFlow(
93
102
  return {
94
103
  exitCode: 3,
95
104
  report: {
96
- ...base, ok: false, runId: terminal.runId, socketPath, status: 'parked',
105
+ ...base, ok: false, runId: result.rootRunId, socketPath, status: 'parked',
97
106
  completedSteps: result.journalSteps.length,
98
107
  diagnostics: [...base.diagnostics, {
99
108
  severity: 'parked', kind: 'run_parked',
@@ -107,7 +116,7 @@ export async function runDirectFlow(
107
116
  report: {
108
117
  ...base,
109
118
  ok: true,
110
- runId: terminal.runId,
119
+ runId: result.rootRunId,
111
120
  socketPath,
112
121
  status: 'completed',
113
122
  completionReason: result.completionReason,
@@ -144,7 +153,7 @@ export async function runDirectFlow(
144
153
  && (error.code === 'helper_slack.credential_missing'
145
154
  || error.code === 'helper_slack.mount_required'
146
155
  || error.code === 'budget_syntax_invalid'
147
- || error.code === 'budget_missing_price'
156
+ || error.code === 'unsupported_promise_lifecycle'
148
157
  || error.code === 'unsupported_header'
149
158
  || error.code === 'agent_cli_unresolved'
150
159
  || error.code === 'llm_cli_unresolved'
@@ -157,7 +166,6 @@ export async function runDirectFlow(
157
166
  error.code === 'helper_slack.credential_missing'
158
167
  || error.code === 'helper_slack.mount_required'
159
168
  || error.code === 'budget_syntax_invalid'
160
- || error.code === 'budget_missing_price'
161
169
  ) ? error.code : 'invalid_spec',
162
170
  message: error.message,
163
171
  }, path)),
package/src/cli/run.ts CHANGED
@@ -14,6 +14,8 @@ import type { RunFailureKind, RunWarningKind, StepFailedDetails } from '../failu
14
14
  import { deterministicFailureDetails } from './deterministic-failure.js';
15
15
  import { JournalClient, JournalProtocolError } from '../journal-client.js';
16
16
  import { attachLocalAgent } from '../local-agent.js';
17
+ import { LlmWorker } from '../llm-worker.js';
18
+ import { readAuthoredRootMetadata, resumeDurableAuthoredFlow } from '../authored-root.js';
17
19
  import type {
18
20
  RunCompletionReason,
19
21
  RunOutcome,
@@ -68,6 +70,8 @@ export interface RunProgress {
68
70
  }
69
71
 
70
72
  export interface RunLifecycleOptions {
73
+ /** Caller-owned hosted invocation identity for authored-root start recovery. */
74
+ authoredAdmissionKey?: string;
71
75
  bucket?: string;
72
76
  allowHumanInfluenced?: boolean;
73
77
  onPtyReady?: (path: string) => void;
@@ -151,7 +155,50 @@ export async function resumeFlow(
151
155
  const connected = await connect(client, 'resume', dataDir, base, options);
152
156
  if (connected !== undefined) return connected;
153
157
 
158
+ let authoredAgent: Awaited<ReturnType<typeof attachLocalAgent>> | undefined;
159
+ let authoredLlm: LlmWorker | undefined;
160
+ let authoredLlmClient: JournalClient | undefined;
154
161
  try {
162
+ const authoredRoot = await readAuthoredRootMetadata(client, runId);
163
+ if (authoredRoot !== undefined) {
164
+ if (authoredRoot.localAgentStream !== undefined && !options.localAgent) {
165
+ throw new Error('authored root requires --local-agent to resume its pinned worker surface');
166
+ }
167
+ if (authoredRoot.localAgentStream === undefined && options.localAgent) {
168
+ throw new Error('authored root was started without a local agent worker surface');
169
+ }
170
+ if (options.localAgent) {
171
+ authoredAgent = await attachLocalAgent(
172
+ client, dataDir, options.onPtyReady, authoredRoot.localAgentStream,
173
+ );
174
+ authoredLlmClient = new JournalClient(socketPath);
175
+ await authoredLlmClient.connect();
176
+ await authoredLlmClient.hello('flows-authored-resume-llm');
177
+ authoredLlm = new LlmWorker(authoredLlmClient, `${authoredAgent.stream}-llm`);
178
+ await authoredLlm.attach();
179
+ }
180
+ const result = await resumeDurableAuthoredFlow(runId, client, {
181
+ dataDir,
182
+ localAgentStream: authoredAgent?.stream,
183
+ lifecycle: options,
184
+ });
185
+ if (result === undefined) throw new Error('authored root disappeared during resume');
186
+ return {
187
+ exitCode: result.completionReason === 'needs_human' ? 3 : 0,
188
+ report: {
189
+ ...base,
190
+ ok: result.completionReason === 'success',
191
+ runId,
192
+ socketPath,
193
+ status: result.completionReason === 'needs_human' ? 'parked' : 'completed',
194
+ ...(result.completionReason === 'success'
195
+ ? { completionReason: 'success' as const }
196
+ : { diagnostics: [{ severity: 'parked' as const, kind: 'run_parked' as const,
197
+ message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.` }] }),
198
+ completedSteps: result.journalSteps.length,
199
+ },
200
+ };
201
+ }
155
202
  // resumeHelperEffect subsumes the old resumeSlackEffect: it handles the
156
203
  // slack effect resume plus every other provider from N's codegen. The
157
204
  // second call the earlier rebase left is a stale reference from before
@@ -166,6 +213,10 @@ export async function resumeFlow(
166
213
  return { exitCode: 2, report: { ...base, runId, socketPath,
167
214
  diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } };
168
215
  }
216
+ if (error instanceof AuthoredFlowExecutionError && error.code === 'unsupported_promise_lifecycle') {
217
+ return { exitCode: 2, report: { ...base, runId, socketPath,
218
+ diagnostics: [{ severity: 'refusal', kind: 'invalid_spec', message: error.message }] } };
219
+ }
169
220
  if (error instanceof AuthoredFlowExecutionError
170
221
  && (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required'
171
222
  || error.code === 'helper_provider.mount_required' || error.code === 'helper_provider.unsupported')) {
@@ -189,7 +240,13 @@ export async function resumeFlow(
189
240
  },
190
241
  };
191
242
  } finally {
192
- client.close();
243
+ try {
244
+ await authoredLlm?.close();
245
+ authoredLlmClient?.close();
246
+ await authoredAgent?.close();
247
+ } finally {
248
+ client.close();
249
+ }
193
250
  }
194
251
  }
195
252
 
@@ -28,6 +28,33 @@ export function cliAdapterKind(executable: string): CliAdapterKind {
28
28
  return resolveAdapterKind(executable);
29
29
  }
30
30
 
31
+ export type CliModelSource = 'step' | 'named' | 'adapter';
32
+
33
+ export interface ResolvedCliModel {
34
+ readonly model?: string;
35
+ readonly source?: CliModelSource;
36
+ }
37
+
38
+ /**
39
+ * Resolve the model once, in authoring priority order: step, selected named
40
+ * agent, then the registered CLI adapter's default. Unregistered executables
41
+ * resolve through the wrapper adapter, whose absent default remains undefined.
42
+ */
43
+ export function resolveCliModelSelection(
44
+ executable: string,
45
+ declarations: Readonly<{ step?: string; named?: string }> = {},
46
+ ): ResolvedCliModel {
47
+ if (declarations.step !== undefined) return { model: declarations.step, source: 'step' };
48
+ if (declarations.named !== undefined) return { model: declarations.named, source: 'named' };
49
+ const model = registeredAdapters()[resolveAdapterKind(executable)].defaultModel;
50
+ return model === undefined ? {} : { model, source: 'adapter' };
51
+ }
52
+
53
+ /** Resolve a runtime model, where any materialized value is step-owned. */
54
+ export function resolveCliModel(executable: string, model?: string): string | undefined {
55
+ return resolveCliModelSelection(executable, { step: model }).model;
56
+ }
57
+
31
58
  /** Prove the adapter command shape before classifying an auth failure. */
32
59
  export function adapterIdentification(kind: CliAdapterKind): CliAdapterIdentification {
33
60
  return registeredAdapters()[kind].buildIdentification();
package/src/cli.ts CHANGED
@@ -53,7 +53,7 @@ type ParsedArgs =
53
53
  | { command: 'cloud-run'; value: string; json: boolean; wait: boolean }
54
54
  | { command: 'check'; json: boolean; watch: boolean; value: string }
55
55
  | { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
56
- | { command: 'resume'; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
56
+ | { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
57
57
  | { command: 'observer'; dataDir: string }
58
58
  | { command: 'hn-monitor'; sub: 'start'; dataDir: string; specPath: string; pollIntervalMs: number | undefined }
59
59
  | { command: 'tick'; sub: 'start'; dataDir: string; specPath: string; scheduleId: string;
@@ -74,7 +74,7 @@ const USAGE = [
74
74
  'flows run --cloud [--json] [--wait] <flow.yaml|spec.json>',
75
75
  'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
76
76
  'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
77
- 'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] <run-id>',
77
+ 'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
78
78
  'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
79
79
  'flows observer [--data-dir <dir>]',
80
80
  'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
@@ -191,7 +191,7 @@ export async function runCli(
191
191
  allowHumanInfluenced: parsed.allowHumanInfluenced,
192
192
  onPtyReady: (path: string) => io.stderr(`PTY ${path}`),
193
193
  ...(parsed.command === 'run' && parsed.reuseFromRunId !== undefined ? { reuseFromRunId: parsed.reuseFromRunId } : {}),
194
- localAgent: parsed.command === 'run' && parsed.localAgent,
194
+ localAgent: parsed.localAgent,
195
195
  onProgress: showProgress,
196
196
  onWait: (progress: RunProgress) => {
197
197
  emitWait(progress, io);
@@ -455,7 +455,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
455
455
  continue;
456
456
  }
457
457
  if (argument === '--local-agent') {
458
- if (command !== 'run' || localAgent) return undefined;
458
+ if (command === 'check' || localAgent) return undefined;
459
459
  localAgent = true;
460
460
  continue;
461
461
  }
@@ -534,7 +534,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
534
534
  ? { command, json, watch, value: positionals[0]! }
535
535
  : command === 'run'
536
536
  ? { command, bucket, reuseFromRunId, localAgent, dataDir, input, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! }
537
- : { command, dataDir, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! };
537
+ : { command, localAgent, dataDir, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! };
538
538
  }
539
539
 
540
540
  function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
package/src/cloud-run.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { RUN_COMPLETION_REASONS } from '@relayflows/surface';
2
2
  import type { RunCompletionReason } from './protocol.js';
3
3
  import { readFile } from 'node:fs/promises';
4
+ import { createHash } from 'node:crypto';
4
5
  import { setTimeout } from 'node:timers/promises';
5
6
  import { parse as parseYaml } from 'yaml';
6
7
  import { canonicalize, specHash } from './canonical.js';
7
8
  import { CompileError, compileSpec, kernelToAuthoring, toKernelSpec } from './compile.js';
8
9
  import type { FlowSpec } from './spec.js';
10
+ import { snapshotJsonValue, type JsonValue } from './json-value.js';
11
+ import { loadAuthoredFlow, type SurfaceModuleAuthority } from './authored-flow-loader.js';
9
12
  import {
10
13
  CloudFlowError, cloudConnection, cloudRequest, cloudRunId, isCloudRecord,
11
14
  type CloudConnectionOptions,
@@ -15,6 +18,8 @@ export type CloudFlowSource = FlowSpec | { path: string };
15
18
  export interface RunInCloudOptions extends CloudConnectionOptions {
16
19
  /** Existing Cloud workspace to run in; provisioning/authorization remains server-side. */
17
20
  workspaceId?: string;
21
+ /** Exact JSON input. Required at runtime for authored .flow.ts; refused for declarative source. */
22
+ input?: JsonValue;
18
23
  }
19
24
  export interface CloudRunReceipt {
20
25
  runId: string;
@@ -24,8 +29,14 @@ export interface CloudRunReceipt {
24
29
  /** Authenticated run API resource; this is not a public sharing URL. */
25
30
  apiUrl: string;
26
31
  }
32
+ export interface CloudAuthoredAuthority {
33
+ readonly schemaVersion: 1;
34
+ readonly sourceSha256: string;
35
+ readonly byteLength: number;
36
+ readonly surface: SurfaceModuleAuthority;
37
+ }
27
38
  export type CloudRunState =
28
- | { runId: string; status: 'pending' | 'running' }
39
+ | { runId: string; status: 'pending' | 'launching' | 'running' }
29
40
  | { runId: string; status: 'completed' | 'failed' | 'cancelled'; completionReason: RunCompletionReason };
30
41
 
31
42
  /**
@@ -38,43 +49,83 @@ export async function runInCloud(
38
49
  options: RunInCloudOptions = {},
39
50
  ): Promise<CloudRunReceipt> {
40
51
  const { baseUrl } = cloudConnection(options);
41
- let spec: FlowSpec;
52
+ let spec: FlowSpec | undefined;
53
+ let authored: { source: string; authority: CloudAuthoredAuthority } | undefined;
54
+ const inputPresent = Object.prototype.hasOwnProperty.call(options, 'input');
55
+ let authoredInput: JsonValue | undefined;
42
56
  try {
43
57
  options.signal?.throwIfAborted();
44
58
  if ('path' in flow) {
45
- if (!/\.(?:ya?ml|json)$/iu.test(flow.path)) {
59
+ if (/\.flow\.ts$/iu.test(flow.path)) {
60
+ const bytes = await readFile(flow.path);
61
+ const source = bytes.toString('utf8');
62
+ if (!bytes.length || Buffer.from(source, 'utf8').compare(bytes) !== 0) {
63
+ throw new CloudFlowError('invalid_input', 'Authored source must be nonempty, lossless UTF-8.');
64
+ }
65
+ const loaded = await loadAuthoredFlow(flow.path);
66
+ loaded.getDefinition(loaded.handle);
67
+ if (loaded.graph.length !== 1) {
68
+ throw new CloudFlowError('unsupported_source',
69
+ 'Cloud authored submission currently accepts one self-contained .flow.ts source without use dependencies.');
70
+ }
71
+ authored = {
72
+ source,
73
+ authority: Object.freeze({
74
+ schemaVersion: 1,
75
+ sourceSha256: createHash('sha256').update(bytes).digest('hex'),
76
+ byteLength: bytes.length,
77
+ surface: loaded.surfaceAuthority,
78
+ }),
79
+ };
80
+ } else if (!/\.(?:ya?ml|json)$/iu.test(flow.path)) {
46
81
  throw new CloudFlowError('unsupported_source',
47
- 'Cloud v2 currently accepts declarative YAML or JSON specs; authored TypeScript flows are not supported by the hosted runtime.');
48
- }
49
- const parsed: unknown = parseYaml(await readFile(flow.path, 'utf8'));
50
- try {
51
- spec = compileSpec(parsed);
52
- } catch (error) {
53
- if (!(error instanceof CompileError)) throw error;
54
- // Prefer the authoring dialect; accept compiled kernel JSON for local-CLI
55
- // compatibility. If both fail, retain the original authoring diagnostic.
82
+ 'Cloud v2 accepts declarative YAML/JSON specs or authored .flow.ts source.');
83
+ } else {
84
+ const parsed: unknown = parseYaml(await readFile(flow.path, 'utf8'));
56
85
  try {
57
- spec = compileSpec(kernelToAuthoring(parsed));
58
- } catch {
59
- throw error;
86
+ spec = compileSpec(parsed);
87
+ } catch (error) {
88
+ if (!(error instanceof CompileError)) throw error;
89
+ // Prefer the authoring dialect; accept compiled kernel JSON for local-CLI
90
+ // compatibility. If both fail, retain the original authoring diagnostic.
91
+ try {
92
+ spec = compileSpec(kernelToAuthoring(parsed));
93
+ } catch {
94
+ throw error;
95
+ }
60
96
  }
61
97
  }
62
98
  } else {
63
99
  spec = compileSpec(flow);
64
100
  }
101
+ if (authored !== undefined) {
102
+ if (!inputPresent) {
103
+ throw new CloudFlowError('invalid_input', 'Cloud authored .flow.ts requires an explicit JSON input. Pass {} when the flow needs no fields.');
104
+ }
105
+ authoredInput = snapshotJsonValue(options.input, 'Cloud authored input');
106
+ } else if (inputPresent) {
107
+ throw new CloudFlowError('invalid_input', 'Cloud input is supported only for authored .flow.ts source.');
108
+ }
65
109
  } catch (error) {
66
110
  options.signal?.throwIfAborted();
67
111
  if (error instanceof CloudFlowError) throw error;
68
112
  throw new CloudFlowError('invalid_input', 'Cannot read or compile the declarative flow. Check the file path and YAML/JSON spec.');
69
113
  }
70
114
  options.signal?.throwIfAborted();
71
- const hash = specHash(toKernelSpec(spec));
115
+ const hash = authored === undefined
116
+ ? specHash(toKernelSpec(spec!))
117
+ : createHash('sha256').update(canonicalize({
118
+ authority: authored.authority,
119
+ input: authoredInput,
120
+ })).digest('hex');
72
121
  const result = await cloudRequest('/api/v1/workflows/run', options, {
73
122
  // JSON is a YAML subset. Sending canonical data preserves the exact spec
74
123
  // while using the server's existing YAML-to-config admission path.
75
- workflow: canonicalize(spec),
76
- fileType: 'yaml',
124
+ workflow: authored?.source ?? canonicalize(spec),
125
+ fileType: authored === undefined ? 'yaml' : 'ts',
77
126
  relayflowVersion: 'v2',
127
+ ...(authored === undefined ? {} : { authoredAuthority: authored.authority }),
128
+ ...(authored === undefined ? {} : { inputs: authoredInput }),
78
129
  ...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
79
130
  });
80
131
  if (!isCloudRecord(result) || (result.status !== 'pending' && result.status !== 'running')) {
@@ -92,10 +143,12 @@ export async function getCloudFlowRun(
92
143
  const result = await cloudRequest(`/api/v1/workflows/runs/${runId}`, options);
93
144
  if (!isCloudRecord(result) || result.runId !== runId || result.relayflowVersion !== 'v2'
94
145
  || typeof result.status !== 'string'
95
- || !['pending', 'running', 'completed', 'failed', 'cancelled'].includes(result.status)) {
146
+ || !['pending', 'launching', 'running', 'completed', 'failed', 'cancelled'].includes(result.status)) {
96
147
  throw new CloudFlowError('invalid_response', 'Cloud returned an invalid v2 run record.');
97
148
  }
98
- if (result.status === 'pending' || result.status === 'running') return { runId, status: result.status };
149
+ if (result.status === 'pending' || result.status === 'launching' || result.status === 'running') {
150
+ return { runId, status: result.status };
151
+ }
99
152
  const report = result.result;
100
153
  const reason = isCloudRecord(report) ? report.completionReason : undefined;
101
154
  if (typeof reason !== 'string' || !(RUN_COMPLETION_REASONS as readonly string[]).includes(reason)
@@ -122,7 +175,7 @@ export async function waitForCloudFlowRun(
122
175
  try {
123
176
  const run = await getCloudFlowRun(runId, options);
124
177
  failures = 0;
125
- if (run.status !== 'pending' && run.status !== 'running') return run;
178
+ if (run.status !== 'pending' && run.status !== 'launching' && run.status !== 'running') return run;
126
179
  } catch (error) {
127
180
  options.signal?.throwIfAborted();
128
181
  const transient = error instanceof CloudFlowError && (error.code === 'transient_error'
@@ -14,7 +14,6 @@ const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [
14
14
  'mcp_undeclared_server',
15
15
  'mcp_unreachable',
16
16
  'budget_syntax_invalid',
17
- 'budget_missing_price',
18
17
  'cli_missing',
19
18
  'cli_unauthenticated',
20
19
  'cli_unresolved',
@@ -61,12 +60,17 @@ export const CHECK_FAILURE_KINDS = [
61
60
  * `vacuous_gate` is the same principle applied to a declared gate that judges
62
61
  * nothing: `schema: {}` and `schema: true` are legal and accepted, but a gate
63
62
  * accepting every output must not be reported as if it constrained one.
63
+ *
64
+ * `budget_unmetered` names an LLM/agent step under a dollar budget whose model
65
+ * has no frozen price (including Codex, which selects its own model). Pricing
66
+ * is light enforcement: the step runs and simply contributes no dollars.
64
67
  */
65
68
  export const PREFLIGHT_WARNING_KINDS = [
66
69
  'unprovable_effects',
67
70
  'command_unresolved',
68
71
  'command_unprovable',
69
72
  'vacuous_gate',
73
+ 'budget_unmetered',
70
74
  ] as const;
71
75
 
72
76
  /**
@@ -204,8 +204,12 @@ export class JournalClient extends EventEmitter {
204
204
  * the run file, and append `run.spawned`. Authoring specs must be compiled
205
205
  * with `toKernelSpec` before crossing this journal-protocol boundary.
206
206
  */
207
- runStart(spec: KernelRunSpec, reuseFromRunId?: string): Promise<VerbContract['run.start']['result']> {
208
- return this.request('run.start', { spec, ...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }) }, null);
207
+ runStart(spec: KernelRunSpec, reuseFromRunId?: string, admissionKey?: string): Promise<VerbContract['run.start']['result']> {
208
+ return this.request('run.start', {
209
+ spec,
210
+ ...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }),
211
+ ...(admissionKey === undefined ? {} : { admission_key: admissionKey }),
212
+ }, null);
209
213
  }
210
214
 
211
215
  /** §3 memoized resume. */
package/src/llm-worker.ts CHANGED
@@ -5,6 +5,7 @@ import type { JournalClient } from './journal-client.js';
5
5
  import type { CompletionReason, StepDispatchEvent } from './protocol.js';
6
6
  import type { KernelLlmStep } from './spec.js';
7
7
  import { runAgentCli } from './worker-cli.js';
8
+ import { resolveCliModel } from './cli-adapter.js';
8
9
  import { withWorkerLease } from './worker-lease.js';
9
10
  import { workerInstruction } from './worker-input.js';
10
11
  import { jsonSchemaOutputError } from './json-schema.js';
@@ -49,11 +50,12 @@ export class LlmWorker extends EventEmitter {
49
50
  const schema = spec.verification?.json_schema;
50
51
  const prompt = schema === undefined ? spec.prompt
51
52
  : `${spec.prompt}\n\nReturn only a JSON value matching this JSON Schema (no Markdown fences):\n${JSON.stringify(schema)}`;
53
+ const effectiveModel = typeof spec.cli === 'string' ? resolveCliModel(spec.cli, spec.model) : spec.model;
52
54
  const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
53
55
  typeof spec.cli === 'string' && typeof spec.prompt === 'string'
54
- ? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, spec.model, undefined, signal, 'llm')
56
+ ? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'llm')
55
57
  : Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'llm step has no declared CLI' }));
56
- const { result, usage } = workerSpend(completed, spec.model);
58
+ const { result, usage } = workerSpend(completed, effectiveModel);
57
59
  let reason: CompletionReason = result.exit_code === 0 ? 'success' : 'worker_error';
58
60
  let output: unknown = result.stdout_tail;
59
61
  let detail = result.stderr_tail;
@@ -3,14 +3,19 @@ import type { JournalClient } from './journal-client.js';
3
3
  import { AgentWorker } from './worker.js';
4
4
 
5
5
  /** A local worker for stream-only steps; no workspace recovery is claimed. */
6
- export async function attachLocalAgent(client: JournalClient, dataDir?: string, onPtyReady?: (path: string) => void): Promise<{
6
+ export async function attachLocalAgent(
7
+ client: JournalClient,
8
+ dataDir?: string,
9
+ onPtyReady?: (path: string) => void,
10
+ requestedStream?: string,
11
+ ): Promise<{
7
12
  stream: string;
8
13
  readonly failure: unknown;
9
14
  close(): Promise<void>;
10
15
  }> {
11
16
  // A fresh, unconsumed stream has offset zero. The executor declares exactly
12
17
  // this stream on its agent steps. No worktree revision is invented.
13
- const stream = `local-agent-${randomUUID()}`;
18
+ const stream = requestedStream ?? `local-agent-${randomUUID()}`;
14
19
  const worker = new AgentWorker(client, {
15
20
  workerId: stream,
16
21
  capacity: 1,
@@ -2,6 +2,7 @@
2
2
  export const MODEL_PRICING: Readonly<Record<string, Readonly<{ input: number; output: number }>>> = Object.freeze({
3
3
  'claude-sonnet-4-6': Object.freeze({ input: 3, output: 15 }),
4
4
  'claude-opus-4-7': Object.freeze({ input: 15, output: 75 }),
5
+ 'claude-opus-5': Object.freeze({ input: 5, output: 25 }),
5
6
  'codex-medium': Object.freeze({ input: 2, output: 8 }),
6
7
  'codex-large': Object.freeze({ input: 5, output: 20 }),
7
8
  });
@@ -15,10 +16,9 @@ export function hasPricing(model: string | undefined): boolean {
15
16
  *
16
17
  * Returns `undefined` for unpriced models — callers should omit `usage`
17
18
  * from the journal payload rather than sending nulls that break the kernel
18
- * wire schema. The refusal for a declared dollar budget against an unpriced
19
- * model is `budgetDiagnostics` at preflight (before any CLI dispatches).
20
- * Throwing here after usage decode would waste the CLI invocation that
21
- * preflight was meant to prevent.
19
+ * wire schema. An unpriced step is unmetered, not refused: it contributes no
20
+ * dollars to a budget, and `budgetDiagnostics` warns about it at preflight.
21
+ * Codex model ids need no entry here; Codex selects its own model.
22
22
  */
23
23
  export function pricedUsage(model: string | undefined, input = 0, output = 0):
24
24
  | { tokens_in: number; tokens_out: number; dollars: string }