@relayflows/sdk 2.0.10 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-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/worker-cli.d.ts +5 -2
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +38 -42
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +8 -2
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- 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/worker-cli.ts +44 -50
- package/src/worker.ts +8 -2
|
@@ -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
|
}
|
package/src/worker-cli.ts
CHANGED
|
@@ -15,8 +15,7 @@ import {
|
|
|
15
15
|
} from './wrapper-session.js';
|
|
16
16
|
import { wrapperEnvironment } from './wrapper-runtime.js';
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
deriveAgentName,
|
|
18
|
+
runAgentRelayTask,
|
|
20
19
|
AgentRelayTransportError,
|
|
21
20
|
type AgentTransport,
|
|
22
21
|
} from './agent-relay-transport.js';
|
|
@@ -32,6 +31,7 @@ export const WAKE_CONTEXT_ENV = 'RELAYFLOW_WAKE_CONTEXT';
|
|
|
32
31
|
export const MODEL_ENV = 'RELAYFLOW_MODEL';
|
|
33
32
|
|
|
34
33
|
export interface WorkerCliResult {
|
|
34
|
+
relay_task?: import('./agent-relay-receipt.js').RelayTaskReceipt;
|
|
35
35
|
tokens_input?: number;
|
|
36
36
|
tokens_output?: number;
|
|
37
37
|
exit_code: number | null;
|
|
@@ -40,12 +40,14 @@ export interface WorkerCliResult {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
* stable, DM-addressable agent name (flows#385).
|
|
43
|
+
* Journal identity and durable dispatch storage used by the Relay task transport.
|
|
45
44
|
*/
|
|
46
45
|
export interface AgentRelayContext {
|
|
47
46
|
runId: string;
|
|
48
47
|
stepId: string;
|
|
48
|
+
idempotencyKey: string;
|
|
49
|
+
dataDir?: string;
|
|
50
|
+
resultSchema?: unknown;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export async function runAgentCli(
|
|
@@ -67,6 +69,10 @@ export async function runAgentCli(
|
|
|
67
69
|
}
|
|
68
70
|
const kind = cliAdapterKind(cli);
|
|
69
71
|
|
|
72
|
+
if (mode === 'agent' && transport === 'relay') {
|
|
73
|
+
return runViaAgentRelay(kind, instruction, wakeContext, model, relayContext, cwd, signal);
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
if (kind === 'relayflows-wrapper-v1') {
|
|
71
77
|
return requirePricedUsage(decodeWrapperResult(await runWrapperSession(
|
|
72
78
|
cli,
|
|
@@ -79,10 +85,6 @@ export async function runAgentCli(
|
|
|
79
85
|
)), model);
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
if (mode === 'agent' && transport === 'relay') {
|
|
83
|
-
return runViaAgentRelay(kind, instruction, model, relayContext, cwd);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
88
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
87
89
|
delete env[WAKE_CONTEXT_ENV];
|
|
88
90
|
delete env[MODEL_ENV];
|
|
@@ -107,54 +109,46 @@ export async function runAgentCli(
|
|
|
107
109
|
return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd), kind), model);
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
/**
|
|
111
|
-
* Relay-transport path (flows#385). Fires an agent-relay spawn HTTP request
|
|
112
|
-
* that registers the CLI as a first-class workspace participant DMs can
|
|
113
|
-
* steer, then reports the spawn outcome as a WorkerCliResult. Streaming
|
|
114
|
-
* completion tracking is a follow-up — this initial slice proves the
|
|
115
|
-
* transport wire and returns quickly with the registered name in stdout.
|
|
116
|
-
*/
|
|
112
|
+
/** Wait under the same worker lease for an authoritative task receipt. */
|
|
117
113
|
async function runViaAgentRelay(
|
|
118
|
-
kind: CliAdapterKind,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
relayContext: AgentRelayContext | undefined,
|
|
122
|
-
worker_cwd: string | undefined,
|
|
114
|
+
kind: CliAdapterKind, instruction: string, wakeContext: unknown,
|
|
115
|
+
model: string | undefined, context: AgentRelayContext | undefined,
|
|
116
|
+
worker_cwd: string | undefined, signal: AbortSignal | undefined,
|
|
123
117
|
): Promise<WorkerCliResult> {
|
|
124
|
-
if (kind === 'relayflows-wrapper-v1') {
|
|
125
|
-
return {
|
|
126
|
-
exit_code: null,
|
|
127
|
-
stdout_tail: '',
|
|
128
|
-
stderr_tail: 'agent-relay transport does not support the relayflows-wrapper-v1 same-process session.',
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (relayContext === undefined) {
|
|
132
|
-
return {
|
|
133
|
-
exit_code: null,
|
|
134
|
-
stdout_tail: '',
|
|
135
|
-
stderr_tail: 'agent-relay transport requires a relayContext (runId, stepId); the caller did not thread it through.',
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
const name = deriveAgentName(relayContext.runId, relayContext.stepId);
|
|
139
118
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
task
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
119
|
+
if (kind === 'relayflows-wrapper-v1') throw new Error('Relay task transport does not support same-process wrappers.');
|
|
120
|
+
if (!context?.dataDir) throw new Error('Relay task transport requires a durable data directory and journal dispatch identity.');
|
|
121
|
+
const task = instruction + (wakeContext === undefined ? '' : `\n\nWake context (journaled):\n${JSON.stringify(wakeContext)}`)
|
|
122
|
+
+ '\n\nReport the final task output with the injected agent_result tool and final=true. Wait for its successful durable acknowledgment before exiting.';
|
|
123
|
+
const received = await runAgentRelayTask({
|
|
124
|
+
cli: kind, task, model, worker_cwd, result_schema: context.resultSchema,
|
|
125
|
+
runId: context.runId, stepId: context.stepId, idempotencyKey: context.idempotencyKey,
|
|
126
|
+
dataDir: context.dataDir,
|
|
127
|
+
}, { signal });
|
|
128
|
+
const receipt = { ...received, error: received.error === null ? null : redactRelayError(received.error) };
|
|
129
|
+
const accounting = receipt.task_execution.accounting;
|
|
130
|
+
const result: WorkerCliResult = {
|
|
131
|
+
relay_task: receipt, exit_code: receipt.status === 'completed' ? 0 : 1,
|
|
132
|
+
stdout_tail: receipt.status === 'completed' ? JSON.stringify(receipt.output) : '',
|
|
133
|
+
stderr_tail: receipt.status === 'failed' ? `Relay task failed: ${receipt.error}` : '',
|
|
134
|
+
...(accounting?.tokens_input === undefined ? {} : { tokens_input: accounting.tokens_input }),
|
|
135
|
+
...(accounting?.tokens_output === undefined ? {} : { tokens_output: accounting.tokens_output }),
|
|
151
136
|
};
|
|
137
|
+
return requirePricedUsage(result, model);
|
|
152
138
|
} catch (error) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
:
|
|
156
|
-
return { exit_code: null, stdout_tail: '', stderr_tail: detail };
|
|
139
|
+
signal?.throwIfAborted();
|
|
140
|
+
const detail = error instanceof AgentRelayTransportError ? error.message
|
|
141
|
+
: error instanceof Error ? error.message : 'Relay task transport failed';
|
|
142
|
+
return { exit_code: null, stdout_tail: '', stderr_tail: redactRelayError(detail) };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function redactRelayError(message: string): string {
|
|
147
|
+
for (const key of ['RELAY_AGENT_TOKEN', 'RELAY_API_KEY']) {
|
|
148
|
+
const secret = process.env[key];
|
|
149
|
+
if (secret) message = message.replaceAll(secret, '[redacted]');
|
|
157
150
|
}
|
|
151
|
+
return message.replace(/\b(?:at|rk|nt|ot|br|arr)_(?:live_)?[A-Za-z0-9_-]+/g, '[redacted]');
|
|
158
152
|
}
|
|
159
153
|
|
|
160
154
|
async function spawnInvocation(
|
package/src/worker.ts
CHANGED
|
@@ -112,7 +112,8 @@ export class AgentWorker extends EventEmitter {
|
|
|
112
112
|
onReady: this.options.onPtyReady, onDrive: () => { humanIntervention = true; },
|
|
113
113
|
}, typeof spec.cwd === 'string' ? spec.cwd : undefined,
|
|
114
114
|
spec.transport === 'relay' ? 'relay' : 'direct',
|
|
115
|
-
{ runId: dispatch.run_id, stepId: dispatch.step_id
|
|
115
|
+
{ runId: dispatch.run_id, stepId: dispatch.step_id, idempotencyKey: dispatch.idempotency_key,
|
|
116
|
+
dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema })
|
|
116
117
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'agent step has no declared CLI' }));
|
|
117
118
|
const { result, usage } = workerSpend(completed, spec.model);
|
|
118
119
|
const completionReason = result.exit_code === 0 ? 'success' : 'worker_error';
|
|
@@ -130,7 +131,8 @@ export class AgentWorker extends EventEmitter {
|
|
|
130
131
|
// emitting an error JSON with exit 0. `completionReason` is
|
|
131
132
|
// derived from exit code, so a CLI that exits 0 while emitting
|
|
132
133
|
// `{"error":...}` will report success with an error payload.
|
|
133
|
-
const output =
|
|
134
|
+
const output = result.relay_task?.status === 'completed' && result.exit_code === 0
|
|
135
|
+
? result.relay_task.output : parseJsonOutput(result.stdout_tail) ?? result;
|
|
134
136
|
|
|
135
137
|
await this.client.stepComplete(
|
|
136
138
|
dispatch.run_id,
|
|
@@ -140,6 +142,10 @@ export class AgentWorker extends EventEmitter {
|
|
|
140
142
|
completionReason,
|
|
141
143
|
{
|
|
142
144
|
output,
|
|
145
|
+
...(result.relay_task === undefined ? {} : { trajectory_tail: { relay_task: {
|
|
146
|
+
invocation_id: result.relay_task.invocation_id, status: result.relay_task.status,
|
|
147
|
+
task_execution: result.relay_task.task_execution, error: result.relay_task.error,
|
|
148
|
+
} } }),
|
|
143
149
|
...(humanIntervention ? { human_intervention: true } : {}),
|
|
144
150
|
...(usage !== undefined ? { usage } : {}),
|
|
145
151
|
started_pins: dispatch.pins,
|