@relayflows/sdk 2.0.55 → 2.0.57

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 (214) hide show
  1. package/dist/adapters/base.d.ts +2 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/devin.d.ts +4 -0
  4. package/dist/adapters/devin.d.ts.map +1 -0
  5. package/dist/adapters/devin.js +20 -0
  6. package/dist/adapters/devin.js.map +1 -0
  7. package/dist/adapters/index.d.ts +3 -2
  8. package/dist/adapters/index.d.ts.map +1 -1
  9. package/dist/adapters/index.js +5 -1
  10. package/dist/adapters/index.js.map +1 -1
  11. package/dist/authored-broker.d.ts +13 -0
  12. package/dist/authored-broker.d.ts.map +1 -0
  13. package/dist/authored-broker.js +75 -0
  14. package/dist/authored-broker.js.map +1 -0
  15. package/dist/authored-budget.d.ts +1 -1
  16. package/dist/authored-budget.d.ts.map +1 -1
  17. package/dist/authored-budget.js +8 -2
  18. package/dist/authored-budget.js.map +1 -1
  19. package/dist/authored-flow-error.d.ts +6 -0
  20. package/dist/authored-flow-error.d.ts.map +1 -1
  21. package/dist/authored-flow-error.js.map +1 -1
  22. package/dist/authored-flow-executor.d.ts +11 -0
  23. package/dist/authored-flow-executor.d.ts.map +1 -1
  24. package/dist/authored-flow-executor.js +104 -20
  25. package/dist/authored-flow-executor.js.map +1 -1
  26. package/dist/authored-flow-loader.d.ts.map +1 -1
  27. package/dist/authored-flow-loader.js +42 -3
  28. package/dist/authored-flow-loader.js.map +1 -1
  29. package/dist/authored-helper-effect.d.ts.map +1 -1
  30. package/dist/authored-helper-effect.js +1 -1
  31. package/dist/authored-helper-effect.js.map +1 -1
  32. package/dist/authored-hooks.d.ts +2 -1
  33. package/dist/authored-hooks.d.ts.map +1 -1
  34. package/dist/authored-hooks.js +2 -2
  35. package/dist/authored-hooks.js.map +1 -1
  36. package/dist/authored-human.d.ts +18 -0
  37. package/dist/authored-human.d.ts.map +1 -1
  38. package/dist/authored-human.js +32 -4
  39. package/dist/authored-human.js.map +1 -1
  40. package/dist/authored-mcp.d.ts.map +1 -1
  41. package/dist/authored-mcp.js +3 -2
  42. package/dist/authored-mcp.js.map +1 -1
  43. package/dist/authored-node-entry.js +4 -0
  44. package/dist/authored-node-entry.js.map +1 -1
  45. package/dist/authored-node-runner.d.ts.map +1 -1
  46. package/dist/authored-node-runner.js +1 -0
  47. package/dist/authored-node-runner.js.map +1 -1
  48. package/dist/authored-plugin-effect.d.ts.map +1 -1
  49. package/dist/authored-plugin-effect.js +3 -2
  50. package/dist/authored-plugin-effect.js.map +1 -1
  51. package/dist/authored-preflight.d.ts.map +1 -1
  52. package/dist/authored-preflight.js +2 -1
  53. package/dist/authored-preflight.js.map +1 -1
  54. package/dist/authored-root.d.ts.map +1 -1
  55. package/dist/authored-root.js +13 -3
  56. package/dist/authored-root.js.map +1 -1
  57. package/dist/authored-serial-ordinals.d.ts +41 -0
  58. package/dist/authored-serial-ordinals.d.ts.map +1 -0
  59. package/dist/authored-serial-ordinals.js +45 -0
  60. package/dist/authored-serial-ordinals.js.map +1 -0
  61. package/dist/authored-worker-step.d.ts +5 -1
  62. package/dist/authored-worker-step.d.ts.map +1 -1
  63. package/dist/authored-worker-step.js +19 -2
  64. package/dist/authored-worker-step.js.map +1 -1
  65. package/dist/budget-preflight.d.ts.map +1 -1
  66. package/dist/budget-preflight.js +18 -3
  67. package/dist/budget-preflight.js.map +1 -1
  68. package/dist/bundle-transport.d.ts +2 -0
  69. package/dist/bundle-transport.d.ts.map +1 -1
  70. package/dist/bundle-transport.js +4 -0
  71. package/dist/bundle-transport.js.map +1 -1
  72. package/dist/cached-flow-runtime.d.ts +3 -0
  73. package/dist/cached-flow-runtime.d.ts.map +1 -0
  74. package/dist/cached-flow-runtime.js +37 -0
  75. package/dist/cached-flow-runtime.js.map +1 -0
  76. package/dist/cli/check.d.ts.map +1 -1
  77. package/dist/cli/check.js +13 -1
  78. package/dist/cli/check.js.map +1 -1
  79. package/dist/cli/cli-probe.d.ts.map +1 -1
  80. package/dist/cli/cli-probe.js +72 -11
  81. package/dist/cli/cli-probe.js.map +1 -1
  82. package/dist/cli/cloud-deploy.js +6 -5
  83. package/dist/cli/cloud-deploy.js.map +1 -1
  84. package/dist/cli/cloud-mirror-session.d.ts +8 -2
  85. package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
  86. package/dist/cli/cloud-mirror-session.js +20 -27
  87. package/dist/cli/cloud-mirror-session.js.map +1 -1
  88. package/dist/cli/direct-run.d.ts.map +1 -1
  89. package/dist/cli/direct-run.js +2 -1
  90. package/dist/cli/direct-run.js.map +1 -1
  91. package/dist/cli/run.d.ts.map +1 -1
  92. package/dist/cli/run.js +19 -4
  93. package/dist/cli/run.js.map +1 -1
  94. package/dist/cli.d.ts.map +1 -1
  95. package/dist/cli.js +39 -18
  96. package/dist/cli.js.map +1 -1
  97. package/dist/cloud-deploy.d.ts +3 -3
  98. package/dist/cloud-deploy.d.ts.map +1 -1
  99. package/dist/cloud-deploy.js +15 -9
  100. package/dist/cloud-deploy.js.map +1 -1
  101. package/dist/cloud-flow-source.d.ts +13 -0
  102. package/dist/cloud-flow-source.d.ts.map +1 -0
  103. package/dist/cloud-flow-source.js +55 -0
  104. package/dist/cloud-flow-source.js.map +1 -0
  105. package/dist/cloud-mirror-transport.d.ts +2 -0
  106. package/dist/cloud-mirror-transport.d.ts.map +1 -1
  107. package/dist/cloud-mirror-transport.js +2 -0
  108. package/dist/cloud-mirror-transport.js.map +1 -1
  109. package/dist/cloud-mirror.d.ts.map +1 -1
  110. package/dist/cloud-mirror.js +10 -1
  111. package/dist/cloud-mirror.js.map +1 -1
  112. package/dist/communication/broker-preflight.d.ts +3 -0
  113. package/dist/communication/broker-preflight.d.ts.map +1 -0
  114. package/dist/communication/broker-preflight.js +22 -0
  115. package/dist/communication/broker-preflight.js.map +1 -0
  116. package/dist/communication/broker-spec.d.ts +20 -0
  117. package/dist/communication/broker-spec.d.ts.map +1 -0
  118. package/dist/communication/broker-spec.js +46 -0
  119. package/dist/communication/broker-spec.js.map +1 -0
  120. package/dist/communication/broker-worker.d.ts +17 -0
  121. package/dist/communication/broker-worker.d.ts.map +1 -0
  122. package/dist/communication/broker-worker.js +214 -0
  123. package/dist/communication/broker-worker.js.map +1 -0
  124. package/dist/communication/environment.d.ts.map +1 -1
  125. package/dist/communication/environment.js +1 -0
  126. package/dist/communication/environment.js.map +1 -1
  127. package/dist/communication/pty.d.ts +29 -0
  128. package/dist/communication/pty.d.ts.map +1 -0
  129. package/dist/communication/pty.js +44 -0
  130. package/dist/communication/pty.js.map +1 -0
  131. package/dist/communication/relay.d.ts +18 -0
  132. package/dist/communication/relay.d.ts.map +1 -1
  133. package/dist/communication/relay.js +4 -0
  134. package/dist/communication/relay.js.map +1 -1
  135. package/dist/communication/skills.d.ts +3 -0
  136. package/dist/communication/skills.d.ts.map +1 -0
  137. package/dist/communication/skills.js +18 -0
  138. package/dist/communication/skills.js.map +1 -0
  139. package/dist/communication/worker.d.ts.map +1 -1
  140. package/dist/communication/worker.js +9 -26
  141. package/dist/communication/worker.js.map +1 -1
  142. package/dist/devin-runtime.d.ts +10 -0
  143. package/dist/devin-runtime.d.ts.map +1 -0
  144. package/dist/devin-runtime.js +37 -0
  145. package/dist/devin-runtime.js.map +1 -0
  146. package/dist/failure-kinds.d.ts +1 -1
  147. package/dist/failure-kinds.d.ts.map +1 -1
  148. package/dist/failure-kinds.js +2 -0
  149. package/dist/failure-kinds.js.map +1 -1
  150. package/dist/plugin-manifest.js +1 -1
  151. package/dist/plugin-manifest.js.map +1 -1
  152. package/dist/preflight.d.ts +1 -1
  153. package/dist/preflight.d.ts.map +1 -1
  154. package/dist/preflight.js +14 -4
  155. package/dist/preflight.js.map +1 -1
  156. package/dist/step-secrets.d.ts.map +1 -1
  157. package/dist/step-secrets.js +1 -0
  158. package/dist/step-secrets.js.map +1 -1
  159. package/dist/worker-cli.d.ts.map +1 -1
  160. package/dist/worker-cli.js +15 -3
  161. package/dist/worker-cli.js.map +1 -1
  162. package/dist/worker.d.ts +1 -0
  163. package/dist/worker.d.ts.map +1 -1
  164. package/dist/worker.js +12 -0
  165. package/dist/worker.js.map +1 -1
  166. package/package.json +2 -2
  167. package/src/adapters/base.ts +2 -0
  168. package/src/adapters/devin.ts +22 -0
  169. package/src/adapters/index.ts +5 -2
  170. package/src/authored-broker.ts +73 -0
  171. package/src/authored-budget.ts +6 -2
  172. package/src/authored-flow-error.ts +6 -0
  173. package/src/authored-flow-executor.ts +115 -20
  174. package/src/authored-flow-loader.ts +43 -3
  175. package/src/authored-helper-effect.ts +2 -1
  176. package/src/authored-hooks.ts +4 -3
  177. package/src/authored-human.ts +35 -4
  178. package/src/authored-mcp.ts +7 -2
  179. package/src/authored-node-entry.ts +4 -1
  180. package/src/authored-node-runner.ts +1 -0
  181. package/src/authored-plugin-effect.ts +7 -2
  182. package/src/authored-preflight.ts +2 -1
  183. package/src/authored-root.ts +13 -3
  184. package/src/authored-serial-ordinals.ts +76 -0
  185. package/src/authored-worker-step.ts +17 -2
  186. package/src/budget-preflight.ts +16 -2
  187. package/src/bundle-transport.ts +4 -0
  188. package/src/cached-flow-runtime.ts +32 -0
  189. package/src/cli/check.ts +11 -1
  190. package/src/cli/cli-probe.ts +65 -11
  191. package/src/cli/cloud-deploy.ts +5 -5
  192. package/src/cli/cloud-mirror-session.ts +23 -29
  193. package/src/cli/direct-run.ts +2 -1
  194. package/src/cli/run.ts +18 -3
  195. package/src/cli.ts +31 -13
  196. package/src/cloud-deploy.ts +19 -14
  197. package/src/cloud-flow-source.ts +58 -0
  198. package/src/cloud-mirror-transport.ts +4 -0
  199. package/src/cloud-mirror.ts +9 -1
  200. package/src/communication/broker-preflight.ts +22 -0
  201. package/src/communication/broker-spec.ts +48 -0
  202. package/src/communication/broker-worker.ts +198 -0
  203. package/src/communication/environment.ts +1 -0
  204. package/src/communication/pty.ts +51 -0
  205. package/src/communication/relay.ts +12 -1
  206. package/src/communication/skills.ts +17 -0
  207. package/src/communication/worker.ts +9 -26
  208. package/src/devin-runtime.ts +33 -0
  209. package/src/failure-kinds.ts +2 -0
  210. package/src/plugin-manifest.ts +1 -1
  211. package/src/preflight.ts +15 -4
  212. package/src/step-secrets.ts +1 -0
  213. package/src/worker-cli.ts +13 -5
  214. package/src/worker.ts +11 -0
