@relayflows/sdk 2.0.10 → 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 (172) 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/agent-relay-receipt.d.ts +24 -0
  7. package/dist/agent-relay-receipt.d.ts.map +1 -0
  8. package/dist/agent-relay-receipt.js +66 -0
  9. package/dist/agent-relay-receipt.js.map +1 -0
  10. package/dist/agent-relay-state.d.ts +18 -0
  11. package/dist/agent-relay-state.d.ts.map +1 -0
  12. package/dist/agent-relay-state.js +58 -0
  13. package/dist/agent-relay-state.js.map +1 -0
  14. package/dist/agent-relay-transport.d.ts +17 -70
  15. package/dist/agent-relay-transport.d.ts.map +1 -1
  16. package/dist/agent-relay-transport.js +177 -112
  17. package/dist/agent-relay-transport.js.map +1 -1
  18. package/dist/authored-admission.d.ts +5 -0
  19. package/dist/authored-admission.d.ts.map +1 -0
  20. package/dist/authored-admission.js +14 -0
  21. package/dist/authored-admission.js.map +1 -0
  22. package/dist/authored-budget.d.ts +1 -1
  23. package/dist/authored-budget.d.ts.map +1 -1
  24. package/dist/authored-budget.js +3 -3
  25. package/dist/authored-budget.js.map +1 -1
  26. package/dist/authored-flow-error.d.ts +1 -1
  27. package/dist/authored-flow-error.d.ts.map +1 -1
  28. package/dist/authored-flow-error.js.map +1 -1
  29. package/dist/authored-flow-executor.d.ts +10 -0
  30. package/dist/authored-flow-executor.d.ts.map +1 -1
  31. package/dist/authored-flow-executor.js +10 -7
  32. package/dist/authored-flow-executor.js.map +1 -1
  33. package/dist/authored-flow-loader.d.ts +20 -0
  34. package/dist/authored-flow-loader.d.ts.map +1 -1
  35. package/dist/authored-flow-loader.js +86 -11
  36. package/dist/authored-flow-loader.js.map +1 -1
  37. package/dist/authored-helper-effect.d.ts +1 -1
  38. package/dist/authored-helper-effect.d.ts.map +1 -1
  39. package/dist/authored-helper-effect.js +5 -3
  40. package/dist/authored-helper-effect.js.map +1 -1
  41. package/dist/authored-mcp.d.ts +1 -1
  42. package/dist/authored-mcp.d.ts.map +1 -1
  43. package/dist/authored-mcp.js +7 -4
  44. package/dist/authored-mcp.js.map +1 -1
  45. package/dist/authored-node-entry.d.ts +2 -0
  46. package/dist/authored-node-entry.d.ts.map +1 -0
  47. package/dist/authored-node-entry.js +111 -0
  48. package/dist/authored-node-entry.js.map +1 -0
  49. package/dist/authored-node-runner.d.ts +11 -0
  50. package/dist/authored-node-runner.d.ts.map +1 -0
  51. package/dist/authored-node-runner.js +246 -0
  52. package/dist/authored-node-runner.js.map +1 -0
  53. package/dist/authored-plugin-effect.d.ts +1 -1
  54. package/dist/authored-plugin-effect.d.ts.map +1 -1
  55. package/dist/authored-plugin-effect.js +7 -4
  56. package/dist/authored-plugin-effect.js.map +1 -1
  57. package/dist/authored-root.d.ts +38 -0
  58. package/dist/authored-root.d.ts.map +1 -0
  59. package/dist/authored-root.js +329 -0
  60. package/dist/authored-root.js.map +1 -0
  61. package/dist/authored-runtime-capability.d.ts +6 -0
  62. package/dist/authored-runtime-capability.d.ts.map +1 -0
  63. package/dist/authored-runtime-capability.js +40 -0
  64. package/dist/authored-runtime-capability.js.map +1 -0
  65. package/dist/authored-source-authority.d.ts +4 -0
  66. package/dist/authored-source-authority.d.ts.map +1 -0
  67. package/dist/authored-source-authority.js +61 -0
  68. package/dist/authored-source-authority.js.map +1 -0
  69. package/dist/authored-worker-step.d.ts +2 -2
  70. package/dist/authored-worker-step.d.ts.map +1 -1
  71. package/dist/authored-worker-step.js +11 -6
  72. package/dist/authored-worker-step.js.map +1 -1
  73. package/dist/budget-preflight.d.ts +17 -3
  74. package/dist/budget-preflight.d.ts.map +1 -1
  75. package/dist/budget-preflight.js +31 -24
  76. package/dist/budget-preflight.js.map +1 -1
  77. package/dist/cli/check.js +3 -1
  78. package/dist/cli/check.js.map +1 -1
  79. package/dist/cli/direct-run.d.ts.map +1 -1
  80. package/dist/cli/direct-run.js +18 -14
  81. package/dist/cli/direct-run.js.map +1 -1
  82. package/dist/cli/run.d.ts +2 -0
  83. package/dist/cli/run.d.ts.map +1 -1
  84. package/dist/cli/run.js +56 -1
  85. package/dist/cli/run.js.map +1 -1
  86. package/dist/cli-adapter.d.ts +16 -0
  87. package/dist/cli-adapter.d.ts.map +1 -1
  88. package/dist/cli-adapter.js +17 -0
  89. package/dist/cli-adapter.js.map +1 -1
  90. package/dist/cli.js +4 -4
  91. package/dist/cli.js.map +1 -1
  92. package/dist/cloud-run.d.ts +11 -1
  93. package/dist/cloud-run.d.ts.map +1 -1
  94. package/dist/cloud-run.js +65 -19
  95. package/dist/cloud-run.js.map +1 -1
  96. package/dist/failure-kinds.d.ts +7 -3
  97. package/dist/failure-kinds.d.ts.map +1 -1
  98. package/dist/failure-kinds.js +5 -1
  99. package/dist/failure-kinds.js.map +1 -1
  100. package/dist/journal-client.d.ts +1 -1
  101. package/dist/journal-client.d.ts.map +1 -1
  102. package/dist/journal-client.js +6 -2
  103. package/dist/journal-client.js.map +1 -1
  104. package/dist/llm-worker.d.ts.map +1 -1
  105. package/dist/llm-worker.js +4 -2
  106. package/dist/llm-worker.js.map +1 -1
  107. package/dist/local-agent.d.ts +1 -1
  108. package/dist/local-agent.d.ts.map +1 -1
  109. package/dist/local-agent.js +2 -2
  110. package/dist/local-agent.js.map +1 -1
  111. package/dist/model-pricing.d.ts +3 -4
  112. package/dist/model-pricing.d.ts.map +1 -1
  113. package/dist/model-pricing.js +4 -4
  114. package/dist/model-pricing.js.map +1 -1
  115. package/dist/preflight.d.ts +4 -1
  116. package/dist/preflight.d.ts.map +1 -1
  117. package/dist/preflight.js +36 -18
  118. package/dist/preflight.js.map +1 -1
  119. package/dist/protocol.d.ts +2 -0
  120. package/dist/protocol.d.ts.map +1 -1
  121. package/dist/spec.d.ts +5 -4
  122. package/dist/spec.d.ts.map +1 -1
  123. package/dist/spec.js.map +1 -1
  124. package/dist/worker-cli.d.ts +5 -2
  125. package/dist/worker-cli.d.ts.map +1 -1
  126. package/dist/worker-cli.js +43 -46
  127. package/dist/worker-cli.js.map +1 -1
  128. package/dist/worker-spend.d.ts +4 -3
  129. package/dist/worker-spend.d.ts.map +1 -1
  130. package/dist/worker-spend.js +13 -4
  131. package/dist/worker-spend.js.map +1 -1
  132. package/dist/worker.d.ts.map +1 -1
  133. package/dist/worker.js +12 -4
  134. package/dist/worker.js.map +1 -1
  135. package/package.json +2 -2
  136. package/src/adapters/base.ts +7 -0
  137. package/src/adapters/claude.ts +1 -0
  138. package/src/agent-relay-receipt.ts +120 -0
  139. package/src/agent-relay-state.ts +73 -0
  140. package/src/agent-relay-transport.ts +242 -156
  141. package/src/authored-admission.ts +17 -0
  142. package/src/authored-budget.ts +4 -3
  143. package/src/authored-flow-error.ts +0 -1
  144. package/src/authored-flow-executor.ts +27 -7
  145. package/src/authored-flow-loader.ts +96 -13
  146. package/src/authored-helper-effect.ts +5 -3
  147. package/src/authored-mcp.ts +6 -4
  148. package/src/authored-node-entry.ts +90 -0
  149. package/src/authored-node-runner.ts +219 -0
  150. package/src/authored-plugin-effect.ts +6 -3
  151. package/src/authored-root.ts +396 -0
  152. package/src/authored-runtime-capability.ts +40 -0
  153. package/src/authored-source-authority.ts +62 -0
  154. package/src/authored-worker-step.ts +15 -4
  155. package/src/budget-preflight.ts +41 -25
  156. package/src/cli/check.ts +3 -1
  157. package/src/cli/direct-run.ts +24 -16
  158. package/src/cli/run.ts +58 -1
  159. package/src/cli-adapter.ts +27 -0
  160. package/src/cli.ts +5 -5
  161. package/src/cloud-run.ts +74 -21
  162. package/src/failure-kinds.ts +5 -1
  163. package/src/journal-client.ts +6 -2
  164. package/src/llm-worker.ts +4 -2
  165. package/src/local-agent.ts +7 -2
  166. package/src/model-pricing.ts +4 -4
  167. package/src/preflight.ts +40 -18
  168. package/src/protocol.ts +2 -0
  169. package/src/spec.ts +5 -4
  170. package/src/worker-cli.ts +50 -54
  171. package/src/worker-spend.ts +15 -4
  172. package/src/worker.ts +12 -4
