@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.
@@ -0,0 +1,24 @@
1
+ export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
2
+ export declare const nonempty: (value: unknown) => value is string;
3
+ export interface RelayTaskExecution {
4
+ execution_id: string;
5
+ run_id: string;
6
+ step_id: string;
7
+ dispatch_id: string;
8
+ deadline: string;
9
+ worker_generation?: string;
10
+ accepted_at?: string;
11
+ accounting?: Record<string, number>;
12
+ }
13
+ export interface RelayTaskReceipt {
14
+ invocation_id: string;
15
+ action_name: "task.run";
16
+ status: "pending" | "dispatched" | "running" | "completed" | "failed";
17
+ task_execution: RelayTaskExecution;
18
+ output: unknown;
19
+ error: string | null;
20
+ completed_at: string | null;
21
+ }
22
+ /** GET is authoritative. A spawn/POST acknowledgment is never a task result. */
23
+ export declare function readTaskReceipt(value: unknown, invocationId: string, input: Record<string, unknown>, previous?: RelayTaskReceipt): RelayTaskReceipt;
24
+ //# sourceMappingURL=agent-relay-receipt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-relay-receipt.d.ts","sourceRoot":"","sources":["../src/agent-relay-receipt.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACH,CAAC;AACvE,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACJ,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AACD,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,UAAU,CAAC;IACxB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtE,cAAc,EAAE,kBAAkB,CAAC;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,gBAAgB,CAsFlB"}
@@ -0,0 +1,66 @@
1
+ import { canonicalize } from "./canonical.js";
2
+ export const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ export const nonempty = (value) => typeof value === "string" && value.length > 0;
4
+ /** GET is authoritative. A spawn/POST acknowledgment is never a task result. */
5
+ export function readTaskReceipt(value, invocationId, input, previous) {
6
+ const context = input.task_context;
7
+ if (!isRecord(value) ||
8
+ value.invocation_id !== invocationId ||
9
+ value.action_name !== "task.run" ||
10
+ canonicalize(value.input) !== canonicalize(input) ||
11
+ !isRecord(value.task_execution)) {
12
+ throw new Error("Relay task receipt has mismatched invocation or input");
13
+ }
14
+ const execution = value.task_execution;
15
+ const validDate = (date) => nonempty(date) && Number.isFinite(Date.parse(date));
16
+ if (!nonempty(execution.execution_id) ||
17
+ !validDate(execution.deadline) ||
18
+ ["run_id", "step_id", "dispatch_id"].some((key) => execution[key] !== context[key]) ||
19
+ !["pending", "dispatched", "running", "completed", "failed"].includes(String(value.status))) {
20
+ throw new Error("Relay task receipt has invalid execution correlation");
21
+ }
22
+ const accepted = nonempty(execution.worker_generation) && validDate(execution.accepted_at);
23
+ if ((value.status === "running" || value.status === "completed") &&
24
+ !accepted) {
25
+ throw new Error("Relay task receipt is missing durable acceptance");
26
+ }
27
+ if ((execution.worker_generation !== undefined ||
28
+ execution.accepted_at !== undefined) &&
29
+ !accepted) {
30
+ throw new Error("Relay task receipt has incomplete durable acceptance");
31
+ }
32
+ if (previous !== undefined &&
33
+ (previous.task_execution.execution_id !== execution.execution_id ||
34
+ previous.task_execution.deadline !== execution.deadline ||
35
+ (previous.task_execution.worker_generation !== undefined &&
36
+ (previous.task_execution.worker_generation !==
37
+ execution.worker_generation ||
38
+ previous.task_execution.accepted_at !== execution.accepted_at)))) {
39
+ throw new Error("Relay task receipt changed its execution, accepted generation/time, or deadline");
40
+ }
41
+ if (execution.accounting !== undefined &&
42
+ (!isRecord(execution.accounting) ||
43
+ Object.values(execution.accounting).some((n) => typeof n !== "number" || !Number.isFinite(n) || n < 0))) {
44
+ throw new Error("Relay task receipt has invalid accounting");
45
+ }
46
+ if (value.status === "completed" &&
47
+ (!Object.hasOwn(value, "output") ||
48
+ value.error !== null ||
49
+ !validDate(value.completed_at))) {
50
+ throw new Error("Relay task receipt has no authoritative final output");
51
+ }
52
+ if (value.status === "failed" &&
53
+ (!nonempty(value.error) || !validDate(value.completed_at))) {
54
+ throw new Error("Relay task failure receipt has no reason");
55
+ }
56
+ return {
57
+ invocation_id: invocationId,
58
+ action_name: "task.run",
59
+ status: value.status,
60
+ task_execution: execution,
61
+ output: value.output,
62
+ error: value.error,
63
+ completed_at: value.completed_at,
64
+ };
65
+ }
66
+ //# sourceMappingURL=agent-relay-receipt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-relay-receipt.js","sourceRoot":"","sources":["../src/agent-relay-receipt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export interface RelayTaskClaim {
2
+ version: 1;
3
+ baseUrl: string;
4
+ callerId: string;
5
+ workspaceId: string;
6
+ invocationId: string;
7
+ runId: string;
8
+ stepId: string;
9
+ idempotencyKey: string;
10
+ input: Record<string, unknown>;
11
+ startedAt: number;
12
+ }
13
+ /** Exclusive creation pins the request before POST; no last-writer overwrite. */
14
+ export declare function claimRelayTask(dataDir: string, claim: RelayTaskClaim): Promise<{
15
+ claim: RelayTaskClaim;
16
+ created: boolean;
17
+ }>;
18
+ //# sourceMappingURL=agent-relay-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-relay-state.d.ts","sourceRoot":"","sources":["../src/agent-relay-state.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iFAAiF;AACjF,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAkDtD"}
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, open, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { canonicalize } from "./canonical.js";
5
+ /** Exclusive creation pins the request before POST; no last-writer overwrite. */
6
+ export async function claimRelayTask(dataDir, claim) {
7
+ const directory = join(dataDir, "relay-tasks");
8
+ await mkdir(directory, { recursive: true });
9
+ const parent = await open(dataDir, "r");
10
+ try {
11
+ await parent.sync();
12
+ }
13
+ finally {
14
+ await parent.close();
15
+ }
16
+ const digest = createHash("sha256")
17
+ .update(canonicalize([claim.runId, claim.stepId, claim.idempotencyKey]))
18
+ .digest("hex");
19
+ const path = join(directory, `${digest}.json`);
20
+ let file;
21
+ try {
22
+ file = await open(path, "wx", 0o600);
23
+ }
24
+ catch (error) {
25
+ if (error.code !== "EEXIST")
26
+ throw error;
27
+ const prior = JSON.parse(await readFile(path, "utf8"));
28
+ if (typeof prior !== "object" ||
29
+ prior === null ||
30
+ !("startedAt" in prior) ||
31
+ typeof prior.startedAt !== "number" ||
32
+ !Number.isSafeInteger(prior.startedAt) ||
33
+ prior.startedAt < 0 ||
34
+ canonicalize(prior) !==
35
+ canonicalize({ ...claim, startedAt: prior.startedAt })) {
36
+ throw new Error("Relay task dispatch conflicts with its durable caller, endpoint, or input");
37
+ }
38
+ return { claim: prior, created: false };
39
+ }
40
+ // A crash during creation leaves a partial claim that fails closed on read.
41
+ // Never remove an uncertain claim and retry under a different identity.
42
+ try {
43
+ await file.writeFile(canonicalize(claim));
44
+ await file.sync();
45
+ }
46
+ finally {
47
+ await file.close();
48
+ }
49
+ const dir = await open(directory, "r");
50
+ try {
51
+ await dir.sync();
52
+ }
53
+ finally {
54
+ await dir.close();
55
+ }
56
+ return { claim, created: true };
57
+ }
58
+ //# sourceMappingURL=agent-relay-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-relay-state.js","sourceRoot":"","sources":["../src/agent-relay-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAe9C,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,OAAe,EACf,KAAqB;IAErB,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAC/C,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IACD,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC;SAChC,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC;SACvE,MAAM,CAAC,KAAK,CAAC,CAAC;IACjB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,GAAG,MAAM,OAAO,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC;IACT,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAC;QACpE,MAAM,KAAK,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAChE,IACE,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,KAAK,IAAI;YACd,CAAC,CAAC,WAAW,IAAI,KAAK,CAAC;YACvB,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ;YACnC,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC;YACtC,KAAK,CAAC,SAAS,GAAG,CAAC;YACnB,YAAY,CAAC,KAAK,CAAC;gBACjB,YAAY,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,EACxD,CAAC;YACD,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAuB,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5D,CAAC;IACD,4EAA4E;IAC5E,wEAAwE;IACxE,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IACnB,CAAC;YAAS,CAAC;QACT,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC"}
@@ -1,84 +1,31 @@
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
- */
16
- export type AgentTransport = 'direct' | 'relay';
17
- export interface AgentRelaySpawnRequest {
18
- /** Registered agent name in the workspace. */
19
- name: string;
20
- /** CLI to launch. */
21
- cli: 'claude' | 'codex' | 'gemini' | 'aider' | 'goose' | 'grok' | 'opencode';
22
- /** Initial task instructions. */
1
+ import { type RelayTaskReceipt } from "./agent-relay-receipt.js";
2
+ export type AgentTransport = "direct" | "relay";
3
+ export interface AgentRelayTaskRequest {
4
+ cli: string;
23
5
  task: string;
24
- /** Optional model powering the worker. */
25
6
  model?: string;
26
- /** Optional working directory for the spawned worker process. */
27
7
  worker_cwd?: string;
28
- /** Optional target fleet node name. */
29
- target_node?: string;
30
- /** Declared objective for workforce reporting. */
31
- objective?: string;
32
- /** Declared role for workforce reporting. */
33
- role?: string;
34
- /** Declared project for workforce reporting. */
35
- project?: string;
36
- /** Declared workstream for workforce reporting. */
37
- workstream?: string;
8
+ result_schema?: unknown;
9
+ runId: string;
10
+ stepId: string;
11
+ idempotencyKey: string;
12
+ dataDir: string;
13
+ /** Engine task contract ceiling; bounded independently of the renewing lease. */
14
+ timeoutMs?: number;
38
15
  }
39
16
  export interface AgentRelayEnv {
40
- /** `RELAY_BASE_URL` env; defaults to https://cast.agentrelay.com. */
41
17
  baseUrl?: string;
42
- /** `RELAY_API_KEY` env (rk_live_...). Required. */
43
- apiKey?: string;
44
- /** `RELAY_DEFAULT_WORKSPACE` env; workspace scoping for the spawn. */
45
- workspaceId?: string;
46
- /** Optional bearer token for the individual agent identity, if pre-registered. */
47
18
  agentToken?: string;
48
19
  }
49
- export interface AgentSpawnHandle {
50
- /** Registered agent name in the workspace — DM this to steer. */
51
- readonly registeredName: string;
52
- /** Invocation id returned by relay; use for status polling. */
53
- readonly invocationId: string;
54
- /** Best-effort deregistration/notification. */
55
- close(): Promise<void>;
56
- }
57
20
  export declare class AgentRelayTransportError extends Error {
58
21
  readonly cause?: unknown | undefined;
59
22
  constructor(message: string, cause?: unknown | undefined);
60
23
  }
61
- /**
62
- * Read the same env the relay MCP already consumes. Explicit override wins.
63
- * Missing RELAY_API_KEY refuses immediately the transport cannot proceed
64
- * unauthenticated and the direct-spawn fallback is a separate decision.
65
- */
66
- export declare function readAgentRelayEnv(env?: NodeJS.ProcessEnv): Required<Pick<AgentRelayEnv, 'baseUrl' | 'apiKey'>> & AgentRelayEnv;
67
- /**
68
- * Spawn a worker via agent-relay HTTP. The endpoint mirrors what the
69
- * `mcp__agent-relay__spawn` MCP tool wraps — a POST that requests a fleet
70
- * node dispatch. Returns a handle keyed on the registered agent name.
71
- *
72
- * The `fetch` argument is injected so tests can mock the transport without
73
- * hitting the network.
74
- */
75
- export declare function agentRelaySpawn(request: AgentRelaySpawnRequest, env?: AgentRelayEnv & {
24
+ export declare function readAgentRelayEnv(env?: NodeJS.ProcessEnv): Required<AgentRelayEnv>;
25
+ /** Exact Relaycast #436 HTTP contract. Only terminal GET receipts can return. */
26
+ export declare function runAgentRelayTask(request: AgentRelayTaskRequest, options?: AgentRelayEnv & {
76
27
  fetch?: typeof fetch;
77
- }): Promise<AgentSpawnHandle>;
78
- /**
79
- * Derive a workspace-unique agent name from a run identity + step id. The
80
- * result is stable across replays of the same step so DMs can be addressed
81
- * even during retries.
82
- */
83
- export declare function deriveAgentName(runId: string, stepId: string): string;
28
+ signal?: AbortSignal;
29
+ pollMs?: number;
30
+ }): Promise<RelayTaskReceipt>;
84
31
  //# sourceMappingURL=agent-relay-transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-relay-transport.d.ts","sourceRoot":"","sources":["../src/agent-relay-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,GAAG,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;IAC7E,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+CAA+C;IAC/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,qBAAa,wBAAyB,SAAQ,KAAK;aACJ,KAAK,CAAC,EAAE,OAAO;gBAAhD,OAAO,EAAE,MAAM,EAAkB,KAAK,CAAC,EAAE,OAAO,YAAA;CAI7D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,QAAQ,CAAC,CAAC,GAAG,aAAa,CAc3I;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,EAC/B,GAAG,GAAE,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CAAO,GACjD,OAAO,CAAC,gBAAgB,CAAC,CA2E3B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIrE"}
1
+ {"version":3,"file":"agent-relay-transport.d.ts","sourceRoot":"","sources":["../src/agent-relay-transport.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,CAAC;AAChD,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AACD,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AACD,qBAAa,wBAAyB,SAAQ,KAAK;aAG/B,KAAK,CAAC,EAAE,OAAO;gBAD/B,OAAO,EAAE,MAAM,EACC,KAAK,CAAC,EAAE,OAAO,YAAA;CAKlC;AACD,wBAAgB,iBAAiB,CAC/B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,QAAQ,CAAC,aAAa,CAAC,CAUzB;AAED,iFAAiF;AACjF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,GAAE,aAAa,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACZ,GACL,OAAO,CAAC,gBAAgB,CAAC,CAmN3B"}
@@ -1,134 +1,199 @@
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 { isRecord, nonempty, readTaskReceipt, } from "./agent-relay-receipt.js";
16
7
  export class AgentRelayTransportError extends Error {
17
8
  cause;
18
9
  constructor(message, cause) {
19
10
  super(message);
20
11
  this.cause = cause;
21
- this.name = 'AgentRelayTransportError';
12
+ this.name = "AgentRelayTransportError";
22
13
  }
23
14
  }
24
- /**
25
- * Read the same env the relay MCP already consumes. Explicit override wins.
26
- * Missing RELAY_API_KEY refuses immediately — the transport cannot proceed
27
- * unauthenticated and the direct-spawn fallback is a separate decision.
28
- */
29
15
  export function readAgentRelayEnv(env = process.env) {
30
- const baseUrl = env['RELAY_BASE_URL']?.trim() || 'https://cast.agentrelay.com';
31
- const apiKey = env['RELAY_API_KEY']?.trim();
32
- if (!apiKey) {
33
- throw new AgentRelayTransportError('agent-relay transport requires RELAY_API_KEY in the environment; falling back to direct transport is the caller\'s responsibility.');
34
- }
16
+ const agentToken = env.RELAY_AGENT_TOKEN?.trim();
17
+ if (!agentToken)
18
+ throw new AgentRelayTransportError("Relay task transport requires a pre-provisioned RELAY_AGENT_TOKEN.");
35
19
  return {
36
- baseUrl,
37
- apiKey,
38
- workspaceId: env['RELAY_DEFAULT_WORKSPACE']?.trim() || undefined,
39
- agentToken: env['RELAY_AGENT_TOKEN']?.trim() || undefined,
20
+ baseUrl: env.RELAY_BASE_URL?.trim() || "https://cast.agentrelay.com",
21
+ agentToken,
40
22
  };
41
23
  }
42
- /**
43
- * Spawn a worker via agent-relay HTTP. The endpoint mirrors what the
44
- * `mcp__agent-relay__spawn` MCP tool wraps — a POST that requests a fleet
45
- * node dispatch. Returns a handle keyed on the registered agent name.
46
- *
47
- * The `fetch` argument is injected so tests can mock the transport without
48
- * hitting the network.
49
- */
50
- export async function agentRelaySpawn(request, env = {}) {
51
- // Only fall back to process.env if the caller didn't supply the required
52
- // fields directly. Tests pass { apiKey, fetch } inline; they shouldn't need
53
- // to also set RELAY_API_KEY in the runner env.
54
- const fromEnv = env.apiKey
55
- ? { baseUrl: env.baseUrl || 'https://cast.agentrelay.com' }
56
- : readAgentRelayEnv();
57
- const resolved = { ...fromEnv, ...env };
58
- const url = new URL('/api/v1/agents/spawn', resolved.baseUrl).toString();
59
- const doFetch = env.fetch ?? globalThis.fetch;
60
- if (typeof doFetch !== 'function') {
61
- throw new AgentRelayTransportError('global fetch is unavailable; provide { fetch } explicitly.');
62
- }
63
- const headers = {
64
- 'content-type': 'application/json',
65
- 'authorization': `Bearer ${resolved.apiKey}`,
66
- };
67
- if (resolved.workspaceId !== undefined)
68
- headers['x-relay-workspace'] = resolved.workspaceId;
69
- if (resolved.agentToken !== undefined)
70
- headers['x-relay-agent-token'] = resolved.agentToken;
71
- let response;
72
- try {
73
- response = await doFetch(url, {
74
- method: 'POST',
75
- headers,
76
- body: JSON.stringify(request),
77
- });
78
- }
79
- catch (cause) {
80
- throw new AgentRelayTransportError(`agent-relay spawn network error at ${url}: ${cause.message ?? String(cause)}`, cause);
81
- }
82
- if (response.status < 200 || response.status >= 300) {
83
- let body;
84
- try {
85
- body = (await response.text()).slice(0, 512);
86
- }
87
- catch {
88
- body = '<no body>';
24
+ /** Exact Relaycast #436 HTTP contract. Only terminal GET receipts can return. */
25
+ export async function runAgentRelayTask(request, options = {}) {
26
+ const env = options.agentToken
27
+ ? {
28
+ baseUrl: options.baseUrl || "https://cast.agentrelay.com",
29
+ agentToken: options.agentToken,
89
30
  }
90
- throw new AgentRelayTransportError(`agent-relay spawn refused with status ${response.status}: ${body}`);
91
- }
92
- let json;
93
- try {
94
- json = await response.json();
31
+ : { ...readAgentRelayEnv(), ...options };
32
+ const base = new URL(env.baseUrl);
33
+ if (base.username ||
34
+ base.password ||
35
+ base.search ||
36
+ base.hash ||
37
+ base.pathname !== "/" ||
38
+ !["https:", "http:"].includes(base.protocol))
39
+ throw new AgentRelayTransportError("Invalid Relay task base URL");
40
+ const loopback = base.hostname === "[::1]" ||
41
+ (isIP(base.hostname) === 4 && base.hostname.startsWith("127."));
42
+ if (base.protocol !== "https:" && !loopback) {
43
+ throw new AgentRelayTransportError("Relay task credentials require HTTPS outside literal loopback addresses");
95
44
  }
96
- catch (cause) {
97
- throw new AgentRelayTransportError('agent-relay spawn response was not JSON', cause);
45
+ const baseUrl = base.origin;
46
+ const timeoutMs = request.timeoutMs ?? 86_400_000;
47
+ if (!Number.isSafeInteger(timeoutMs) ||
48
+ timeoutMs < 1 ||
49
+ timeoutMs > 86_400_000 ||
50
+ ![
51
+ request.runId,
52
+ request.stepId,
53
+ request.idempotencyKey,
54
+ request.dataDir,
55
+ request.cli,
56
+ ].every(nonempty)) {
57
+ throw new AgentRelayTransportError("Relay task requires durable dispatch identity and a valid deadline");
98
58
  }
99
- const invocationId = json.invocation?.invocationId;
100
- const registeredName = json.invocation?.input?.name ?? request.name;
101
- if (typeof invocationId !== 'string' || invocationId.length === 0) {
102
- throw new AgentRelayTransportError('agent-relay spawn response missing invocation.invocationId');
103
- }
104
- return {
105
- registeredName,
106
- invocationId,
107
- async close() {
108
- // Best-effort: post to /api/v1/invocations/<id>/close if defined server-side;
109
- // no throw on error because the invocation may already be terminal.
110
- const closeUrl = new URL(`/api/v1/invocations/${encodeURIComponent(invocationId)}/close`, resolved.baseUrl).toString();
59
+ const pollMs = options.pollMs ?? 1000;
60
+ if (!Number.isFinite(pollMs) || pollMs < 1)
61
+ throw new AgentRelayTransportError("Invalid Relay task polling interval");
62
+ const doFetch = options.fetch ?? globalThis.fetch;
63
+ const outer = options.signal ?? new AbortController().signal;
64
+ let deadline = Date.now() + timeoutMs + 30_000;
65
+ let missingDeadline = Date.now() + 30_000;
66
+ async function http(path, body) {
67
+ for (;;) {
68
+ outer.throwIfAborted();
69
+ if (Date.now() >= deadline)
70
+ throw new AgentRelayTransportError("Relay task status unavailable before its reconciliation deadline");
71
+ const bounded = AbortSignal.any([
72
+ outer,
73
+ AbortSignal.timeout(Math.min(15_000, Math.max(1, deadline - Date.now()))),
74
+ ]);
75
+ let response;
76
+ let value;
111
77
  try {
112
- await doFetch(closeUrl, {
113
- method: 'POST',
114
- headers,
115
- body: JSON.stringify({ reason: 'sdk_transport_close' }),
78
+ response = await doFetch(new URL(path, baseUrl), {
79
+ method: body === undefined ? "GET" : "POST",
80
+ redirect: "error",
81
+ signal: bounded,
82
+ headers: {
83
+ authorization: `Bearer ${env.agentToken}`,
84
+ "content-type": "application/json",
85
+ ...(body === undefined
86
+ ? {}
87
+ : { "Idempotency-Key": request.idempotencyKey }),
88
+ },
89
+ ...(body === undefined ? {} : { body: canonicalize(body) }),
116
90
  });
91
+ if (response.status === 404 &&
92
+ body === undefined &&
93
+ path.includes("/invocations/")) {
94
+ if (Date.now() >= missingDeadline)
95
+ throw new AgentRelayTransportError("Relay task dispatch remains unconfirmed; no repeat POST is permitted");
96
+ }
97
+ else if (!response.ok &&
98
+ response.status !== 429 &&
99
+ response.status < 500) {
100
+ // Never copy untrusted response bodies, URLs, or tokens into diagnostics.
101
+ throw new AgentRelayTransportError(`Relay task request refused with HTTP ${response.status}`);
102
+ }
103
+ if (response.ok)
104
+ value = await response.json();
105
+ if (body !== undefined && !response.ok)
106
+ return {}; // ambiguous POST: GET only below
107
+ }
108
+ catch (error) {
109
+ outer.throwIfAborted();
110
+ if (error instanceof AgentRelayTransportError)
111
+ throw error;
112
+ if (body !== undefined)
113
+ return {}; // response loss or invalid JSON never causes another POST
114
+ if (response?.ok && !bounded.aborted)
115
+ throw new AgentRelayTransportError("Relay task response was not valid JSON");
117
116
  }
118
- catch {
119
- // Deliberate: close is advisory in this minimum-viable slice.
117
+ outer.throwIfAborted();
118
+ if (response?.ok && value !== undefined) {
119
+ if (!isRecord(value) || value.ok !== true || !isRecord(value.data))
120
+ throw new AgentRelayTransportError("Relay task response has an invalid data envelope");
121
+ return value.data;
120
122
  }
123
+ await delay(pollMs, undefined, { signal: outer });
124
+ }
125
+ }
126
+ // Resolving the agent is read-only. Pin identity before any invocation so a
127
+ // restarted runner cannot create a second task using another caller's key.
128
+ const agent = await http("/v1/agent");
129
+ if (!nonempty(agent.id))
130
+ throw new AgentRelayTransportError("Relay task caller identity is missing");
131
+ if (!nonempty(agent.workspace_id))
132
+ throw new AgentRelayTransportError("Relay task workspace identity is missing");
133
+ // Pinned action-invoke-v1 identity contract from Relaycast #436.
134
+ const invocationId = "inv_idem_" +
135
+ createHash("sha256")
136
+ .update([
137
+ "action-invoke-v1",
138
+ agent.workspace_id,
139
+ agent.id,
140
+ "task.run",
141
+ request.idempotencyKey,
142
+ ].join("\0"))
143
+ .digest("hex");
144
+ const input = {
145
+ cli: request.cli,
146
+ task: request.task,
147
+ ...(request.model === undefined ? {} : { model: request.model }),
148
+ ...(request.worker_cwd === undefined
149
+ ? {}
150
+ : { worker_cwd: request.worker_cwd }),
151
+ ...(request.result_schema === undefined
152
+ ? {}
153
+ : { result_schema: request.result_schema }),
154
+ task_context: {
155
+ run_id: request.runId,
156
+ step_id: request.stepId,
157
+ dispatch_id: request.idempotencyKey,
158
+ timeout_ms: timeoutMs,
121
159
  },
122
160
  };
123
- }
124
- /**
125
- * Derive a workspace-unique agent name from a run identity + step id. The
126
- * result is stable across replays of the same step so DMs can be addressed
127
- * even during retries.
128
- */
129
- export function deriveAgentName(runId, stepId) {
130
- // Keep readable + collision-safe: prefix with 'flow-' and use lower-kebab.
131
- const clean = (s) => s.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
132
- return `flow-${clean(runId)}-${clean(stepId)}`.slice(0, 96);
161
+ const { claim, created } = await claimRelayTask(request.dataDir, {
162
+ version: 1,
163
+ baseUrl,
164
+ callerId: agent.id,
165
+ workspaceId: agent.workspace_id,
166
+ invocationId,
167
+ runId: request.runId,
168
+ stepId: request.stepId,
169
+ idempotencyKey: request.idempotencyKey,
170
+ input,
171
+ startedAt: Date.now(),
172
+ });
173
+ // A restarted runner may recover a terminal receipt after the task deadline.
174
+ // Give that read one bounded window; never reopen or extend the remote task.
175
+ deadline = Math.max(claim.startedAt + timeoutMs + 30_000, Date.now() + 15_000);
176
+ missingDeadline = claim.startedAt + 30_000;
177
+ if (created) {
178
+ const ack = await http("/v1/actions/task.run/invoke", { input });
179
+ if (Object.keys(ack).length &&
180
+ (ack.invocation_id !== invocationId ||
181
+ ack.action_name !== "task.run" ||
182
+ canonicalize(ack.input) !== canonicalize(input))) {
183
+ throw new AgentRelayTransportError("Relay task acknowledgment has mismatched invocation or input");
184
+ }
185
+ }
186
+ let previous;
187
+ for (;;) {
188
+ const value = await http(`/v1/actions/task.run/invocations/${encodeURIComponent(invocationId)}`);
189
+ if (value.caller_id !== agent.id)
190
+ throw new AgentRelayTransportError("Relay task receipt belongs to another caller");
191
+ const receipt = readTaskReceipt(value, invocationId, input, previous);
192
+ outer.throwIfAborted();
193
+ if (receipt.status === "completed" || receipt.status === "failed")
194
+ return receipt;
195
+ previous = receipt;
196
+ await delay(pollMs, undefined, { signal: outer });
197
+ }
133
198
  }
134
199
  //# sourceMappingURL=agent-relay-transport.js.map