@@ -0,0 +1,17 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { basename } from 'node:path';
4
+
5
+ const execute = promisify(execFile);
6
+ /** Install full skill directories (including references) using the canonical package. */
7
+ export async function installBrokerSkills(skills: string[], cli: string, cwd: string, signal: AbortSignal): Promise<void> {
8
+ if (!skills.length) return;
9
+ const identity = basename(cli).replace(/\.exe$/i, '');
10
+ // Devin reads .windsurf/skills, which the canonical skills installer supports.
11
+ const harness = ({ claude: 'claude-code', codex: 'codex', gemini: 'gemini-cli',
12
+ 'cursor-agent': 'cursor', opencode: 'opencode', devin: 'windsurf' } as Record<string, string>)[identity];
13
+ if (!harness) throw new Error(`Skill installation is unsupported for broker CLI ${identity}`);
14
+ await execute('npx', ['--yes', 'skills', 'add', 'https://github.com/AgentWorkforce/skills',
15
+ '--skill', ...skills, '--agent', harness, '--yes', '--copy'],
16
+ { cwd, signal, timeout: 120_000, maxBuffer: 1024 * 1024 });
17
+ }
@@ -1,6 +1,4 @@
1
1
  import { communicationHistory } from './history.js';
2
- import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
3
- import { basename, join } from 'node:path';
4
2
  import { setTimeout as delay } from 'node:timers/promises';
