agent-relay 4.0.34 → 4.0.36

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 (199) hide show
  1. package/README.md +3 -6
  2. package/dist/index.cjs +775 -40
  3. package/dist/src/cli/bootstrap.js.map +1 -1
  4. package/dist/src/cli/commands/cloud.js.map +1 -1
  5. package/dist/src/cli/commands/core.d.ts.map +1 -1
  6. package/dist/src/cli/commands/core.js.map +1 -1
  7. package/dist/src/cli/commands/messaging.d.ts.map +1 -1
  8. package/dist/src/cli/commands/messaging.js.map +1 -1
  9. package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
  10. package/dist/src/cli/commands/monitoring.js.map +1 -1
  11. package/node_modules/@agent-relay/cloud/package.json +2 -2
  12. package/node_modules/@agent-relay/config/package.json +2 -2
  13. package/node_modules/@agent-relay/hooks/package.json +4 -4
  14. package/node_modules/@agent-relay/sdk/dist/relay.d.ts.map +1 -1
  15. package/node_modules/@agent-relay/sdk/dist/relay.js.map +1 -1
  16. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-enforcement.test.d.ts +2 -0
  17. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-enforcement.test.d.ts.map +1 -0
  18. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-enforcement.test.js +437 -0
  19. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-enforcement.test.js.map +1 -0
  20. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-tracker.test.d.ts +2 -0
  21. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-tracker.test.d.ts.map +1 -0
  22. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-tracker.test.js +99 -0
  23. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/budget-tracker.test.js.map +1 -0
  24. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/proxy-env.test.d.ts +2 -0
  25. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/proxy-env.test.d.ts.map +1 -0
  26. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/proxy-env.test.js +135 -0
  27. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/proxy-env.test.js.map +1 -0
  28. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-custom.test.d.ts +2 -0
  29. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-custom.test.d.ts.map +1 -0
  30. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-custom.test.js +236 -0
  31. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-custom.test.js.map +1 -0
  32. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-traceback.test.d.ts +2 -0
  33. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-traceback.test.d.ts.map +1 -0
  34. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-traceback.test.js +448 -0
  35. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification-traceback.test.js.map +1 -0
  36. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification.test.js +71 -4
  37. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/verification.test.js.map +1 -1
  38. package/node_modules/@agent-relay/sdk/dist/workflows/budget-tracker.d.ts +75 -0
  39. package/node_modules/@agent-relay/sdk/dist/workflows/budget-tracker.d.ts.map +1 -0
  40. package/node_modules/@agent-relay/sdk/dist/workflows/budget-tracker.js +180 -0
  41. package/node_modules/@agent-relay/sdk/dist/workflows/budget-tracker.js.map +1 -0
  42. package/node_modules/@agent-relay/sdk/dist/workflows/builder.d.ts +1 -0
  43. package/node_modules/@agent-relay/sdk/dist/workflows/builder.d.ts.map +1 -1
  44. package/node_modules/@agent-relay/sdk/dist/workflows/builder.js +17 -2
  45. package/node_modules/@agent-relay/sdk/dist/workflows/builder.js.map +1 -1
  46. package/node_modules/@agent-relay/sdk/dist/workflows/index.d.ts +2 -0
  47. package/node_modules/@agent-relay/sdk/dist/workflows/index.d.ts.map +1 -1
  48. package/node_modules/@agent-relay/sdk/dist/workflows/index.js +2 -0
  49. package/node_modules/@agent-relay/sdk/dist/workflows/index.js.map +1 -1
  50. package/node_modules/@agent-relay/sdk/dist/workflows/proxy-env.d.ts +52 -0
  51. package/node_modules/@agent-relay/sdk/dist/workflows/proxy-env.d.ts.map +1 -0
  52. package/node_modules/@agent-relay/sdk/dist/workflows/proxy-env.js +92 -0
  53. package/node_modules/@agent-relay/sdk/dist/workflows/proxy-env.js.map +1 -0
  54. package/node_modules/@agent-relay/sdk/dist/workflows/run-summary-table.d.ts +2 -1
  55. package/node_modules/@agent-relay/sdk/dist/workflows/run-summary-table.d.ts.map +1 -1
  56. package/node_modules/@agent-relay/sdk/dist/workflows/run-summary-table.js +41 -9
  57. package/node_modules/@agent-relay/sdk/dist/workflows/run-summary-table.js.map +1 -1
  58. package/node_modules/@agent-relay/sdk/dist/workflows/runner.d.ts +17 -0
  59. package/node_modules/@agent-relay/sdk/dist/workflows/runner.d.ts.map +1 -1
  60. package/node_modules/@agent-relay/sdk/dist/workflows/runner.js +390 -16
  61. package/node_modules/@agent-relay/sdk/dist/workflows/runner.js.map +1 -1
  62. package/node_modules/@agent-relay/sdk/dist/workflows/types.d.ts +47 -1
  63. package/node_modules/@agent-relay/sdk/dist/workflows/types.d.ts.map +1 -1
  64. package/node_modules/@agent-relay/sdk/dist/workflows/types.js.map +1 -1
  65. package/node_modules/@agent-relay/sdk/dist/workflows/verification.d.ts +9 -0
  66. package/node_modules/@agent-relay/sdk/dist/workflows/verification.d.ts.map +1 -1
  67. package/node_modules/@agent-relay/sdk/dist/workflows/verification.js +78 -2
  68. package/node_modules/@agent-relay/sdk/dist/workflows/verification.js.map +1 -1
  69. package/node_modules/@agent-relay/sdk/package.json +8 -4
  70. package/node_modules/@agent-relay/telemetry/dist/config.d.ts.map +1 -1
  71. package/node_modules/@agent-relay/telemetry/dist/config.js +2 -4
  72. package/node_modules/@agent-relay/telemetry/dist/config.js.map +1 -1
  73. package/node_modules/@agent-relay/telemetry/dist/events.d.ts +1 -2
  74. package/node_modules/@agent-relay/telemetry/dist/events.d.ts.map +1 -1
  75. package/node_modules/@agent-relay/telemetry/dist/index.d.ts +1 -1
  76. package/node_modules/@agent-relay/telemetry/dist/index.d.ts.map +1 -1
  77. package/node_modules/@agent-relay/telemetry/dist/index.js +1 -1
  78. package/node_modules/@agent-relay/telemetry/dist/index.js.map +1 -1
  79. package/node_modules/@agent-relay/telemetry/package.json +1 -1
  80. package/node_modules/@agent-relay/trajectory/package.json +2 -2
  81. package/node_modules/@agent-relay/user-directory/package.json +2 -2
  82. package/node_modules/@agent-relay/utils/package.json +2 -2
  83. package/node_modules/@aws-sdk/core/dist-cjs/index.js +2 -0
  84. package/node_modules/@aws-sdk/core/dist-cjs/submodules/client/index.js +3 -0
  85. package/node_modules/@aws-sdk/core/dist-es/submodules/client/setFeature.js +2 -0
  86. package/node_modules/@aws-sdk/core/dist-types/submodules/client/setFeature.d.ts +1 -1
  87. package/node_modules/@aws-sdk/core/package.json +6 -4
  88. package/node_modules/@aws-sdk/credential-provider-env/package.json +2 -2
  89. package/node_modules/@aws-sdk/credential-provider-http/package.json +5 -5
  90. package/node_modules/@aws-sdk/credential-provider-ini/package.json +9 -9
  91. package/node_modules/@aws-sdk/credential-provider-login/package.json +3 -3
  92. package/node_modules/@aws-sdk/credential-provider-node/package.json +7 -7
  93. package/node_modules/@aws-sdk/credential-provider-process/package.json +2 -2
  94. package/node_modules/@aws-sdk/credential-provider-sso/package.json +4 -4
  95. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +3 -3
  96. package/node_modules/@aws-sdk/middleware-flexible-checksums/package.json +4 -4
  97. package/node_modules/@aws-sdk/middleware-sdk-s3/package.json +5 -5
  98. package/node_modules/@aws-sdk/middleware-user-agent/package.json +5 -5
  99. package/node_modules/@aws-sdk/nested-clients/package.json +18 -18
  100. package/node_modules/@aws-sdk/region-config-resolver/package.json +2 -2
  101. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +2 -2
  102. package/node_modules/@aws-sdk/token-providers/package.json +3 -3
  103. package/node_modules/@aws-sdk/util-endpoints/package.json +2 -2
  104. package/node_modules/@aws-sdk/util-user-agent-node/package.json +2 -2
  105. package/node_modules/axios/CHANGELOG.md +112 -44
  106. package/node_modules/axios/README.md +30 -0
  107. package/node_modules/axios/dist/axios.js +34 -11
  108. package/node_modules/axios/dist/axios.js.map +1 -1
  109. package/node_modules/axios/dist/axios.min.js +2 -2
  110. package/node_modules/axios/dist/axios.min.js.map +1 -1
  111. package/node_modules/axios/dist/browser/axios.cjs +32 -6
  112. package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
  113. package/node_modules/axios/dist/esm/axios.js +32 -6
  114. package/node_modules/axios/dist/esm/axios.js.map +1 -1
  115. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  116. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  117. package/node_modules/axios/dist/node/axios.cjs +91 -37
  118. package/node_modules/axios/dist/node/axios.cjs.map +1 -1
  119. package/node_modules/axios/index.d.cts +1 -0
  120. package/node_modules/axios/index.d.ts +1 -0
  121. package/node_modules/axios/lib/adapters/http.js +69 -22
  122. package/node_modules/axios/lib/core/mergeConfig.js +13 -1
  123. package/node_modules/axios/lib/env/data.js +1 -1
  124. package/node_modules/axios/lib/helpers/resolveConfig.js +14 -2
  125. package/node_modules/axios/lib/helpers/validator.js +3 -1
  126. package/node_modules/axios/package.json +1 -1
  127. package/package.json +9 -9
  128. package/packages/cloud/package.json +2 -2
  129. package/packages/config/package.json +2 -2
  130. package/packages/hooks/package.json +4 -4
  131. package/packages/sdk/dist/relay.d.ts.map +1 -1
  132. package/packages/sdk/dist/relay.js.map +1 -1
  133. package/packages/sdk/dist/workflows/__tests__/budget-enforcement.test.d.ts +2 -0
  134. package/packages/sdk/dist/workflows/__tests__/budget-enforcement.test.d.ts.map +1 -0
  135. package/packages/sdk/dist/workflows/__tests__/budget-enforcement.test.js +437 -0
  136. package/packages/sdk/dist/workflows/__tests__/budget-enforcement.test.js.map +1 -0
  137. package/packages/sdk/dist/workflows/__tests__/budget-tracker.test.d.ts +2 -0
  138. package/packages/sdk/dist/workflows/__tests__/budget-tracker.test.d.ts.map +1 -0
  139. package/packages/sdk/dist/workflows/__tests__/budget-tracker.test.js +99 -0
  140. package/packages/sdk/dist/workflows/__tests__/budget-tracker.test.js.map +1 -0
  141. package/packages/sdk/dist/workflows/__tests__/proxy-env.test.d.ts +2 -0
  142. package/packages/sdk/dist/workflows/__tests__/proxy-env.test.d.ts.map +1 -0
  143. package/packages/sdk/dist/workflows/__tests__/proxy-env.test.js +135 -0
  144. package/packages/sdk/dist/workflows/__tests__/proxy-env.test.js.map +1 -0
  145. package/packages/sdk/dist/workflows/__tests__/verification-custom.test.d.ts +2 -0
  146. package/packages/sdk/dist/workflows/__tests__/verification-custom.test.d.ts.map +1 -0
  147. package/packages/sdk/dist/workflows/__tests__/verification-custom.test.js +236 -0
  148. package/packages/sdk/dist/workflows/__tests__/verification-custom.test.js.map +1 -0
  149. package/packages/sdk/dist/workflows/__tests__/verification-traceback.test.d.ts +2 -0
  150. package/packages/sdk/dist/workflows/__tests__/verification-traceback.test.d.ts.map +1 -0
  151. package/packages/sdk/dist/workflows/__tests__/verification-traceback.test.js +448 -0
  152. package/packages/sdk/dist/workflows/__tests__/verification-traceback.test.js.map +1 -0
  153. package/packages/sdk/dist/workflows/__tests__/verification.test.js +71 -4
  154. package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -1
  155. package/packages/sdk/dist/workflows/budget-tracker.d.ts +75 -0
  156. package/packages/sdk/dist/workflows/budget-tracker.d.ts.map +1 -0
  157. package/packages/sdk/dist/workflows/budget-tracker.js +180 -0
  158. package/packages/sdk/dist/workflows/budget-tracker.js.map +1 -0
  159. package/packages/sdk/dist/workflows/builder.d.ts +1 -0
  160. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  161. package/packages/sdk/dist/workflows/builder.js +17 -2
  162. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  163. package/packages/sdk/dist/workflows/index.d.ts +2 -0
  164. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  165. package/packages/sdk/dist/workflows/index.js +2 -0
  166. package/packages/sdk/dist/workflows/index.js.map +1 -1
  167. package/packages/sdk/dist/workflows/proxy-env.d.ts +52 -0
  168. package/packages/sdk/dist/workflows/proxy-env.d.ts.map +1 -0
  169. package/packages/sdk/dist/workflows/proxy-env.js +92 -0
  170. package/packages/sdk/dist/workflows/proxy-env.js.map +1 -0
  171. package/packages/sdk/dist/workflows/run-summary-table.d.ts +2 -1
  172. package/packages/sdk/dist/workflows/run-summary-table.d.ts.map +1 -1
  173. package/packages/sdk/dist/workflows/run-summary-table.js +41 -9
  174. package/packages/sdk/dist/workflows/run-summary-table.js.map +1 -1
  175. package/packages/sdk/dist/workflows/runner.d.ts +17 -0
  176. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  177. package/packages/sdk/dist/workflows/runner.js +390 -16
  178. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  179. package/packages/sdk/dist/workflows/types.d.ts +47 -1
  180. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  181. package/packages/sdk/dist/workflows/types.js.map +1 -1
  182. package/packages/sdk/dist/workflows/verification.d.ts +9 -0
  183. package/packages/sdk/dist/workflows/verification.d.ts.map +1 -1
  184. package/packages/sdk/dist/workflows/verification.js +78 -2
  185. package/packages/sdk/dist/workflows/verification.js.map +1 -1
  186. package/packages/sdk/package.json +8 -4
  187. package/packages/telemetry/dist/config.d.ts.map +1 -1
  188. package/packages/telemetry/dist/config.js +2 -4
  189. package/packages/telemetry/dist/config.js.map +1 -1
  190. package/packages/telemetry/dist/events.d.ts +1 -2
  191. package/packages/telemetry/dist/events.d.ts.map +1 -1
  192. package/packages/telemetry/dist/index.d.ts +1 -1
  193. package/packages/telemetry/dist/index.d.ts.map +1 -1
  194. package/packages/telemetry/dist/index.js +1 -1
  195. package/packages/telemetry/dist/index.js.map +1 -1
  196. package/packages/telemetry/package.json +1 -1
  197. package/packages/trajectory/package.json +2 -2
  198. package/packages/user-directory/package.json +2 -2
  199. package/packages/utils/package.json +2 -2
