@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.
- package/dist/adapters/base.d.ts +6 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +1 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/agent-relay-receipt.d.ts +24 -0
- package/dist/agent-relay-receipt.d.ts.map +1 -0
- package/dist/agent-relay-receipt.js +66 -0
- package/dist/agent-relay-receipt.js.map +1 -0
- package/dist/agent-relay-state.d.ts +18 -0
- package/dist/agent-relay-state.d.ts.map +1 -0
- package/dist/agent-relay-state.js +58 -0
- package/dist/agent-relay-state.js.map +1 -0
- package/dist/agent-relay-transport.d.ts +17 -70
- package/dist/agent-relay-transport.d.ts.map +1 -1
- package/dist/agent-relay-transport.js +177 -112
- package/dist/agent-relay-transport.js.map +1 -1
- package/dist/authored-admission.d.ts +5 -0
- package/dist/authored-admission.d.ts.map +1 -0
- package/dist/authored-admission.js +14 -0
- package/dist/authored-admission.js.map +1 -0
- package/dist/authored-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +3 -3
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +10 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +10 -7
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +20 -0
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +86 -11
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.d.ts +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +5 -3
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +7 -4
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.d.ts +2 -0
- package/dist/authored-node-entry.d.ts.map +1 -0
- package/dist/authored-node-entry.js +111 -0
- package/dist/authored-node-entry.js.map +1 -0
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -0
- package/dist/authored-node-runner.js +246 -0
- package/dist/authored-node-runner.js.map +1 -0
- package/dist/authored-plugin-effect.d.ts +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +7 -4
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-root.d.ts +38 -0
- package/dist/authored-root.d.ts.map +1 -0
- package/dist/authored-root.js +329 -0
- package/dist/authored-root.js.map +1 -0
- package/dist/authored-runtime-capability.d.ts +6 -0
- package/dist/authored-runtime-capability.d.ts.map +1 -0
- package/dist/authored-runtime-capability.js +40 -0
- package/dist/authored-runtime-capability.js.map +1 -0
- package/dist/authored-source-authority.d.ts +4 -0
- package/dist/authored-source-authority.d.ts.map +1 -0
- package/dist/authored-source-authority.js +61 -0
- package/dist/authored-source-authority.js.map +1 -0
- package/dist/authored-worker-step.d.ts +2 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +11 -6
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts +17 -3
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +31 -24
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/check.js +3 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +18 -14
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +56 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli-adapter.d.ts +16 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +17 -0
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/cloud-run.d.ts +11 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +65 -19
- package/dist/cloud-run.js.map +1 -1
- package/dist/failure-kinds.d.ts +7 -3
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +5 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +1 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +6 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +2 -2
- package/dist/local-agent.js.map +1 -1
- package/dist/model-pricing.d.ts +3 -4
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +4 -4
- package/dist/model-pricing.js.map +1 -1
- package/dist/preflight.d.ts +4 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +36 -18
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +5 -4
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts +5 -2
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +43 -46
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts +4 -3
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +13 -4
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +12 -4
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/claude.ts +1 -0
- package/src/agent-relay-receipt.ts +120 -0
- package/src/agent-relay-state.ts +73 -0
- package/src/agent-relay-transport.ts +242 -156
- package/src/authored-admission.ts +17 -0
- package/src/authored-budget.ts +4 -3
- package/src/authored-flow-error.ts +0 -1
- package/src/authored-flow-executor.ts +27 -7
- package/src/authored-flow-loader.ts +96 -13
- package/src/authored-helper-effect.ts +5 -3
- package/src/authored-mcp.ts +6 -4
- package/src/authored-node-entry.ts +90 -0
- package/src/authored-node-runner.ts +219 -0
- package/src/authored-plugin-effect.ts +6 -3
- package/src/authored-root.ts +396 -0
- package/src/authored-runtime-capability.ts +40 -0
- package/src/authored-source-authority.ts +62 -0
- package/src/authored-worker-step.ts +15 -4
- package/src/budget-preflight.ts +41 -25
- package/src/cli/check.ts +3 -1
- package/src/cli/direct-run.ts +24 -16
- package/src/cli/run.ts +58 -1
- package/src/cli-adapter.ts +27 -0
- package/src/cli.ts +5 -5
- package/src/cloud-run.ts +74 -21
- package/src/failure-kinds.ts +5 -1
- package/src/journal-client.ts +6 -2
- package/src/llm-worker.ts +4 -2
- package/src/local-agent.ts +7 -2
- package/src/model-pricing.ts +4 -4
- package/src/preflight.ts +40 -18
- package/src/protocol.ts +2 -0
- package/src/spec.ts +5 -4
- package/src/worker-cli.ts +50 -54
- package/src/worker-spend.ts +15 -4
- package/src/worker.ts +12 -4
|
@@ -1,187 +1,273 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 =
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
32
|
+
constructor(
|
|
33
|
+
message: string,
|
|
34
|
+
public readonly cause?: unknown,
|
|
35
|
+
) {
|
|
64
36
|
super(message);
|
|
65
|
-
this.name =
|
|
37
|
+
this.name = "AgentRelayTransportError";
|
|
66
38
|
}
|
|
67
39
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
46
|
+
"Relay task transport requires a pre-provisioned RELAY_AGENT_TOKEN.",
|
|
80
47
|
);
|
|
81
|
-
}
|
|
82
48
|
return {
|
|
83
|
-
baseUrl,
|
|
84
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
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
|
-
|
|
147
|
-
cause,
|
|
84
|
+
"Relay task credentials require HTTPS outside literal loopback addresses",
|
|
148
85
|
);
|
|
149
86
|
}
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
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
|
-
|
|
102
|
+
"Relay task requires durable dispatch identity and a valid deadline",
|
|
155
103
|
);
|
|
156
104
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
167
|
-
method:
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}
|
package/src/authored-budget.ts
CHANGED
|
@@ -20,8 +20,9 @@ export class AuthoredBudget {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async execute<T>(journal: JournalClient, spec: KernelRunSpec,
|
|
24
|
-
|
|
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);
|
|
@@ -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(
|
|
189
|
+
const lowerDeterministic = authoredDeterministicRunner(
|
|
190
|
+
definition.name, journal, journalSteps, budget, options.rootRunId,
|
|
191
|
+
);
|
|
177
192
|
|
|
178
|
-
const worker = authoredWorkerRunner(
|
|
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,
|
|
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]),
|