5
3
  import type { JournalClient } from '../journal-client.js';
6
4
  import type { StepDispatchEvent } from '../protocol.js';
@@ -8,13 +6,12 @@ import type { ResolvedKernelAgentStep } from '../resolved-cli-identity.js';
8
6
  import { isLeaseLost, withWorkerLease } from '../worker-lease.js';
9
7
  import { isReadInterruptionError } from '../journal-read-policy.js';
10
8
  import { workerInstruction } from '../worker-input.js';
11
- import { resolveCliModel } from '../cli-adapter.js';
12
9
  import { resolveAgentCwd } from '../agent-cwd.js';
13
10
  import { channelName, type CommunicationInstruction } from './spec.js';
14
11
  import { acquireRelayRuntime, type RelayHandle } from './relay.js';
15
12
  import { CommunicationSession } from './session.js';
16
13
  import { openCommunicationTools } from './tools.js';
17
- import { agentEnvironment } from './environment.js';
14
+ import { spawnFlowPty, shellQuote } from './pty.js';
18
15
 
19
16
  export function requireCommunicationCli(cli: string | undefined): void {
20
17
  if (!cli?.trim()) throw new Error('Agent communication requires a declared CLI executable');
@@ -62,7 +59,7 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
62
59
  let pumping: Promise<void> | undefined;
63
60
  let receipts = Promise.resolve();
64
61
  let unsubscribe: (() => void) | undefined;
65
- let cliLinkDirectory: string | undefined;
62
+ let pty: Awaited<ReturnType<typeof spawnFlowPty>> | undefined;
66
63
  try {
67
64
  let resolve!: (value: unknown) => void;
68
65
  let reject!: (reason: unknown) => void;
@@ -74,8 +71,7 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
74
71
  if (signal.aborted) abort();
75
72
  const session = new CommunicationSession(client, dispatch, instruction, relay, summary => resolve({ summary }), reject);
76
73
  tools = await openCommunicationTools(request => session.invoke(request));
77
- const quote = (value: string) => "'" + value.replaceAll("'", "'\"'\"'") + "'";
78
- const helper = `node ${quote(tools.helperPath)}`;
74
+ const helper = `node ${shellQuote(tools.helperPath)}`;
79
75
  const history = await communicationHistory(client, dispatch, instruction);
80
76
  const prompt = `${workerInstruction(instruction.instruction, dispatch)}${history}\n\nYou are flow agent ${dispatch.step_id}. Concurrent peers send messages automatically into your session through Relay. Do not poll for messages. Use your shell/terminal execution tool with ${helper} for flow communication:\n- send PEER STABLE_ID 'message text' (outgoing peers: ${instruction.outgoing.join(', ') || 'none'})\n- ack PEER DELIVERY_SEQ only after processing that message (incoming peers: ${instruction.incoming.join(', ') || 'none'})\n- complete 'summary' when your task and conversation are finished.\nUse these helpers for all peer communication; do not use Relay MCP messaging tools. Stay available for injected messages until the conversation is complete. Use stable semantic message IDs so retries deduplicate sends.`;
81
77
  const name = `${relay.prefix}-${dispatch.step_id}`;
@@ -89,24 +85,11 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
89
85
  });