@@ -16,10 +16,12 @@ import { stripAnsi as stripAnsiFn } from '../pty.js';
16
16
  import { resolveSpawnPolicy } from '../spawn-from-env.js';
17
17
  import { getCliDefinition } from '../cli-registry.js';
18
18
  import { resolveCliSync } from '../cli-resolver.js';
19
+ import { buildNormalizedProxyEnv, getStrippedApiKeyVars, resolveProxyEnv, resolveProxyTokenFromEnv, resolveProxyUrlFromEnv, } from './proxy-env.js';
19
20
  import { loadCustomSteps, resolveAllCustomSteps, validateCustomStepsUsage, CustomStepsParseError, CustomStepResolutionError, } from './custom-steps.js';
20
21
  import { provisionWorkflowAgents, resolveAgentPermissions } from '../provisioner/index.js';
21
22
  import { collectCliSession } from './cli-session-collector.js';
22
23
  import { executeApiStep } from './api-executor.js';
24
+ import { BudgetExceededError, BudgetTracker } from './budget-tracker.js';
23
25
  import { ChannelMessenger } from './channel-messenger.js';
24
26
  import { InMemoryWorkflowDb } from './memory-db.js';
25
27
  import { buildCommand as buildProcessCommand, spawnProcess } from './process-spawner.js';