@@ -1,187 +1,273 @@
1
- /**
2
- * Agent-relay HTTP transport for f.agent step dispatch (flows#385).
3
- *
4
- * The SDK's default agent path is a direct `child_process.spawn(cli, ...)`
5
- * in worker-cli.ts. That is unobservable — you cannot DM the spawned agent
6
- * mid-flight, and every ecosystem tool that wants to steer or watch it
7
- * ends up shelling out to the `agent-relay` CLI, which is fragile.
8
- *
9
- * This module speaks to agent-relay via structured HTTP: the same call
10
- * shape the `mcp__agent-relay__spawn` MCP tool posts. The SDK becomes a
11
- * first-class agent-relay participant, registered under a derived agent
12
- * name a caller can DM against.
13
- *
14
- * Scope: spawn + close. Streaming inbox and observation are follow-ups.
15
- */
1
+ import { createHash } from "node:crypto";
2
+ import { isIP } from "node:net";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { claimRelayTask } from "./agent-relay-state.js";
5
+ import { canonicalize } from "./canonical.js";
6
+ import {
7
+ isRecord,
8
+ nonempty,
9
+ readTaskReceipt,
10
+ type RelayTaskReceipt,
11
+ } from "./agent-relay-receipt.js";
16
12
 
17
- export type AgentTransport = 'direct' | 'relay';
18
-
19
- export interface AgentRelaySpawnRequest {
20
- /** Registered agent name in the workspace. */
21
- name: string;
22
- /** CLI to launch. */
23
- cli: 'claude' | 'codex' | 'gemini' | 'aider' | 'goose' | 'grok' | 'opencode';
24
- /** Initial task instructions. */
13
+ export type AgentTransport = "direct" | "relay";
14
+ export interface AgentRelayTaskRequest {
15
+ cli: string;
25
16
  task: string;
26
- /** Optional model powering the worker. */
27
17
  model?: string;
28
- /** Optional working directory for the spawned worker process. */
29
18
  worker_cwd?: string;
30
- /** Optional target fleet node name. */
31
- target_node?: string;
32
- /** Declared objective for workforce reporting. */
33
- objective?: string;
34
- /** Declared role for workforce reporting. */
35
- role?: string;
36
- /** Declared project for workforce reporting. */
37
- project?: string;
38
- /** Declared workstream for workforce reporting. */
39
- workstream?: string;
19
+ result_schema?: unknown;
20
+ runId: string;
21
+ stepId: string;
22
+ idempotencyKey: string;
23
+ dataDir: string;
24
+ /** Engine task contract ceiling; bounded independently of the renewing lease. */
25
+ timeoutMs?: number;
40
26
  }
41
-
42
27
  export interface AgentRelayEnv {
43
- /** `RELAY_BASE_URL` env; defaults to https://cast.agentrelay.com. */
44
28
  baseUrl?: string;
45
- /** `RELAY_API_KEY` env (rk_live_...). Required. */
46
- apiKey?: string;
47
- /** `RELAY_DEFAULT_WORKSPACE` env; workspace scoping for the spawn. */
48
- workspaceId?: string;
49
- /** Optional bearer token for the individual agent identity, if pre-registered. */
50
29
  agentToken?: string;
51
30
  }
52
-
53
- export interface AgentSpawnHandle {
54
- /** Registered agent name in the workspace — DM this to steer. */
55
- readonly registeredName: string;
56
- /** Invocation id returned by relay; use for status polling. */
57
- readonly invocationId: string;
58
- /** Best-effort deregistration/notification. */
59
- close(): Promise<void>;
60
- }
61
-
62
31
  export class AgentRelayTransportError extends Error {
63
- constructor(message: string, public readonly cause?: unknown) {
32
+ constructor(
33
+ message: string,
34
+ public readonly cause?: unknown,
35
+ ) {
64
36
  super(message);
65
- this.name = 'AgentRelayTransportError';
37
+ this.name = "AgentRelayTransportError";
66
38
  }
67
39
  }
68
-
69
- /**
70
- * Read the same env the relay MCP already consumes. Explicit override wins.
71
- * Missing RELAY_API_KEY refuses immediately — the transport cannot proceed
72
- * unauthenticated and the direct-spawn fallback is a separate decision.
73
- */
74
- export function readAgentRelayEnv(env: NodeJS.ProcessEnv = process.env): Required<Pick<AgentRelayEnv, 'baseUrl' | 'apiKey'>> & AgentRelayEnv {
75
- const baseUrl = env['RELAY_BASE_URL']?.trim() || 'https://cast.agentrelay.com';
76
- const apiKey = env['RELAY_API_KEY']?.trim();
77
- if (!apiKey) {
40
+ export function readAgentRelayEnv(
41
+ env: NodeJS.ProcessEnv = process.env,
42
+ ): Required<AgentRelayEnv> {
43
+ const agentToken = env.RELAY_AGENT_TOKEN?.trim();
44
+ if (!agentToken)
78
45
  throw new AgentRelayTransportError(
79
- 'agent-relay transport requires RELAY_API_KEY in the environment; falling back to direct transport is the caller\'s responsibility.',
46
+ "Relay task transport requires a pre-provisioned RELAY_AGENT_TOKEN.",
80
47
  );
81
- }
82
48
  return {
83
- baseUrl,
84
- apiKey,
85
- workspaceId: env['RELAY_DEFAULT_WORKSPACE']?.trim() || undefined,
86
- agentToken: env['RELAY_AGENT_TOKEN']?.trim() || undefined,
49
+ baseUrl: env.RELAY_BASE_URL?.trim() || "https://cast.agentrelay.com",
50
+ agentToken,
87
51
  };
88
52
  }
89
53
 
90
- /**
91
- * Spawn a worker via agent-relay HTTP. The endpoint mirrors what the
92
- * `mcp__agent-relay__spawn` MCP tool wraps — a POST that requests a fleet
93
- * node dispatch. Returns a handle keyed on the registered agent name.
94
- *
95
- * The `fetch` argument is injected so tests can mock the transport without
96
- * hitting the network.
97
- */
98
- export async function agentRelaySpawn(
99
- request: AgentRelaySpawnRequest,
100
- env: AgentRelayEnv & { fetch?: typeof fetch } = {},
101
- ): Promise<AgentSpawnHandle> {
102
- // Only fall back to process.env if the caller didn't supply the required
103
- // fields directly. Tests pass { apiKey, fetch } inline; they shouldn't need
104
- // to also set RELAY_API_KEY in the runner env.
105
- const fromEnv: Partial<AgentRelayEnv> = env.apiKey
106
- ? { baseUrl: env.baseUrl || 'https://cast.agentrelay.com' }
107
- : readAgentRelayEnv();
108
- const resolved = { ...fromEnv, ...env };
109
- const url = new URL('/api/v1/agents/spawn', resolved.baseUrl).toString();
110
- const doFetch: typeof fetch = env.fetch ?? (globalThis as typeof globalThis & { fetch: typeof fetch }).fetch;
111
- if (typeof doFetch !== 'function') {
112
- throw new AgentRelayTransportError('global fetch is unavailable; provide { fetch } explicitly.');
113
- }
114
- const headers: Record<string, string> = {
115
- 'content-type': 'application/json',
116
- 'authorization': `Bearer ${resolved.apiKey}`,
117
- };
118
- if (resolved.workspaceId !== undefined) headers['x-relay-workspace'] = resolved.workspaceId;
119
- if (resolved.agentToken !== undefined) headers['x-relay-agent-token'] = resolved.agentToken;
120
-
121
- let response: Response;
122
- try {
123
- response = await doFetch(url, {
124
- method: 'POST',
125
- headers,
126
- body: JSON.stringify(request),
127
- });
128
- } catch (cause) {
129
- throw new AgentRelayTransportError(
130
- `agent-relay spawn network error at ${url}: ${(cause as Error).message ?? String(cause)}`,
131
- cause,
132
- );
133
- }
134
-
135
- if (response.status < 200 || response.status >= 300) {
136
- let body: string;
137
- try { body = (await response.text()).slice(0, 512); } catch { body = '<no body>'; }
138
- throw new AgentRelayTransportError(
139
- `agent-relay spawn refused with status ${response.status}: ${body}`,
140
- );
141
- }
142
-
143
- let json: { invocation?: { invocationId?: string; input?: { name?: string } } };
144
- try { json = await response.json() as typeof json; } catch (cause) {
54
+ /** Exact Relaycast #436 HTTP contract. Only terminal GET receipts can return. */
55
+ export async function runAgentRelayTask(
56
+ request: AgentRelayTaskRequest,
57
+ options: AgentRelayEnv & {
58
+ fetch?: typeof fetch;
59
+ signal?: AbortSignal;
60
+ pollMs?: number;
61
+ } = {},
62
+ ): Promise<RelayTaskReceipt> {
63
+ const env = options.agentToken
64
+ ? {
65
+ baseUrl: options.baseUrl || "https://cast.agentrelay.com",
66
+ agentToken: options.agentToken,
67
+ }
68
+ : { ...readAgentRelayEnv(), ...options };
69
+ const base = new URL(env.baseUrl!);
70
+ if (
71
+ base.username ||
72
+ base.password ||
73
+ base.search ||
74
+ base.hash ||
75
+ base.pathname !== "/" ||
76
+ !["https:", "http:"].includes(base.protocol)
77
+ )
78
+ throw new AgentRelayTransportError("Invalid Relay task base URL");
79
+ const loopback =
80
+ base.hostname === "[::1]" ||
81
+ (isIP(base.hostname) === 4 && base.hostname.startsWith("127."));
82
+ if (base.protocol !== "https:" && !loopback) {
145
83
  throw new AgentRelayTransportError(
146
- 'agent-relay spawn response was not JSON',
147
- cause,
84
+ "Relay task credentials require HTTPS outside literal loopback addresses",
148
85
  );
149
86
  }
150
- const invocationId = json.invocation?.invocationId;
151
- const registeredName = json.invocation?.input?.name ?? request.name;
152
- if (typeof invocationId !== 'string' || invocationId.length === 0) {
87
+ const baseUrl = base.origin;
88
+ const timeoutMs = request.timeoutMs ?? 86_400_000;
89
+ if (
90
+ !Number.isSafeInteger(timeoutMs) ||
91
+ timeoutMs < 1 ||
92
+ timeoutMs > 86_400_000 ||
93
+ ![
94
+ request.runId,
95
+ request.stepId,
96
+ request.idempotencyKey,
97
+ request.dataDir,
98
+ request.cli,
99
+ ].every(nonempty)
100
+ ) {
153
101
  throw new AgentRelayTransportError(
154
- 'agent-relay spawn response missing invocation.invocationId',
102
+ "Relay task requires durable dispatch identity and a valid deadline",
155
103
  );
156
104
  }
157
-
158
- return {
159
- registeredName,
160
- invocationId,
161
- async close(): Promise<void> {
162
- // Best-effort: post to /api/v1/invocations/<id>/close if defined server-side;
163
- // no throw on error because the invocation may already be terminal.
164
- const closeUrl = new URL(`/api/v1/invocations/${encodeURIComponent(invocationId)}/close`, resolved.baseUrl).toString();
105
+ const pollMs = options.pollMs ?? 1000;
106
+ if (!Number.isFinite(pollMs) || pollMs < 1)
107
+ throw new AgentRelayTransportError("Invalid Relay task polling interval");
108
+ const doFetch = options.fetch ?? globalThis.fetch;
109
+ const outer = options.signal ?? new AbortController().signal;
110
+ let deadline = Date.now() + timeoutMs + 30_000;
111
+ let missingDeadline = Date.now() + 30_000;
112
+ async function http(
113
+ path: string,
114
+ body?: unknown,
115
+ ): Promise<Record<string, unknown>> {
116
+ for (;;) {
117
+ outer.throwIfAborted();
118
+ if (Date.now() >= deadline)
119
+ throw new AgentRelayTransportError(
120
+ "Relay task status unavailable before its reconciliation deadline",
121
+ );
122
+ const bounded = AbortSignal.any([
123
+ outer,
124
+ AbortSignal.timeout(
125
+ Math.min(15_000, Math.max(1, deadline - Date.now())),
126
+ ),
127
+ ]);
128
+ let response: Response | undefined;
129
+ let value: unknown;
165
130
  try {
166
- await doFetch(closeUrl, {
167
- method: 'POST',
168
- headers,
169
- body: JSON.stringify({ reason: 'sdk_transport_close' }),
131
+ response = await doFetch(new URL(path, baseUrl), {
132
+ method: body === undefined ? "GET" : "POST",
133
+ redirect: "error",
134
+ signal: bounded,
135
+ headers: {
136
+ authorization: `Bearer ${env.agentToken}`,
137
+ "content-type": "application/json",
138
+ ...(body === undefined
139
+ ? {}
140
+ : { "Idempotency-Key": request.idempotencyKey }),
141
+ },
142
+ ...(body === undefined ? {} : { body: canonicalize(body) }),
170
143
  });
171
- } catch {
172
- // Deliberate: close is advisory in this minimum-viable slice.
144
+ if (
145
+ response.status === 404 &&
146
+ body === undefined &&
147
+ path.includes("/invocations/")
148
+ ) {
149
+ if (Date.now() >= missingDeadline)
150
+ throw new AgentRelayTransportError(
151
+ "Relay task dispatch remains unconfirmed; no repeat POST is permitted",
152
+ );
153
+ } else if (
154
+ !response.ok &&
155
+ response.status !== 429 &&
156
+ response.status < 500
157
+ ) {
158
+ // Never copy untrusted response bodies, URLs, or tokens into diagnostics.
159
+ throw new AgentRelayTransportError(
160
+ `Relay task request refused with HTTP ${response.status}`,
161
+ );
162
+ }
163
+ if (response.ok) value = await response.json();
164
+ if (body !== undefined && !response.ok) return {}; // ambiguous POST: GET only below
165
+ } catch (error) {
166
+ outer.throwIfAborted();
167
+ if (error instanceof AgentRelayTransportError) throw error;
168
+ if (body !== undefined) return {}; // response loss or invalid JSON never causes another POST
169
+ if (response?.ok && !bounded.aborted)
170
+ throw new AgentRelayTransportError(
171
+ "Relay task response was not valid JSON",
172
+ );
173
+ }
174
+ outer.throwIfAborted();
175
+ if (response?.ok && value !== undefined) {
176
+ if (!isRecord(value) || value.ok !== true || !isRecord(value.data))
177
+ throw new AgentRelayTransportError(
178
+ "Relay task response has an invalid data envelope",
179
+ );
180
+ return value.data;
173
181
  }
182
+ await delay(pollMs, undefined, { signal: outer });
183
+ }
184
+ }
185
+ // Resolving the agent is read-only. Pin identity before any invocation so a
186
+ // restarted runner cannot create a second task using another caller's key.
187
+ const agent = await http("/v1/agent");
188
+ if (!nonempty(agent.id))
189
+ throw new AgentRelayTransportError("Relay task caller identity is missing");
190
+ if (!nonempty(agent.workspace_id))
191
+ throw new AgentRelayTransportError(
192
+ "Relay task workspace identity is missing",
193
+ );
194
+ // Pinned action-invoke-v1 identity contract from Relaycast #436.
195
+ const invocationId =
196
+ "inv_idem_" +
197
+ createHash("sha256")
198
+ .update(
199
+ [
200
+ "action-invoke-v1",
201
+ agent.workspace_id,
202
+ agent.id,
203
+ "task.run",
204
+ request.idempotencyKey,
205
+ ].join("\0"),
206
+ )
207
+ .digest("hex");
208
+ const input = {
209
+ cli: request.cli,
210
+ task: request.task,
211
+ ...(request.model === undefined ? {} : { model: request.model }),
212
+ ...(request.worker_cwd === undefined
213
+ ? {}
214
+ : { worker_cwd: request.worker_cwd }),
215
+ ...(request.result_schema === undefined
216
+ ? {}
217
+ : { result_schema: request.result_schema }),
218
+ task_context: {
219
+ run_id: request.runId,
220
+ step_id: request.stepId,
221
+ dispatch_id: request.idempotencyKey,
222
+ timeout_ms: timeoutMs,
174
223
  },
175
224
  };
176
- }
177
-
178
- /**
179
- * Derive a workspace-unique agent name from a run identity + step id. The
180
- * result is stable across replays of the same step so DMs can be addressed
181
- * even during retries.
182
- */
183
- export function deriveAgentName(runId: string, stepId: string): string {
184
- // Keep readable + collision-safe: prefix with 'flow-' and use lower-kebab.
185
- const clean = (s: string): string => s.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
186
- return `flow-${clean(runId)}-${clean(stepId)}`.slice(0, 96);
225
+ const { claim, created } = await claimRelayTask(request.dataDir, {
226
+ version: 1,
227
+ baseUrl,
228
+ callerId: agent.id,
229
+ workspaceId: agent.workspace_id,
230
+ invocationId,
231
+ runId: request.runId,
232
+ stepId: request.stepId,
233
+ idempotencyKey: request.idempotencyKey,
234
+ input,
235
+ startedAt: Date.now(),
236
+ });
237
+ // A restarted runner may recover a terminal receipt after the task deadline.
238
+ // Give that read one bounded window; never reopen or extend the remote task.
239
+ deadline = Math.max(
240
+ claim.startedAt + timeoutMs + 30_000,
241
+ Date.now() + 15_000,
242
+ );
243
+ missingDeadline = claim.startedAt + 30_000;
244
+ if (created) {
245
+ const ack = await http("/v1/actions/task.run/invoke", { input });
246
+ if (
247
+ Object.keys(ack).length &&
248
+ (ack.invocation_id !== invocationId ||
249
+ ack.action_name !== "task.run" ||
250
+ canonicalize(ack.input) !== canonicalize(input))
251
+ ) {
252
+ throw new AgentRelayTransportError(
253
+ "Relay task acknowledgment has mismatched invocation or input",
254
+ );
255
+ }
256
+ }
257
+ let previous: RelayTaskReceipt | undefined;
258
+ for (;;) {
259
+ const value = await http(
260
+ `/v1/actions/task.run/invocations/${encodeURIComponent(invocationId)}`,
261
+ );
262
+ if (value.caller_id !== agent.id)
263
+ throw new AgentRelayTransportError(
264
+ "Relay task receipt belongs to another caller",
265
+ );
266
+ const receipt = readTaskReceipt(value, invocationId, input, previous);
267
+ outer.throwIfAborted();
268
+ if (receipt.status === "completed" || receipt.status === "failed")
269
+ return receipt;
270
+ previous = receipt;
271
+ await delay(pollMs, undefined, { signal: outer });
272
+ }
187
273
  }
@@ -0,0 +1,17 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /** Stable retry identity for one operation in one durable authored root. */
4
+ export function authoredChildAdmissionKey(
5
+ rootRunId: string | undefined,
6
+ stepId: string,
7
+ ): string | undefined {
8
+ if (rootRunId === undefined) return undefined;
9
+ return `authored-child:${createHash('sha256').update(`${rootRunId}\0${stepId}`).digest('hex')}`;
10
+ }
11
+
12
+ /** Stable local worker surface for retries of one caller-owned root start. */
13
+ export function authoredLocalAgentStream(admissionIdentity: string): string {
14
+ return `local-agent-${createHash('sha256')
15
+ .update(`authored-root-worker\0${admissionIdentity}`)
16
+ .digest('hex')}`;
17
+ }
@@ -20,8 +20,9 @@ export class AuthoredBudget {
20
20
  }
21
21
  }
22
22
 
23
- async execute<T>(journal: JournalClient, spec: KernelRunSpec, consume: (outcome: RunOutcome) => Promise<T>): Promise<T> {
24
- if (this.limit === undefined) return consume(await journal.runStart(spec));
23
+ async execute<T>(journal: JournalClient, spec: KernelRunSpec,
24
+ consume: (outcome: RunOutcome) => Promise<T>, admissionKey?: string): Promise<T> {
25
+ if (this.limit === undefined) return consume(await journal.runStart(spec, undefined, admissionKey));
25
26
  const previous = this.tail;
26
27
  let release!: () => void;
27
28
  this.tail = new Promise<void>(resolve => { release = resolve; });
@@ -38,7 +39,7 @@ export class AuthoredBudget {
38
39
  tokens_in: exactNumber(total.input), tokens_out: exactNumber(total.output),
39
40
  dollars: `${total.micro / 1_000_000n}.${String(total.micro % 1_000_000n).padStart(6, '0')}`,
40
41
  wallclock_ms: exactNumber(total.ms), ...(this.limit.window === 'day' && day !== undefined ? { day } : {}),
41
- } } });
42
+ } } }, undefined, admissionKey);
42
43
  try {
43
44
  if (outcome.completion_reason === 'budget_exceeded') throw new AuthoredFlowExecutionError('step_failed', 'Flow budget exceeded before the next step.', 'budget_exceeded', outcome.run_id);
44
45
  return await consume(outcome);
@@ -9,7 +9,6 @@ export type AuthoredFlowExecutionErrorCode =
9
9
  | 'helper_slack.credential_missing'
10
10
  | 'helper_slack.mount_required'
11
11
  | 'budget_syntax_invalid'
12
- | 'budget_missing_price'
13
12
  | 'agent_cli_unresolved'
14
13
  | 'agent_parked'
15
14
  | 'llm_cli_unresolved'
@@ -1,3 +1,4 @@
1
+ import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
1
2
  import { pluginHelpers } from './plugin-loader.js';
2
3
  import { runPluginEffect } from './authored-plugin-effect.js';
3
4
  import { randomUUID } from 'node:crypto';
@@ -71,7 +72,16 @@ export interface AuthoredFlowJournalStep {
71
72
  readonly completionReason: ProtocolCompletionReason;
72
73
  }
73
74
 
75
+ export interface AuthoredExecutionRuntime {
76
+ readonly kind: 'node';
77
+ readonly version: string;
78
+ readonly executableSha256: string;
79
+ readonly payloadSha256: string;
80
+ }
81
+
74
82
  export interface AuthoredFlowExecutionResult {
83
+ readonly executionRuntime?: AuthoredExecutionRuntime;
84
+ readonly rootRunId?: string;
75
85
  readonly name: string;
76
86
  readonly completionReason: FlowCompletionReason;
77
87
  readonly journalSteps: readonly AuthoredFlowJournalStep[];
@@ -122,6 +132,8 @@ export interface ExecuteAuthoredFlowOptions {
122
132
  readonly onWait?: RunLifecycleOptions['onWait'];
123
133
  readonly onProgress?: (event: ProgressEvent) => void;
124
134
  readonly localAgentStream?: string;
135
+ /** Durable kernel root that owns this body's child admission identities. */
136
+ readonly rootRunId?: string;
125
137
  }
126
138
 
127
139
  export async function executeAuthoredFlow<Input = undefined>(
@@ -130,6 +142,7 @@ export async function executeAuthoredFlow<Input = undefined>(
130
142
  input?: Input,
131
143
  options: ExecuteAuthoredFlowOptions = {},
132
144
  ): Promise<AuthoredFlowExecutionResult> {
145
+ assertAuthoredPromiseHooks();
133
146
  const getDefinition = options.getDefinition ?? getAuthoredFlowDefinition;
134
147
  const localAgentStream = options.localAgentStream;
135
148
  const onProgress = options.onProgress;
@@ -173,9 +186,14 @@ export async function executeAuthoredFlow<Input = undefined>(
173
186
  let nextStep = 1;
174
187
  let requestedCompletion: FlowCompletionReason | undefined;
175
188
 
176
- const lowerDeterministic = authoredDeterministicRunner(definition.name, journal, journalSteps, budget);
189
+ const lowerDeterministic = authoredDeterministicRunner(
190
+ definition.name, journal, journalSteps, budget, options.rootRunId,
191
+ );
177
192
 
178
- const worker = authoredWorkerRunner(definition, journal, flowPath, journalSteps, waitOptions, localAgentStream, budget, definition.header.budget);
193
+ const worker = authoredWorkerRunner(
194
+ definition, journal, flowPath, journalSteps, waitOptions,
195
+ localAgentStream, budget, definition.header.budget, options.rootRunId,
196
+ );
179
197
 
180
198
  function llmOperation(strings: TemplateStringsArray, ...values: unknown[]): Step<string>;
181
199
  function llmOperation(prompt: string, options: LlmOptions): Step<unknown>;
@@ -205,7 +223,7 @@ export async function executeAuthoredFlow<Input = undefined>(
205
223
  return trackStep(authoredSteps, llmOp);
206
224
  }
207
225
 
208
- const slackRun = randomUUID();
226
+ const slackRun = options.rootRunId ?? randomUUID();
209
227
  function slackOperation<T>(call: SlackCall): Step<T> {
210
228
  assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion);
211
229
  const snapshot = snapshotJsonValue(call, 'f.slack call') as unknown as SlackCall;
@@ -215,7 +233,7 @@ export async function executeAuthoredFlow<Input = undefined>(
215
233
  () => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion),
216
234
  async () => {
217
235
  const receipt = await runHelperEffect(journal, definition.name, id, snapshot,
218
- options.dataDir ?? dirname(journal.socketPath), journalSteps);
236
+ options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId);
219
237
  return (call.verb === 'react' ? undefined : receipt) as T;
220
238
  },
221
239
  lifecycle,
@@ -231,7 +249,7 @@ export async function executeAuthoredFlow<Input = undefined>(
231
249
  return trackStep(authoredSteps, new AuthoredFlowOperation<T>(id, verb,
232
250
  () => assertOperationAllowed(verb, definition.name, requestedCompletion),
233
251
  async () => await runHelperEffect(journal, definition.name, id, snapshot,
234
- options.dataDir ?? dirname(journal.socketPath), journalSteps) as T,
252
+ options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId) as T,
235
253
  lifecycle));
236
254
  }),
237
255
  slack: {
@@ -247,7 +265,7 @@ export async function executeAuthoredFlow<Input = undefined>(
247
265
  id, 'mcp',
248
266
  () => assertOperationAllowed('mcp', definition.name, requestedCompletion),
249
267
  () => runMcpEffect(journal, definition.name, id, server, tool, args,
250
- known, checkedMcp.servers[server]!, journalSteps),
268
+ known, checkedMcp.servers[server]!, journalSteps, options.rootRunId),
251
269
  lifecycle,
252
270
  ));
253
271
  }),
@@ -327,7 +345,8 @@ export async function executeAuthoredFlow<Input = undefined>(
327
345
  const id = `plugin-${nextStep++}`;
328
346
  return trackStep(authoredSteps, new AuthoredFlowOperation(
329
347
  id, label, () => assertOperationAllowed(label, definition.name, requestedCompletion),
330
- () => runPluginEffect(journal, definition.name, id, plugin, verb, args, journalSteps, budget),
348
+ () => runPluginEffect(journal, definition.name, id, plugin, verb, args,
349
+ journalSteps, budget, options.rootRunId),
331
350
  lifecycle,
332
351
  ));
333
352
  }));
@@ -387,6 +406,7 @@ export async function executeAuthoredFlow<Input = undefined>(
387
406
  await lowerDeterministic(`complete-${nextStep}`, requestedCompletion === 'needs_human'
388
407
  ? `printf '%s' '{"completionReason":"needs_human"}'` : ':', true);
389
408
  return Object.freeze({
409
+ ...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
390
410
  name: definition.name,
391
411
  completionReason: requestedCompletion,
392
412
  journalSteps: Object.freeze([...journalSteps]),