90
86
  void receipts.catch(reject);
91
87
  });
92
- // Relay supplies each CLI's launch flags and injection behavior.
93
- const cliIdentity = spec.cli_identity ?? spec.cli!;
94
- const argv0 = basename(cliIdentity);
95
- let command = spec.cli!;
96
- if (basename(command) !== argv0) {
97
- const linkRoot = join(dataDir, 'communication');
98
- await mkdir(linkRoot, { recursive: true, mode: 0o700 });
99
- cliLinkDirectory = await mkdtemp(join(linkRoot, 'cli-'));
100
- command = join(cliLinkDirectory, argv0);
101
- await symlink(spec.cli!, command, 'file');
102
- }
103
- handle = await relay.broker.spawnPty({ name, cli: basename(cliIdentity).replace(/\.exe$/i, ''), task: prompt, channels: [], skipRelayPrompt: true,
104
- model: resolveCliModel(cliIdentity, spec.model), cwd: directory,
105
- harnessConfig: { runtime: 'pty', command: quote(command), args: [],
106
- cwd: directory, env: { ...agentEnvironment(cliIdentity, environment ?? process.env,
107
- spec.secrets, client.supportsFeature?.('step_secret_scoping') ?? true),
108
- RELAYFLOW_COMMUNICATION_SOCKET: tools.path, RELAYFLOW_COMMUNICATION_TOKEN: tools.token },
109
- delivery: { mode: 'pty-injection', format: 'relay-block' } } });
88
+ pty = await spawnFlowPty(relay.broker, { name, cli: spec.cli!, identity: spec.cli_identity,
89
+ model: spec.model, task: prompt, channels: [], cwd: directory, dataDir,
90
+ environment, socket: tools.path, token: tools.token,
91
+ secrets: spec.secrets, scopeSecrets: client.supportsFeature?.('step_secret_scoping') ?? true });
92
+ handle = pty.handle;
110
93
  const ready = await handle.waitForReady(Math.min(instruction.timeoutMs, 90_000));