@@ -52,6 +54,10 @@ const ENV_ALLOWLIST = new Set([
52
54
  'RUST_BACKTRACE',
53
55
  'RELAY_API_KEY',
54
56
  'RELAYCAST_BASE_URL',
57
+ 'RELAY_LLM_PROXY',
58
+ 'RELAY_LLM_PROXY_URL',
59
+ 'CREDENTIAL_PROXY_TOKEN',
60
+ 'RELAY_LLM_PROXY_TOKEN',
55
61
  'AGENT_RELAY_DASHBOARD_PORT',
56
62
  'AGENT_RELAY_RUN_ID_FILE',
57
63
  'EDITOR',
@@ -149,12 +155,16 @@ export class WorkflowRunner {
149
155
  activeAgentHandles = new Map();
150
156
  /** Per-agent workflow tokens for relay/relayfile auth across spawn modes. */
151
157
  agentTokens = new Map();
158
+ /** Per-agent credential proxy tokens keyed by logical agent definition name. */
159
+ proxyTokens = new Map();
152
160
  /** Per-agent relayfile mounts keyed by logical agent definition name. */
153
161
  agentMounts = new Map();
154
162
  // PTY-based output capture: accumulate terminal output per-agent
155
163
  ptyOutputBuffers = new Map();
156
164
  /** Snapshot of PTY output from the most recent failed attempt, keyed by step name. */
157
165
  lastFailedStepOutput = new Map();
166
+ /** Most recent custom verification failure details, keyed by step name. */
167
+ lastCustomVerificationFailure = new Map();
158
168
  ptyListeners = new Map();
159
169
  ptyLogStreams = new Map();
160
170
  /** Path to workers.json so `agents:kill` can find workflow-spawned agents */
@@ -185,6 +195,8 @@ export class WorkflowRunner {
185
195
  activeReviewers = new Map();
186
196
  /** Structured CLI session reports captured during the current run, keyed by step name. */
187
197
  agentReports = new Map();
198
+ /** Optional per-run token budget tracker; only created when budgets are configured. */
199
+ budgetTracker;
188
200
  static PTY_TASK_ARG_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB
189
201
  processBackend;
190
202
  constructor(options = {}) {
@@ -245,6 +257,51 @@ export class WorkflowRunner {
245
257
  }
246
258
  return { resolved, errors, warnings };
247
259
  }
260
+ initializeBudgetTracker(config, workflow) {
261
+ const agentMap = new Map(config.agents.map((agent) => [agent.name, WorkflowRunner.resolveAgentDef(agent)]));
262
+ const stepConfigs = workflow.steps.flatMap((step) => {
263
+ if (step.type === 'deterministic' ||
264
+ step.type === 'worktree' ||
265
+ step.type === 'integration' ||
266
+ !step.agent) {
267
+ return [];
268
+ }
269
+ const agentDef = agentMap.get(step.agent);
270
+ return [
271
+ {
272
+ stepName: step.name,
273
+ agentName: step.agent,
274
+ maxTokens: agentDef?.constraints?.maxTokens,
275
+ },
276
+ ];
277
+ });
278
+ const hasWorkflowBudget = config.swarm.tokenBudget !== undefined;
279
+ const hasAgentBudgets = stepConfigs.some((step) => step.maxTokens !== undefined);
280
+ this.budgetTracker =
281
+ hasWorkflowBudget || hasAgentBudgets
282
+ ? new BudgetTracker({
283
+ workflowBudget: config.swarm.tokenBudget,
284
+ steps: stepConfigs,
285
+ })
286
+ : undefined;
287
+ }
288
+ ensureBudgetAllowsSpawn(stepName, agentName) {
289
+ if (!this.budgetTracker)
290
+ return;
291
+ const budgetCheck = this.budgetTracker.checkCanSpawn(stepName);
292
+ if (budgetCheck.allowed)
293
+ return;
294
+ const workflowBudget = this.budgetTracker.getRunSummaryBudgetData()?.workflow;
295
+ const used = workflowBudget?.used.toLocaleString('en-US') ?? '0';
296
+ const limit = workflowBudget?.limit?.toLocaleString('en-US') ?? '--';
297
+ this.log(`[budget] Skipping step ${stepName} — workflow budget exhausted (used ${used} of ${limit})`);
298
+ throw new BudgetExceededError(stepName, 'workflow', workflowBudget?.limit ?? 0, workflowBudget?.used ?? 0);
299
+ }
300
+ getTotalReportTokens(report) {
301
+ if (!report.tokens)
302
+ return undefined;
303
+ return report.tokens.input + report.tokens.output + report.tokens.cacheRead;
304
+ }
248
305
  validatePermissions(agents, permissionProfiles, source = '<config>') {
249
306
  const errors = [];
250
307
  const warnings = [];
@@ -1091,16 +1148,169 @@ export class WorkflowRunner {
1091
1148
  // Dashboard not running — silently ignore.
1092
1149
  });
1093
1150
  }
1094
- getRelayEnv() {
1095
- if (!this.relayApiKey && !this.relayOptions.env) {
1151
+ async loadCredentialProxyModule() {
1152
+ try {
1153
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
1154
+ const module = (await dynamicImport('@agent-relay/credential-proxy'));
1155
+ return typeof module.mintProxyToken === 'function' ? module : null;
1156
+ }
1157
+ catch (error) {
1158
+ if (error?.code === 'ERR_MODULE_NOT_FOUND') {
1159
+ return null;
1160
+ }
1161
+ throw error;
1162
+ }
1163
+ }
1164
+ resolveCredentialProxyProvider(agentDef, config) {
1165
+ const configuredProviders = Object.keys(config.swarm.credentialProxy?.providers ?? {});
1166
+ const explicitProvider = agentDef.credentials?.provider?.trim().toLowerCase();
1167
+ if (explicitProvider === 'openai' ||
1168
+ explicitProvider === 'anthropic' ||
1169
+ explicitProvider === 'openrouter') {
1170
+ return explicitProvider;
1171
+ }
1172
+ const model = agentDef.constraints?.model?.trim().toLowerCase() ?? '';
1173
+ if (model.includes('openrouter')) {
1174
+ return 'openrouter';
1175
+ }
1176
+ if (model.includes('claude') || model.includes('anthropic')) {
1177
+ return 'anthropic';
1178
+ }
1179
+ if (model.includes('openai') ||
1180
+ model.includes('chatgpt') ||
1181
+ model.includes('gpt') ||
1182
+ /\bo[134](?:\b|-)/.test(model)) {
1183
+ return 'openai';
1184
+ }
1185
+ if (configuredProviders.length === 1) {
1186
+ const [onlyProvider] = configuredProviders;
1187
+ if (onlyProvider === 'openai' || onlyProvider === 'anthropic' || onlyProvider === 'openrouter') {
1188
+ return onlyProvider;
1189
+ }
1190
+ }
1191
+ switch (agentDef.cli) {
1192
+ case 'claude':
1193
+ return 'anthropic';
1194
+ case 'codex':
1195
+ case 'aider':
1196
+ case 'goose':
1197
+ case 'opencode':
1198
+ case 'cursor':
1199
+ case 'cursor-agent':
1200
+ return 'openai';
1201
+ default:
1202
+ throw new Error(`Unable to resolve credential proxy provider for agent "${agentDef.name}". Set credentials.provider or constraints.model.`);
1203
+ }
1204
+ }
1205
+ resolveCredentialProxySecret(config) {
1206
+ const configuredSecret = config.swarm.credentialProxy?.jwtSecret;
1207
+ if (configuredSecret?.startsWith('$')) {
1208
+ const envSecret = process.env[configuredSecret.slice(1)];
1209
+ if (envSecret) {
1210
+ return envSecret;
1211
+ }
1212
+ }
1213
+ else if (configuredSecret) {
1214
+ return configuredSecret;
1215
+ }
1216
+ const defaultSecret = process.env.RELAY_PROXY_JWT_SECRET;
1217
+ if (defaultSecret) {
1218
+ return defaultSecret;
1219
+ }
1220
+ throw new Error('Credential proxy JWT secret is missing. Set swarm.credentialProxy.jwtSecret or RELAY_PROXY_JWT_SECRET.');
1221
+ }
1222
+ async mintAgentProxyToken(agentDef, config) {
1223
+ const proxyConfig = config.swarm?.credentialProxy;
1224
+ if (!proxyConfig?.proxyUrl || !agentDef.credentials?.proxy) {
1225
+ return undefined;
1226
+ }
1227
+ const provider = this.resolveCredentialProxyProvider(agentDef, config);
1228
+ const providerConfig = proxyConfig.providers?.[provider];
1229
+ const credentialId = providerConfig?.credentialId;
1230
+ if (!credentialId) {
1231
+ throw new Error(`Credential proxy provider "${provider}" is not configured for agent "${agentDef.name}".`);
1232
+ }
1233
+ const budget = agentDef.constraints?.maxTokens ?? proxyConfig.defaultBudget;
1234
+ const cacheKey = `${agentDef.name}:${provider}:${credentialId}:${budget ?? 'default'}`;
1235
+ const cachedToken = this.proxyTokens.get(cacheKey);
1236
+ if (cachedToken) {
1237
+ return cachedToken;
1238
+ }
1239
+ const credentialProxy = await this.loadCredentialProxyModule();
1240
+ if (!credentialProxy) {
1241
+ throw new Error('Credential proxy mode requires the optional peer dependency "@agent-relay/credential-proxy".');
1242
+ }
1243
+ const nowSeconds = Math.floor(Date.now() / 1000);
1244
+ const token = await credentialProxy.mintProxyToken({
1245
+ sub: this.workspaceId,
1246
+ aud: 'relay-llm-proxy',
1247
+ provider,
1248
+ credentialId,
1249
+ budget,
1250
+ exp: nowSeconds + 15 * 60,
1251
+ }, this.resolveCredentialProxySecret(config));
1252
+ this.proxyTokens.set(cacheKey, token);
1253
+ return token;
1254
+ }
1255
+ async resolveAgentProxyMode(agentDef, config) {
1256
+ if (!agentDef.credentials?.proxy) {
1257
+ return undefined;
1258
+ }
1259
+ const env = this.getMergedRelayEnvSource();
1260
+ const configuredProxyUrl = config?.swarm?.credentialProxy?.proxyUrl;
1261
+ const proxyUrl = configuredProxyUrl ?? resolveProxyUrlFromEnv(env);
1262
+ if (!proxyUrl) {
1096
1263
  return undefined;
1097
1264
  }
1265
+ if (!configuredProxyUrl) {
1266
+ const injectedToken = resolveProxyTokenFromEnv(env);
1267
+ if (!injectedToken) {
1268
+ return undefined;
1269
+ }
1270
+ return {
1271
+ url: proxyUrl,
1272
+ token: injectedToken,
1273
+ };
1274
+ }
1275
+ const token = await this.mintAgentProxyToken(agentDef, config);
1276
+ if (!token) {
1277
+ return undefined;
1278
+ }
1279
+ return {
1280
+ url: proxyUrl,
1281
+ token,
1282
+ };
1283
+ }
1284
+ getMergedRelayEnvSource() {
1098
1285
  return {
1099
1286
  ...process.env,
1100
1287
  ...(this.relayOptions.env ?? {}),
1101
1288
  ...(this.relayApiKey ? { RELAY_API_KEY: this.relayApiKey } : {}),
1102
1289
  };
1103
1290
  }
1291
+ getRelayEnv(proxyMode) {
1292
+ const env = this.getMergedRelayEnvSource();
1293
+ const inheritedProxyUrl = resolveProxyUrlFromEnv(env);
1294
+ const inheritedProxyToken = resolveProxyTokenFromEnv(env);
1295
+ if (!this.relayApiKey &&
1296
+ !this.relayOptions.env &&
1297
+ !proxyMode &&
1298
+ !(inheritedProxyUrl && inheritedProxyToken)) {
1299
+ return undefined;
1300
+ }
1301
+ const normalizedProxy = proxyMode?.url && proxyMode.token
1302
+ ? proxyMode
1303
+ : inheritedProxyUrl && inheritedProxyToken
1304
+ ? { url: inheritedProxyUrl, token: inheritedProxyToken }
1305
+ : undefined;
1306
+ if (normalizedProxy) {
1307
+ Object.assign(env, buildNormalizedProxyEnv(normalizedProxy.url, normalizedProxy.token));
1308
+ for (const key of getStrippedApiKeyVars()) {
1309
+ delete env[key];
1310
+ }
1311
+ }
1312
+ return env;
1313
+ }
1104
1314
  async provisionAgents(config) {
1105
1315
  // Cloud launcher already compiled and seeded relayfile ACLs before the
1106
1316
  // sandbox started. Skip in-sandbox provisioning — the relayfile API has
@@ -1109,6 +1319,7 @@ export class WorkflowRunner {
1109
1319
  return;
1110
1320
  }
1111
1321
  this.agentTokens.clear();
1322
+ this.proxyTokens.clear();
1112
1323
  await this.stopProvisionedMounts();
1113
1324
  const agentsToProvision = {};
1114
1325
  for (const agent of config.agents) {
@@ -2042,6 +2253,7 @@ export class WorkflowRunner {
2042
2253
  this.runtimeStepAgents.clear();
2043
2254
  this.stepCompletionEvidence.clear();
2044
2255
  this.agentReports.clear();
2256
+ this.initializeBudgetTracker(config, workflow);
2045
2257
  this.log(`Starting workflow "${workflow.name}" (${workflow.steps.length} steps)`);
2046
2258
  // Initialize trajectory recording
2047
2259
  this.trajectory = new WorkflowTrajectory(config.trajectories, runId, this.cwd);
@@ -2371,6 +2583,7 @@ export class WorkflowRunner {
2371
2583
  }
2372
2584
  finally {
2373
2585
  this.lastFailedStepOutput.clear();
2586
+ this.lastCustomVerificationFailure.clear();
2374
2587
  for (const stream of this.ptyLogStreams.values())
2375
2588
  stream.end();
2376
2589
  this.ptyLogStreams.clear();
@@ -2973,6 +3186,7 @@ export class WorkflowRunner {
2973
3186
  const specialistDef = WorkflowRunner.resolveAgentDef(rawAgentDef);
2974
3187
  // API-mode agents: execute via direct API call instead of spawning a PTY/subprocess.
2975
3188
  if (specialistDef.cli === 'api') {
3189
+ this.ensureBudgetAllowsSpawn(step.name, agentName);
2976
3190
  const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
2977
3191
  const resolvedTask = this.interpolateStepTask(step.task ?? '', stepOutputContext);
2978
3192
  state.row.status = 'running';
@@ -3050,6 +3264,8 @@ export class WorkflowRunner {
3050
3264
  let lastAttemptStartedAt;
3051
3265
  let lastEffectiveAgentDef;
3052
3266
  let lastEffectiveCwd;
3267
+ let lastAttemptReportCaptured = false;
3268
+ let lastDiagnosticResult = null;
3053
3269
  // OWNER_DECISION: INCOMPLETE_RETRY is enforced here at the attempt-loop level so every
3054
3270
  // interactive execution path shares the same contract:
3055
3271
  // - retries remaining => throw back into the loop and retry
@@ -3060,6 +3276,11 @@ export class WorkflowRunner {
3060
3276
  // Reset per-attempt exit info so stale values don't leak across retries
3061
3277
  lastExitCode = undefined;
3062
3278
  lastExitSignal = undefined;
3279
+ lastAttemptStartedAt = undefined;
3280
+ lastEffectiveAgentDef = undefined;
3281
+ lastEffectiveCwd = undefined;
3282
+ lastAttemptReportCaptured = false;
3283
+ let stepOutputForDiagnostic = '';
3063
3284
  if (attempt > 0) {
3064
3285
  this.emit({ type: 'step:retrying', runId, stepName: step.name, attempt });
3065
3286
  this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
@@ -3077,6 +3298,7 @@ export class WorkflowRunner {
3077
3298
  await this.delay(retryDelay);
3078
3299
  }
3079
3300
  try {
3301
+ this.ensureBudgetAllowsSpawn(step.name, agentName);
3080
3302
  lastAttemptStartedAt = Date.now();
3081
3303
  // Mark step as running
3082
3304
  state.row.status = 'running';
@@ -3114,12 +3336,29 @@ export class WorkflowRunner {
3114
3336
  let resolvedTask = this.interpolateStepTask(step.task ?? '', stepOutputContext);
3115
3337
  // On retry attempts, prepend failure context so the agent knows what went wrong
3116
3338
  if (attempt > 0 && lastError) {
3117
- const priorOutput = (this.lastFailedStepOutput.get(step.name) ?? '').slice(-2000);
3118
- resolvedTask =
3119
- `[RETRY — Attempt ${attempt + 1}/${maxRetries + 1}]\n` +
3120
- `Previous attempt failed: ${lastError}\n` +
3121
- (priorOutput ? `Previous output (last 2000 chars):\n${priorOutput}\n` : '') +
3122
- `---\n${resolvedTask}`;
3339
+ if (lastDiagnosticResult) {
3340
+ resolvedTask =
3341
+ `[RETRY — Attempt ${attempt + 1}/${maxRetries + 1}] Verification failed.\n` +
3342
+ `Diagnostic analysis:\n${lastDiagnosticResult.analysis}\n\n` +
3343
+ `Original error: ${lastError}\n---\n${resolvedTask}`;
3344
+ }
3345
+ else {
3346
+ const priorOutput = (this.lastFailedStepOutput.get(step.name) ?? '').slice(-2000);
3347
+ const customVerificationFailure = this.lastCustomVerificationFailure.get(step.name);
3348
+ const verificationFailurePrompt = customVerificationFailure
3349
+ ? `[VERIFICATION FAILED] Your code did not pass the verification check.\n` +
3350
+ `Command: ${customVerificationFailure.command}\n` +
3351
+ `Output:\n` +
3352
+ `${customVerificationFailure.output}\n\n` +
3353
+ `Fix the issues above before proceeding.\n`
3354
+ : '';
3355
+ resolvedTask =
3356
+ `[RETRY — Attempt ${attempt + 1}/${maxRetries + 1}]\n` +
3357
+ `Previous attempt failed: ${lastError}\n` +
3358
+ verificationFailurePrompt +
3359
+ (priorOutput ? `Previous output (last 2000 chars):\n${priorOutput}\n` : '') +
3360
+ `---\n${resolvedTask}`;
3361
+ }
3123
3362
  }
3124
3363
  // If this is an interactive agent, append awareness of non-interactive workers
3125
3364
  // so the lead knows not to message them and to use step output chaining instead
@@ -3160,6 +3399,7 @@ export class WorkflowRunner {
3160
3399
  if (usesDedicatedOwner) {
3161
3400
  const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs, attempt);
3162
3401
  specialistOutput = result.specialistOutput;
3402
+ stepOutputForDiagnostic = result.specialistOutput;
3163
3403
  ownerOutput = result.ownerOutput;
3164
3404
  ownerElapsed = result.ownerElapsed;
3165
3405
  completionReason = result.completionReason;
@@ -3228,6 +3468,7 @@ export class WorkflowRunner {
3228
3468
  }
3229
3469
  }
3230
3470
  specialistOutput = output;
3471
+ stepOutputForDiagnostic = output;
3231
3472
  ownerOutput = output;
3232
3473
  }
3233
3474
  // Even non-interactive steps can emit an explicit OWNER_DECISION contract.
@@ -3281,6 +3522,7 @@ export class WorkflowRunner {
3281
3522
  }
3282
3523
  }
3283
3524
  await this.captureAgentReport(runId, step.name, lastEffectiveAgentDef, lastEffectiveCwd, lastAttemptStartedAt, Date.now());
3525
+ lastAttemptReportCaptured = true;
3284
3526
  // Mark completed
3285
3527
  state.row.status = 'completed';
3286
3528
  state.row.output = combinedOutput;
@@ -3310,6 +3552,26 @@ export class WorkflowRunner {
3310
3552
  catch (err) {
3311
3553
  lastError = err instanceof Error ? err.message : String(err);
3312
3554
  lastCompletionReason = err instanceof WorkflowCompletionError ? err.completionReason : undefined;
3555
+ const diagnosticVerification = step.verification;
3556
+ if (err instanceof WorkflowCompletionError &&
3557
+ err.completionReason === 'failed_verification' &&
3558
+ diagnosticVerification?.diagnosticAgent &&
3559
+ attempt < maxRetries) {
3560
+ lastDiagnosticResult = await this.runDiagnosticAgent(step, lastError, stepOutputForDiagnostic || (this.lastFailedStepOutput.get(step.name) ?? ''), agentMap, runId);
3561
+ }
3562
+ else {
3563
+ lastDiagnosticResult = null;
3564
+ }
3565
+ if (lastCompletionReason !== 'failed_verification') {
3566
+ this.lastCustomVerificationFailure.delete(step.name);
3567
+ }
3568
+ if (!(err instanceof BudgetExceededError) && !lastAttemptReportCaptured) {
3569
+ await this.captureAgentReport(runId, step.name, lastEffectiveAgentDef, lastEffectiveCwd, lastAttemptStartedAt, Date.now());
3570
+ lastAttemptReportCaptured = true;
3571
+ }
3572
+ if (err instanceof BudgetExceededError) {
3573
+ break;
3574
+ }
3313
3575
  if (lastCompletionReason === 'retry_requested_by_owner' && attempt >= maxRetries) {
3314
3576
  lastError = this.buildOwnerRetryBudgetExceededMessage(step.name, maxRetries, lastError);
3315
3577
  }
@@ -3330,7 +3592,9 @@ export class WorkflowRunner {
3330
3592
  const verificationValue = typeof step.verification === 'object' && 'value' in step.verification
3331
3593
  ? String(step.verification.value)
3332
3594
  : undefined;
3333
- await this.captureAgentReport(runId, step.name, lastEffectiveAgentDef, lastEffectiveCwd, lastAttemptStartedAt, Date.now());
3595
+ if (!lastAttemptReportCaptured) {
3596
+ await this.captureAgentReport(runId, step.name, lastEffectiveAgentDef, lastEffectiveCwd, lastAttemptStartedAt, Date.now());
3597
+ }
3334
3598
  await this.trajectory?.stepFailed(step, lastError ?? 'Unknown error', maxRetries + 1, maxRetries, {
3335
3599
  agent: agentName,
3336
3600
  nonInteractive,
@@ -3343,6 +3607,73 @@ export class WorkflowRunner {
3343
3607
  }, lastCompletionReason);
3344
3608
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? 'Unknown error'}`);
3345
3609
  }
3610
+ async runDiagnosticAgent(step, verificationError, stepOutput, agentMap, runId) {
3611
+ const verification = step.verification;
3612
+ const diagnosticAgentName = verification?.diagnosticAgent;
3613
+ if (!verification || !diagnosticAgentName) {
3614
+ return null;
3615
+ }
3616
+ const rawDiagnosticDef = agentMap.get(diagnosticAgentName);
3617
+ if (!rawDiagnosticDef) {
3618
+ this.log(`[${step.name}] Diagnostic agent "${diagnosticAgentName}" not found — falling back to standard retry`);
3619
+ return null;
3620
+ }
3621
+ const diagnosticAgentDef = {
3622
+ ...WorkflowRunner.resolveAgentDef(rawDiagnosticDef),
3623
+ interactive: false,
3624
+ };
3625
+ const verificationCommand = verification.type === 'custom' ? verification.value : `${verification.type}: ${verification.value}`;
3626
+ const diagnosticTimeout = verification.diagnosticTimeout ?? 60_000;
3627
+ const diagnosticPrompt = `The following verification failed after step "${step.name}".\n\n` +
3628
+ `Verification command: ${verificationCommand}\n` +
3629
+ `Verification output:\n${verificationError}\n\n` +
3630
+ `Step task was:\n${step.task ?? ''}\n\n` +
3631
+ `Step output (last 2000 chars):\n${stepOutput.slice(-2000)}\n\n` +
3632
+ `Analyze what went wrong. Be specific. Do NOT fix the code.`;
3633
+ const diagnosticStep = {
3634
+ ...step,
3635
+ name: `${step.name}-diagnostic-${runId.slice(0, 8)}`,
3636
+ agent: diagnosticAgentName,
3637
+ task: diagnosticPrompt,
3638
+ verification: undefined,
3639
+ retries: 0,
3640
+ };
3641
+ const diagnosticCwd = this.resolveExecutionCwd(diagnosticStep, diagnosticAgentDef);
3642
+ const startedAt = Date.now();
3643
+ try {
3644
+ this.ensureBudgetAllowsSpawn(step.name, diagnosticAgentName);
3645
+ this.log(`[${step.name}] Verification failed — running diagnostic agent '${diagnosticAgentName}'...`);
3646
+ const diagnosticResult = await this.execNonInteractive(diagnosticAgentDef, diagnosticStep, diagnosticTimeout);
3647
+ const elapsedMs = Date.now() - startedAt;
3648
+ await this.captureAgentReport(runId, step.name, diagnosticAgentDef, diagnosticCwd, startedAt, Date.now());
3649
+ const analysis = diagnosticResult.output.trim();
3650
+ const tokenCount = Math.max(1, Math.ceil(analysis.length / 4));
3651
+ const firstLine = analysis
3652
+ .split(/\r?\n/)
3653
+ .map((line) => line.trim())
3654
+ .find(Boolean) ?? '(no analysis returned)';
3655
+ this.log(`[${step.name}] Diagnostic complete (${elapsedMs}ms, ${tokenCount} tokens): ${firstLine}`);
3656
+ return {
3657
+ analysis,
3658
+ metadata: {
3659
+ agentName: diagnosticAgentName,
3660
+ elapsedMs,
3661
+ tokenCount,
3662
+ },
3663
+ };
3664
+ }
3665
+ catch (error) {
3666
+ await this.captureAgentReport(runId, step.name, diagnosticAgentDef, diagnosticCwd, startedAt, Date.now());
3667
+ const message = error instanceof Error ? error.message : String(error);
3668
+ if (/\btimed out\b/i.test(message)) {
3669
+ this.log(`[${step.name}] Diagnostic timed out — falling back to standard retry`);
3670
+ }
3671
+ else {
3672
+ this.log(`[${step.name}] Diagnostic failed — falling back to standard retry: ${message}`);
3673
+ }
3674
+ return null;
3675
+ }
3676
+ }
3346
3677
  buildOwnerRetryBudgetExceededMessage(stepName, maxRetries, ownerDecisionError) {
3347
3678
  const attempts = maxRetries + 1;
3348
3679
  const prefix = `Step "${stepName}" `;
@@ -4340,7 +4671,11 @@ export class WorkflowRunner {
4340
4671
  this.postToChannel(`**[${step.name}]** Assigned to \`${agentName}\` (non-interactive)`);
4341
4672
  const stdoutChunks = [];
4342
4673
  const stderrChunks = [];
4343
- const env = { ...(this.getRelayEnv() ?? filteredEnv()) };
4674
+ const proxyMode = await this.resolveAgentProxyMode(agentDef, this.currentConfig);
4675
+ const env = { ...(this.getRelayEnv(proxyMode) ?? filteredEnv()) };
4676
+ if (proxyMode?.url && proxyMode.token) {
4677
+ Object.assign(env, resolveProxyEnv(agentDef.cli, proxyMode.url, proxyMode.token));
4678
+ }
4344
4679
  const agentToken = this.agentTokens.get(agentDef.name);
4345
4680
  const mount = this.agentMounts.get(agentDef.name);
4346
4681
  if (agentToken) {
@@ -4356,7 +4691,7 @@ export class WorkflowRunner {
4356
4691
  }
4357
4692
  env.RELAYFILE_BASE_URL =
4358
4693
  env.RELAYFILE_BASE_URL ??
4359
- this.getRelayEnv()?.RELAYFILE_BASE_URL ??
4694
+ this.getRelayEnv(proxyMode)?.RELAYFILE_BASE_URL ??
4360
4695
  process.env.RELAYFILE_BASE_URL ??
4361
4696
  'http://127.0.0.1:8080';
4362
4697
  try {
@@ -4555,6 +4890,11 @@ export class WorkflowRunner {
4555
4890
  RELAY_API_KEY: this.relayApiKey ?? 'workflow-runner',
4556
4891
  AGENT_CHANNELS: (agentChannels ?? []).join(','),
4557
4892
  });
4893
+ const proxyMode = await this.resolveAgentProxyMode(agentDef, this.currentConfig);
4894
+ const baseEnv = this.getRelayEnv(proxyMode);
4895
+ const proxyEnvOverrides = proxyMode?.url && proxyMode.token
4896
+ ? resolveProxyEnv(agentDef.cli, proxyMode.url, proxyMode.token)
4897
+ : undefined;
4558
4898
  const spawnOptions = {
4559
4899
  name: agentName,
4560
4900
  model: agentDef.constraints?.model,
@@ -4564,6 +4904,7 @@ export class WorkflowRunner {
4564
4904
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
4565
4905
  cwd: agentCwd,
4566
4906
  agentToken: this.agentTokens.get(agentDef.name),
4907
+ env: proxyEnvOverrides ? { ...baseEnv, ...proxyEnvOverrides } : baseEnv,
4567
4908
  };
4568
4909
  const sdkSpawner = getWorkflowSdkSpawner(this.relay, agentDef.cli);
4569
4910
  if (sdkSpawner) {
@@ -4965,10 +5306,31 @@ export class WorkflowRunner {
4965
5306
  }
4966
5307
  // ── Verification ────────────────────────────────────────────────────────
4967
5308
  runVerification(check, output, stepName, injectedTaskText, options) {
4968
- return runVerification(check, output, stepName, injectedTaskText, { ...options, cwd: this.cwd }, {
4969
- recordStepToolSideEffect: (name, effect) => this.recordStepToolSideEffect(name, effect),
4970
- getOrCreateStepEvidenceRecord: (name) => this.getOrCreateStepEvidenceRecord(name),
4971
- log: (message) => this.log(message),
5309
+ try {
5310
+ const result = runVerification(check, output, stepName, injectedTaskText, { ...options, cwd: this.cwd }, {
5311
+ recordStepToolSideEffect: (name, effect) => this.recordStepToolSideEffect(name, effect),
5312
+ getOrCreateStepEvidenceRecord: (name) => this.getOrCreateStepEvidenceRecord(name),
5313
+ log: (message) => this.log(message),
5314
+ });
5315
+ this.updateCustomVerificationFailure(stepName, check, result.error);
5316
+ return result;
5317
+ }
5318
+ catch (error) {
5319
+ this.updateCustomVerificationFailure(stepName, check, error instanceof Error ? error.message : String(error));
5320
+ throw error;
5321
+ }
5322
+ }
5323
+ updateCustomVerificationFailure(stepName, check, errorMessage) {
5324
+ if (check.type !== 'custom' || !check.value || !errorMessage) {
5325
+ this.lastCustomVerificationFailure.delete(stepName);
5326
+ return;
5327
+ }
5328
+ const marker = `custom check "${check.value}" failed\n`;
5329
+ const markerIndex = errorMessage.indexOf(marker);
5330
+ const output = markerIndex === -1 ? errorMessage.trim() : errorMessage.slice(markerIndex + marker.length).trim();
5331
+ this.lastCustomVerificationFailure.set(stepName, {
5332
+ command: check.value,
5333
+ output,
4972
5334
  });
4973
5335
  }
4974
5336
  // ── State helpers ─────────────────────────────────────────────────────
@@ -5020,6 +5382,18 @@ export class WorkflowRunner {
5020
5382
  });
5021
5383
  if (!report)
5022
5384
  return;
5385
+ const totalTokens = this.getTotalReportTokens(report);
5386
+ if (this.budgetTracker && report.tokens) {
5387
+ this.budgetTracker.recordUsage(stepName, report.tokens);
5388
+ this.budgetTracker.isOverBudget(stepName);
5389
+ const budgetStatus = this.budgetTracker.getBudgetStatus(stepName);
5390
+ if (budgetStatus.agentLimitExceeded) {
5391
+ const stepBudget = this.budgetTracker.getStepBudgetStatus(stepName);
5392
+ const used = stepBudget?.used?.toLocaleString('en-US') ?? totalTokens?.toLocaleString('en-US') ?? '0';
5393
+ const limit = stepBudget?.limit?.toLocaleString('en-US') ?? '--';
5394
+ this.log(`[budget] Step ${stepName} exceeded its agent budget (${used} of ${limit})`);
5395
+ }
5396
+ }
5023
5397
  this.agentReports.set(stepName, report);
5024
5398
  this.emit({ type: 'step:agent-report', runId, stepName, report });
5025
5399
  await this.persistAgentReport(runId, stepName, report);
@@ -5171,7 +5545,7 @@ export class WorkflowRunner {
5171
5545
  console.log(chalk.dim('━'.repeat(70)));
5172
5546
  // Always show the summary table — with agent reports when available,
5173
5547
  // with just step/status/duration when not (non-interactive agents).
5174
- console.log(formatRunSummaryTable(outcomes, this.agentReports));
5548
+ console.log(formatRunSummaryTable(outcomes, this.agentReports, this.budgetTracker?.getRunSummaryBudgetData()));
5175
5549
  // Show errors and output excerpts for failed steps below the table
5176
5550
  for (const outcome of outcomes) {
5177
5551
  if (outcome.status !== 'failed')