111
94
  if (ready.reason !== 'ready') throw new Error(`Communication agent did not become ready: ${ready.reason}`);
112
95
  pumping = (async () => {
@@ -123,7 +106,7 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
123
106
  finally { try { await handle?.release('Flow communication attempt ended', { deleteIdentity: true }); }
124
107
  finally { try { await tools?.close(); } finally {
125
108
  try { await relay.close(); }
126
- finally { if (cliLinkDirectory) await rm(cliLinkDirectory, { recursive: true, force: true }); }
109
+ finally { await pty?.close(); }
127
110
  } } }
128
111
  }
129
112
  }
@@ -0,0 +1,33 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import type { CliInvocation } from './adapters/base.js';
5
+
6
+ /** Private attempt/probe state, never the user's interactive Devin login. */
7
+ export function prepareDevinRuntime(environment: NodeJS.ProcessEnv, invocation?: CliInvocation) {
8
+ let root: string | undefined;
9
+ const env = { ...environment };
10
+ const args = [...(invocation?.args ?? [])];
11
+ const directory = () => root ??= mkdtempSync(join(environment.TMPDIR ?? tmpdir(), 'relayflows-devin-'));
12
+ const close = () => { if (root !== undefined) rmSync(root, { recursive: true, force: true }); };
13
+ try {
14
+ if (env.DEVIN_API_KEY !== undefined) {
15
+ const key = env.DEVIN_API_KEY.trim();
16
+ if (!key) throw new Error('DEVIN_API_KEY must not be empty');
17
+ const data = join(directory(), 'data');
18
+ mkdirSync(join(data, 'devin'), { recursive: true, mode: 0o700 });
19
+ writeFileSync(join(data, 'devin', 'credentials.toml'),
20
+ `windsurf_api_key = ${JSON.stringify(key)}\n`, { mode: 0o600 });
21
+ env.XDG_DATA_HOME = data;
22
+ env.XDG_CONFIG_HOME = join(directory(), 'config');
23
+ env.XDG_CACHE_HOME = join(directory(), 'cache');
24
+ delete env.DEVIN_API_KEY;
25
+ }
26
+ if (invocation?.promptFile !== undefined) {
27
+ const path = join(directory(), 'prompt.txt');
28
+ writeFileSync(path, invocation.promptFile, { mode: 0o600 });
29
+ args.push('--prompt-file', path);
30
+ }
31
+ return { env, args, close };
32
+ } catch (error) { close(); throw error; }
33
+ }
@@ -138,6 +138,8 @@ export const RUN_FAILURE_KINDS = [
138
138
  'unsafe_parallel_runtime',
139
139
  /** A predicate `.gate(fn)` judged false; the verdict is journaled as `<step>.gate`. */
140
140
  'gate_failed',
141
+ /** A resumed serial body reached a journaled step id with a different call; refused, not executed. */
142
+ 'serial_replay_order_mismatch',
141
143
  /** `flows answer` named a wait the run is not asking: unknown, or already answered. */
142
144
  'human_wait_unknown',
143
145
  /** `f.memory.learn` was given a finding without non-empty string fields. */
@@ -39,7 +39,7 @@ export interface PluginManifest {
39
39
  preflight: { credentials: readonly string[]; servers: readonly string[] };
40
40
  }
41
41
  const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
42
- const reserved = new Set(['run', 'llm', 'agent', 'human', 'dispatch', 'done', 'cloud', 'memory', 'mcp', 'slack', '__proto__', 'constructor', 'prototype', 'then']);
42
+ const reserved = new Set(['run', 'llm', 'agent', 'message', 'release', 'human', 'dispatch', 'done', 'cloud', 'memory', 'mcp', 'slack', '__proto__', 'constructor', 'prototype', 'then']);
43
43
  const object = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
44
44
  const strings = (v: unknown): v is string[] => Array.isArray(v) && v.every(s => typeof s === 'string' && s.trim().length > 0);
45
45
  export function pluginPackageName(name: string): string {
package/src/preflight.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { brokerInstruction } from './communication/broker-spec.js';
1
2
  import { communicationInstruction } from './communication/spec.js';
2
3
  import { loadPlugins, type LoadedPlugin } from './plugin-loader.js';
3
4
  import { PluginError } from './plugin-manifest.js';
@@ -10,7 +11,7 @@ import type { TriggerSource } from '@relayflows/surface';
10
11
  import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
11
12
  import { compileSpec, CompileError } from './compile.js';
12
13
  import { helperCall } from './yaml-helpers.js';
13
- import { resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
14
+ import { cliAdapterKind, resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
14
15
  import { isNamedGate, NAMED_GATE_FAILURE_KINDS, type NamedGateFailureKind } from './named-gates.js';
15
16
  import { namedGateScanCoverageDiagnostics } from './named-gate-preflight.js';
16
17
  import { compileScopes, type ScopeInput, type MountRegistry } from './scope-compiler.js';
@@ -64,6 +65,7 @@ export interface CliProbeResult {
64
65
 
65
66
  export type CliProbeFailureDetail =
66
67
  | 'spawn_failed'
68
+ | 'devin_api_key_empty'
67
69
  | `timeout:${number}ms`
68
70
  | `signal:${string}`;
69
71
 
@@ -285,6 +287,10 @@ export function resolvePreflight(flow: unknown, options: ResolutionOptions): Pre
285
287
  message: unresolvedCliMessage(step.id, options),
286
288
  });
287
289
  } else {
290
+ if (step.type === 'llm' && cliAdapterKind(resolution.cli) === 'devin') {
291
+ cliResolutionDiagnostics.push({ severity: 'refusal', kind: 'cli_unsupported', stepId: step.id,
292
+ cli: resolution.cli, message: `Step "${step.id}" uses Devin for an llm step; Devin supports agent steps only.` });
293
+ }
288
294
  resolutions.push(resolution);
289
295
  resolutionByStep.set(step.id, resolution);
290
296
  }
@@ -340,7 +346,8 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
340
346
  }
341
347
  const resolution = resolutionByStep.get(step.id)!;
342
348
  probeResolvedCli(resolution, options.probes, cliProbeResults, diagnostics,
343
- step.type === 'agent' && communicationInstruction(step.instruction) !== undefined, step.secrets);
349
+ step.type === 'agent' && (communicationInstruction(step.instruction) !== undefined || brokerInstruction(step.instruction) !== undefined),
350
+ step.secrets);
344
351
  }
345
352
 
346
353
  for (const trigger of compiled.triggers ?? []) {
@@ -592,13 +599,16 @@ function probeResolvedCli(
592
599
  kind: 'cli_unsupported',
593
600
  stepId: resolution.stepId,
594
601
  cli: resolution.cli,
595
- message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it is neither a supported raw Claude/Codex executable nor a conforming Relayflows wrapper; custom wrappers must identify with the relayflows-agent-cli-v1 contract.`,
602
+ message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it is neither a supported raw Claude/Codex/Devin executable nor a conforming Relayflows wrapper; custom wrappers must identify with the relayflows-agent-cli-v1 contract.`,
596
603
  });
597
604
  } else if (managed && result.authenticated === 'unverified') {
598
605
  diagnostics.push({ severity: 'warning', kind: 'managed_cli_unverified', stepId: resolution.stepId,
599
606
  message: `Step "${resolution.stepId}" uses Relay's interactive CLI transport. The executable exists, but authentication${resolution.model ? ' and access to model "' + resolution.model + '"' : ''} could not be verified before startup. The managed session must execute its task and report completion; startup or task failure fails the step.` });
600
607
  } else if (result.authenticated !== true) {
601
608
  const command = result.authCommand ?? `${resolution.cli} auth status`;
609
+ const loginHint = cliAdapterKind(resolution.cli) === 'devin'
610
+ ? ' For hosted flows, set FLOW_DEVIN_API_KEY to a Devin CLI login token; local runs use DEVIN_API_KEY. A REST API key is a different credential.'
611
+ : '';
602
612
  // Say what the probe reported. A refusal that names only the command turns
603
613
  // a transient provider rejection and a genuinely unauthenticated CLI into
604
614
  // the same message, and the difference decides whether retrying is
@@ -614,7 +624,7 @@ function probeResolvedCli(
614
624
  kind: 'cli_unauthenticated',
615
625
  stepId: resolution.stepId,
616
626
  cli: resolution.cli,
617
- message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}`,
627
+ message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}${loginHint}`,
618
628
  });
619
629
  } else if (resolution.model !== undefined && result.modelAvailable !== true) {
620
630
  diagnostics.push(modelProbeRefusal(resolution as CliResolution & { model: string }, result));
@@ -666,6 +676,7 @@ function probeFailedMessage(
666
676
  ): string {
667
677
  const prefix = `Could not verify CLI "${resolution.cli}" for step "${resolution.stepId}"`;
668
678
  if (detail === undefined) return `${prefix}.`;
679
+ if (detail === 'devin_api_key_empty') return `${prefix}: DEVIN_API_KEY must contain a non-empty Devin CLI login token. For hosted flows, set FLOW_DEVIN_API_KEY.`;
669
680
  if (detail === 'spawn_failed') return `${prefix}: the probe process could not be started.`;
670
681
  if (detail.startsWith('signal:')) {
671
682
  return `${prefix}: the probe was terminated by signal "${detail.slice('signal:'.length)}".`;
@@ -47,6 +47,7 @@ const CLI_LOGIN: Record<CliAdapterKind, readonly string[]> = {
47
47
  'AWS_SESSION_TOKEN', 'AWS_REGION', 'AWS_DEFAULT_REGION',
48
48
  ],
49
49
  codex: ['CODEX_HOME', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENROUTER_API_KEY'],
50
+ devin: ['DEVIN_API_KEY', 'DEVIN_MODEL'],
50
51
  'relayflows-wrapper-v1': [],
51
52
  };
52
53
 
package/src/worker-cli.ts CHANGED
@@ -45,6 +45,7 @@ import {
45
45
  type AgentTransport,
46
46
  } from './agent-relay-transport.js';
47
47
  import { pinCliAlias } from './cli/pinned-cli-alias.js';
48
+ import { prepareDevinRuntime } from './devin-runtime.js';
48
49
  import { promptOperand } from './adapters/base.js';
49
50
 
50
51
  export type { CliTransportCause, CliTransportEvidence, CliTransportPhase } from './cli-transport-evidence.js';
@@ -277,15 +278,22 @@ export async function runAgentCli(
277
278
  // The flags go before the `--` that ends the task operand (promptOperand):
278
279
  // after it they would reach the CLI as task text, not options.
279
280
  const args = [...invocation.args];
280
- args.splice(args.length - promptOperand('').length, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
281
+ if (kind !== 'devin') args.splice(args.length - promptOperand('').length, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
281
282
  const completion = kind === 'claude' ? claudeResultOutcome : undefined;
282
283
  // Retain the source values for output redaction even though the child
283
284
  // receives only its filtered environment. A provider can echo a value from
284
285
  // its prompt or fixture that was not granted as an environment variable.
285
- const redactionEnvironment = { ...processEnvironment, ...env };
286
- return requirePricedUsage(decodeProviderResult(await spawnInvocation(
287
- cli, { ...invocation, args }, env, signal, sidechannel, cwd, completion, argv0, kind, redactionEnvironment,
288
- ), kind, redactionEnvironment), effectiveModel);
286
+ const redactionEnvironment = { ...processEnvironment, ...env,
287
+ ...(kind === 'devin' && env.DEVIN_API_KEY !== undefined ? { DEVIN_API_KEY: env.DEVIN_API_KEY.trim() } : {}) };
288
+ const prepared = kind === 'devin' ? prepareDevinRuntime(env, { ...invocation, args }) : undefined;
289
+ try {
290
+ const result = await spawnInvocation(cli,
291
+ { ...invocation, args: prepared?.args ?? args }, prepared?.env ?? env,
292
+ signal, sidechannel, cwd, completion, argv0, kind, redactionEnvironment);
293
+ // Print mode has neither provider frames nor authoritative token counts.
294
+ return kind === 'devin' ? { ...result, stdout_tail: redactText(result.stdout_tail, redactionEnvironment) }
295
+ : requirePricedUsage(decodeProviderResult(result, kind, redactionEnvironment), effectiveModel);
296
+ } finally { prepared?.close(); }
289
297
  }
290
298
  }
291
299
 
package/src/worker.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { brokerInstruction } from './communication/broker-spec.js';
2
+ import { BrokerSessions } from './communication/broker-worker.js';
1
3
  import { communicationInstruction } from './communication/spec.js';
2
4
  import { workerSpend } from './worker-spend.js';
3
5
  import { reportedCost } from './reported-cost.js';
@@ -60,6 +62,7 @@ export interface AgentWorkerOptions {
60
62
  * shutdown.
61
63
  */
62
64
  export class AgentWorker extends EventEmitter {
65
+ private readonly brokerSessions: BrokerSessions;
63
66
  private attached = false;
64
67
  private closing = false;
65
68
  private readonly inFlight: Set<Promise<void>> = new Set();
@@ -69,6 +72,7 @@ export class AgentWorker extends EventEmitter {
69
72
  private readonly options: AgentWorkerOptions,
70
73
  ) {
71
74
  super();
75
+ this.brokerSessions = new BrokerSessions(options.runRoot, options.environment);
72
76
  }
73
77
 
74
78
  async attach(): Promise<void> {
@@ -107,6 +111,7 @@ export class AgentWorker extends EventEmitter {
107
111
  if (pending.length > 0) {
108
112
  await Promise.allSettled(pending);
109
113
  }
114
+ await this.brokerSessions.close();
110
115
  this.attached = false;
111
116
  }
112
117
 
@@ -128,6 +133,12 @@ export class AgentWorker extends EventEmitter {
128
133
  await completeHelperDispatch(this.client, dispatch, helper, this.options.dataDir);
129
134
  return;
130
135
  }
136
+ const broker = brokerInstruction(spec.instruction);
137
+ if (broker) {
138
+ if (!this.options.dataDir) throw new Error('Broker agents require a worker data directory');
139
+ await this.brokerSessions.complete(this.client, dispatch, broker, this.options.dataDir);
140
+ return;
141
+ }
131
142
  const communication = communicationInstruction(spec.instruction);
132
143
  if (communication) {
133
144
  if (!this.options.dataDir) throw new Error('Agent communication requires a worker data directory');