@claude-flow/cli 3.55.0 → 3.56.1

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 (71) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/.claude/settings.json +9 -9
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/auth/client.js +40 -24
  5. package/dist/src/commands/agent.js +12 -0
  6. package/dist/src/commands/analyze.js +18 -14
  7. package/dist/src/commands/config.js +1 -1
  8. package/dist/src/commands/daemon.js +11 -19
  9. package/dist/src/commands/doctor.js +65 -58
  10. package/dist/src/commands/memory-backup.d.ts +1 -1
  11. package/dist/src/commands/memory-backup.js +23 -9
  12. package/dist/src/commands/memory.d.ts +0 -4
  13. package/dist/src/commands/memory.js +10 -2
  14. package/dist/src/commands/plugins.js +13 -1
  15. package/dist/src/commands/task.js +12 -0
  16. package/dist/src/init/settings-generator.js +3 -3
  17. package/dist/src/mcp-tools/agent-execute-core.js +244 -188
  18. package/dist/src/mcp-tools/agent-tools.js +15 -16
  19. package/dist/src/mcp-tools/agentdb-tools.js +22 -3
  20. package/dist/src/mcp-tools/claims-tools.js +3 -11
  21. package/dist/src/mcp-tools/config-tools.js +118 -13
  22. package/dist/src/mcp-tools/hooks-tools.js +47 -2
  23. package/dist/src/mcp-tools/memory-tools.js +29 -10
  24. package/dist/src/mcp-tools/record-store.d.ts +3 -0
  25. package/dist/src/mcp-tools/record-store.js +21 -0
  26. package/dist/src/mcp-tools/session-tools.js +47 -13
  27. package/dist/src/mcp-tools/task-tools.js +5 -11
  28. package/dist/src/mcp-tools/workflow-tools.js +78 -29
  29. package/dist/src/memory/append-conditions.d.ts +28 -0
  30. package/dist/src/memory/append-conditions.js +56 -0
  31. package/dist/src/memory/graph-edge-writer.d.ts +9 -0
  32. package/dist/src/memory/graph-edge-writer.js +90 -0
  33. package/dist/src/memory/memory-bridge.d.ts +4 -0
  34. package/dist/src/memory/memory-bridge.js +50 -9
  35. package/dist/src/memory/memory-initializer.d.ts +4 -0
  36. package/dist/src/memory/memory-initializer.js +100 -62
  37. package/dist/src/memory/memory-root.d.ts +2 -0
  38. package/dist/src/memory/memory-root.js +28 -0
  39. package/dist/src/memory/sona-optimizer.d.ts +2 -0
  40. package/dist/src/memory/sona-optimizer.js +4 -0
  41. package/dist/src/parser.js +7 -0
  42. package/dist/src/plugins/store/discovery.d.ts +6 -0
  43. package/dist/src/plugins/store/discovery.js +7 -3
  44. package/dist/src/ruvector/neural-router.js +7 -2
  45. package/dist/src/services/claim-service.js +39 -18
  46. package/dist/src/services/claims-store.d.ts +3 -0
  47. package/dist/src/services/claims-store.js +40 -0
  48. package/dist/src/services/config-file-manager.d.ts +2 -2
  49. package/dist/src/services/config-file-manager.js +22 -3
  50. package/dist/src/services/distill-tuning.js +17 -10
  51. package/dist/src/services/memory-backup.d.ts +7 -0
  52. package/dist/src/services/memory-backup.js +29 -2
  53. package/dist/src/services/memory-distillation.js +2 -2
  54. package/dist/src/services/policy-ledger-anchor.d.ts +8 -0
  55. package/dist/src/services/policy-ledger-anchor.js +18 -3
  56. package/dist/src/services/policy-runtime.js +2 -2
  57. package/dist/src/services/worker-daemon.d.ts +4 -2
  58. package/dist/src/services/worker-daemon.js +14 -6
  59. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  60. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
  61. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
  62. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
  63. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
  64. package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
  65. package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
  66. package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
  67. package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
  68. package/node_modules/@claude-flow/security/package.json +1 -1
  69. package/package.json +4 -4
  70. package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
  71. package/.claude/agents/tmp.json +0 -0
@@ -101,7 +101,9 @@ export async function callAnthropicMessages(input) {
101
101
  // key-presence inference, kept below as the last-resort fallback.
102
102
  const explicitProvider = (input.provider || process.env.RUFLO_PROVIDER || '').toLowerCase();
103
103
  const ollamaKey = process.env.OLLAMA_API_KEY;
104
- const anthropicKey = process.env.ANTHROPIC_API_KEY;
104
+ const anthropicAuthToken = process.env.ANTHROPIC_AUTH_TOKEN;
105
+ // A gateway bearer token is also an Anthropic credential for provider inference.
106
+ const anthropicKey = process.env.ANTHROPIC_API_KEY || anthropicAuthToken;
105
107
  // #2042 — OpenRouter is an OpenAI-compat endpoint that fronts dozens of
106
108
  // providers. Reporter (@ummcke00) had `providers.openrouter.apiKey` in
107
109
  // their config.yaml but agent_execute hardcoded Anthropic. Detect via
@@ -109,7 +111,7 @@ export async function callAnthropicMessages(input) {
109
111
  // when no Anthropic key is available (same precedence as the Ollama
110
112
  // branch above).
111
113
  const openrouterKey = process.env.OPENROUTER_API_KEY;
112
- const useOpenRouter = explicitProvider === 'openrouter' || (!anthropicKey && !!openrouterKey);
114
+ const useOpenRouter = explicitProvider === 'openrouter' || (!explicitProvider && !anthropicKey && !!openrouterKey);
113
115
  // #2962 — only consult the persisted config when a candidate is actually
114
116
  // relevant (explicit choice, or no env key found anywhere), so a normal
115
117
  // ANTHROPIC_API_KEY-only setup never pays a config-file read.
@@ -117,7 +119,7 @@ export async function callAnthropicMessages(input) {
117
119
  ? getPersistedProviderConfig('ollama')
118
120
  : undefined;
119
121
  const persistedOpenRouter = explicitProvider === 'openrouter' && !openrouterKey ? getPersistedProviderConfig('openrouter') : undefined;
120
- const useOllama = explicitProvider === 'ollama' || (!anthropicKey && !openrouterKey && (!!ollamaKey || !!persistedOllama));
122
+ const useOllama = explicitProvider === 'ollama' || (!explicitProvider && !anthropicKey && !openrouterKey && (!!ollamaKey || !!persistedOllama));
121
123
  if (useOpenRouter) {
122
124
  const apiKey = openrouterKey || persistedOpenRouter?.apiKey;
123
125
  if (apiKey) {
@@ -133,6 +135,7 @@ export async function callAnthropicMessages(input) {
133
135
  defaultModel: process.env.OPENROUTER_DEFAULT_MODEL || persistedOpenRouter?.model || 'anthropic/claude-sonnet-4-6',
134
136
  });
135
137
  }
138
+ return { success: false, error: 'OpenRouter was selected but no API key is configured.' };
136
139
  }
137
140
  if (useOllama) {
138
141
  // #2962 — retire the undocumented OLLAMA_API_KEY=local sentinel
@@ -151,22 +154,26 @@ export async function callAnthropicMessages(input) {
151
154
  model: input.model || persistedOllama?.model,
152
155
  });
153
156
  }
157
+ return { success: false, error: 'Ollama was selected but no API key or self-hosted endpoint is configured.' };
154
158
  }
155
159
  if (!anthropicKey) {
156
160
  return {
157
161
  success: false,
158
- error: 'No LLM provider configured. Set ANTHROPIC_API_KEY (Tier-3), OPENROUTER_API_KEY (#2042), or OLLAMA_API_KEY (Tier-2 — #1725).',
162
+ error: 'No LLM provider configured. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN (Tier-3), OPENROUTER_API_KEY (#2042), or OLLAMA_API_KEY (Tier-2 — #1725).',
159
163
  };
160
164
  }
161
165
  const model = input.model || DEFAULT_ANTHROPIC_MODEL;
162
166
  const startedAt = Date.now();
167
+ const controller = new AbortController();
168
+ const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
163
169
  try {
164
- const controller = new AbortController();
165
- const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
166
- const res = await fetch('https://api.anthropic.com/v1/messages', {
170
+ const baseUrl = (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com').replace(/\/+$/, '');
171
+ const res = await fetch(`${baseUrl}/v1/messages`, {
167
172
  method: 'POST',
168
173
  headers: {
169
- 'x-api-key': anthropicKey,
174
+ ...(anthropicAuthToken
175
+ ? { Authorization: `Bearer ${anthropicAuthToken}` }
176
+ : { 'x-api-key': anthropicKey }),
170
177
  'anthropic-version': '2023-06-01',
171
178
  'content-type': 'application/json',
172
179
  },
@@ -191,7 +198,6 @@ export async function callAnthropicMessages(input) {
191
198
  }),
192
199
  signal: controller.signal,
193
200
  });
194
- clearTimeout(timer);
195
201
  if (!res.ok) {
196
202
  const errText = await res.text().catch(() => '<unreadable error body>');
197
203
  return { success: false, model, error: `Anthropic API error ${res.status}: ${errText.slice(0, 400)}` };
@@ -223,6 +229,9 @@ export async function callAnthropicMessages(input) {
223
229
  durationMs: Date.now() - startedAt,
224
230
  };
225
231
  }
232
+ finally {
233
+ clearTimeout(timer);
234
+ }
226
235
  }
227
236
  /**
228
237
  * Ollama Cloud / OpenAI-compat provider — Tier-2 routing per ADR-026 + #1725.
@@ -252,9 +261,9 @@ async function callOllamaCompat(input) {
252
261
  // (the daemon binds to 11434 with no auth by default), but Ollama Cloud
253
262
  // does. Send the bearer when the key is non-empty AND looks cloud-shaped.
254
263
  const sendAuth = !!input.apiKey && input.apiKey !== 'local';
264
+ const controller = new AbortController();
265
+ const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
255
266
  try {
256
- const controller = new AbortController();
257
- const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
258
267
  const res = await fetch(url, {
259
268
  method: 'POST',
260
269
  headers: {
@@ -274,7 +283,6 @@ async function callOllamaCompat(input) {
274
283
  }),
275
284
  signal: controller.signal,
276
285
  });
277
- clearTimeout(timer);
278
286
  if (!res.ok) {
279
287
  const errText = await res.text().catch(() => '<unreadable error body>');
280
288
  return { success: false, model, error: `Ollama API error ${res.status} at ${url}: ${errText.slice(0, 400)}` };
@@ -304,6 +312,9 @@ async function callOllamaCompat(input) {
304
312
  durationMs: Date.now() - startedAt,
305
313
  };
306
314
  }
315
+ finally {
316
+ clearTimeout(timer);
317
+ }
307
318
  }
308
319
  /**
309
320
  * Generic OpenAI-compat caller for OpenRouter and other OpenAI-shaped
@@ -319,9 +330,9 @@ async function callOpenAICompat(input) {
319
330
  const startedAt = Date.now();
320
331
  const base = input.baseUrl.replace(/\/+$/, '');
321
332
  const url = `${base}/v1/chat/completions`;
333
+ const controller = new AbortController();
334
+ const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
322
335
  try {
323
- const controller = new AbortController();
324
- const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
325
336
  const messages = [];
326
337
  if (input.systemPrompt)
327
338
  messages.push({ role: 'system', content: input.systemPrompt });
@@ -344,7 +355,6 @@ async function callOpenAICompat(input) {
344
355
  }),
345
356
  signal: controller.signal,
346
357
  });
347
- clearTimeout(timer);
348
358
  if (!res.ok) {
349
359
  const errText = await res.text().catch(() => '<unreadable error body>');
350
360
  return { success: false, model, error: `${input.providerLabel} API error ${res.status}: ${errText.slice(0, 400)}` };
@@ -374,6 +384,9 @@ async function callOpenAICompat(input) {
374
384
  durationMs: Date.now() - startedAt,
375
385
  };
376
386
  }
387
+ finally {
388
+ clearTimeout(timer);
389
+ }
377
390
  }
378
391
  function resolveOpenAICompatModel(input, fallback) {
379
392
  if (!input)
@@ -418,6 +431,8 @@ export function resolveAnthropicModel(input) {
418
431
  return input.slice('anthropic:'.length);
419
432
  return input;
420
433
  }
434
+ // Tracks overlapping calls in this process; persisted state remains authoritative.
435
+ const activeExecutions = new Map();
421
436
  export async function executeAgentTask(input) {
422
437
  const store = loadAgentStore();
423
438
  const agent = store.agents[input.agentId];
@@ -465,194 +480,235 @@ export async function executeAgentTask(input) {
465
480
  `You are a ${agent.agentType} agent operating as part of a Ruflo swarm. ` +
466
481
  `Agent ID: ${input.agentId}. Domain: ${agent.domain ?? 'general'}. ` +
467
482
  `Respond directly and stay focused on the task. If you need information you don't have, state that explicitly.`;
483
+ const initialModelId = agent.modelId;
484
+ const initialModel = agent.model;
468
485
  agent.status = 'busy';
469
486
  agent.taskCount = (agent.taskCount || 0) + 1;
470
487
  saveAgentStore(store);
471
- const startedAt = Date.now();
472
- // #2042 — delegate to callAnthropicMessages so the v3 provider router
473
- // (Anthropic / Ollama / OpenRouter) governs which backend is hit.
474
- // #2962 — forward the agent's own explicit provider (set at spawn time
475
- // from --provider / persisted config) so it outranks env-var inference.
476
- // Only the first-attempt call; the ADR-149 fallback-retry call below is
477
- // driven by the cost-optimal neural router picking model-id alternatives,
478
- // an orthogonal mechanism left as-is to avoid unintended interaction.
479
- let result = await callAnthropicMessages({
480
- model: anthropicModel,
481
- prompt: input.prompt,
482
- systemPrompt,
483
- maxTokens: input.maxTokens,
484
- temperature: input.temperature,
485
- timeoutMs: input.timeoutMs,
486
- provider: agent.provider,
487
- });
488
- // ADR-149 iter 7 — fallback chain on retryable failures (429, 5xx,
489
- // timeout). When the cost-optimal neural backend picked a specific
490
- // model id and that model fails for a transient reason, fall back to
491
- // the next-cheapest candidate that clears the quality bar. Budget is
492
- // bounded by CLAUDE_FLOW_ROUTER_FALLBACK_MAX_RETRIES (default 1) so
493
- // upstream outages don't cause retry storms.
494
- const fallbackBudget = Math.max(0, parseInt(process.env.CLAUDE_FLOW_ROUTER_FALLBACK_MAX_RETRIES ?? '1', 10) || 1);
495
- const fallbackHistory = [];
496
- if (!result.success && agent.modelId && fallbackBudget > 0) {
497
- const isRetryable = /\b(429|500|502|503|504|timeout|ECONNRESET|ETIMEDOUT)\b/i.test(result.error ?? '');
498
- if (isRetryable) {
499
- try {
500
- const { nextCostOptimalAlternative } = await import('../ruvector/neural-router.js');
501
- // ADR-149 iter 9 — delegate to the shared task-embedder LRU. The
502
- // pipeline + cache are shared with agent-tools.ts, so the embedding
503
- // for this prompt is almost always already cached from the initial
504
- // routing decision (no extra inference cost in steady state).
505
- const { embedTaskWithCache } = await import('../ruvector/task-embedder.js');
506
- const embedding = await embedTaskWithCache(input.prompt);
507
- if (embedding) {
508
- const excludeIds = [agent.modelId];
509
- for (let attempt = 0; attempt < fallbackBudget && !result.success; attempt++) {
510
- fallbackHistory.push({ modelId: excludeIds[excludeIds.length - 1], error: result.error ?? '' });
511
- const alt = await nextCostOptimalAlternative(embedding, excludeIds);
512
- if (!alt || !alt.modelId)
513
- break;
514
- excludeIds.push(alt.modelId);
515
- const altResult = await callAnthropicMessages({
516
- model: alt.modelId,
517
- prompt: input.prompt,
518
- systemPrompt,
519
- maxTokens: input.maxTokens,
520
- temperature: input.temperature,
521
- timeoutMs: input.timeoutMs,
522
- });
523
- // Record the model that ACTUALLY answered (or errored). On success,
524
- // update agent.modelId so downstream observers see the retry winner.
525
- agent.modelId = alt.modelId;
526
- result = altResult;
488
+ const executionKey = JSON.stringify([getAgentPath(), input.agentId, agent.createdAt]);
489
+ activeExecutions.set(executionKey, (activeExecutions.get(executionKey) || 0) + 1);
490
+ try {
491
+ const startedAt = Date.now();
492
+ // #2042 — delegate to callAnthropicMessages so the v3 provider router
493
+ // (Anthropic / Ollama / OpenRouter) governs which backend is hit.
494
+ // #2962 — forward the agent's own explicit provider (set at spawn time
495
+ // from --provider / persisted config) so it outranks env-var inference.
496
+ // Only the first-attempt call; the ADR-149 fallback-retry call below is
497
+ // driven by the cost-optimal neural router picking model-id alternatives,
498
+ // an orthogonal mechanism left as-is to avoid unintended interaction.
499
+ let result = await callAnthropicMessages({
500
+ model: anthropicModel,
501
+ prompt: input.prompt,
502
+ systemPrompt,
503
+ maxTokens: input.maxTokens,
504
+ temperature: input.temperature,
505
+ timeoutMs: input.timeoutMs,
506
+ provider: agent.provider,
507
+ });
508
+ // ADR-149 iter 7 — fallback chain on retryable failures (429, 5xx,
509
+ // timeout). When the cost-optimal neural backend picked a specific
510
+ // model id and that model fails for a transient reason, fall back to
511
+ // the next-cheapest candidate that clears the quality bar. Budget is
512
+ // bounded by CLAUDE_FLOW_ROUTER_FALLBACK_MAX_RETRIES (default 1) so
513
+ // upstream outages don't cause retry storms.
514
+ const configuredFallbackBudget = parseInt(process.env.CLAUDE_FLOW_ROUTER_FALLBACK_MAX_RETRIES ?? '1', 10);
515
+ const fallbackBudget = Number.isNaN(configuredFallbackBudget) ? 1 : Math.max(0, configuredFallbackBudget);
516
+ const fallbackHistory = [];
517
+ // Keep feedback for each completed attempt before switching model identity.
518
+ const failedAttempts = [];
519
+ if (!result.success && agent.modelId && fallbackBudget > 0) {
520
+ const isRetryable = /\b(429|500|502|503|504|timeout|ECONNRESET|ETIMEDOUT)\b/i.test(result.error ?? '');
521
+ if (isRetryable) {
522
+ try {
523
+ const { nextCostOptimalAlternative } = await import('../ruvector/neural-router.js');
524
+ // ADR-149 iter 9 — delegate to the shared task-embedder LRU. The
525
+ // pipeline + cache are shared with agent-tools.ts, so the embedding
526
+ // for this prompt is almost always already cached from the initial
527
+ // routing decision (no extra inference cost in steady state).
528
+ const { embedTaskWithCache } = await import('../ruvector/task-embedder.js');
529
+ const embedding = await embedTaskWithCache(input.prompt);
530
+ if (embedding) {
531
+ const excludeIds = [agent.modelId];
532
+ for (let attempt = 0; attempt < fallbackBudget && !result.success; attempt++) {
533
+ fallbackHistory.push({ modelId: excludeIds[excludeIds.length - 1], error: result.error ?? '' });
534
+ const alt = await nextCostOptimalAlternative(embedding, excludeIds);
535
+ if (!alt || !alt.modelId)
536
+ break;
537
+ excludeIds.push(alt.modelId);
538
+ const altResult = await callAnthropicMessages({
539
+ model: alt.modelId,
540
+ prompt: input.prompt,
541
+ systemPrompt,
542
+ maxTokens: input.maxTokens,
543
+ temperature: input.temperature,
544
+ timeoutMs: input.timeoutMs,
545
+ });
546
+ // Record the model that ACTUALLY answered (or errored). On success,
547
+ // update agent.modelId so downstream observers see the retry winner.
548
+ failedAttempts.push({ model: agent.model ?? 'sonnet', modelId: agent.modelId });
549
+ agent.modelId = alt.modelId;
550
+ agent.model = alt.model;
551
+ result = altResult;
552
+ }
527
553
  }
528
554
  }
529
- }
530
- catch {
531
- // Fallback chain is best-effort — preserve the original error result.
555
+ catch {
556
+ // Fallback chain is best-effort — preserve the original error result.
557
+ }
532
558
  }
533
559
  }
534
- }
535
- agent.status = 'idle';
536
- // ADR-149 — close the bandit feedback loop. `recordModelOutcome` updates
537
- // the Beta(α,β) prior for the agent's tier so the Thompson sampler learns
538
- // from production traffic instead of staying frozen at install-day priors.
539
- // This is best-effort: any error here must NOT break the agent execution.
540
- // For now, "success" = the model returned a response without an API error.
541
- // A finer-grained signal (user-accepted output / regression-detected) is a
542
- // follow-up; this commit closes the bandit's basic learning loop.
543
- try {
544
- const { recordModelOutcome, recordModelOutcomeByModelId } = await import('../ruvector/model-router.js');
545
- // Bandit priors are keyed on the 3 canonical tiers (haiku/sonnet/opus/inherit);
546
- // collapse opus-4.7 → opus before recording so the bandit's per-tier Beta
547
- // updates correctly.
548
- const tier = agent.model === 'opus-4.7' ? 'opus' :
549
- agent.model ?? 'sonnet';
550
- const outcome = result.success ? 'success' : 'failure';
551
- recordModelOutcome(input.prompt, tier, outcome);
552
- // ADR-149 — also write to the shadow per-modelId priors when the cost-
553
- // optimal neural backend picked a concrete model id. Selection logic
554
- // still uses tier priors, but the per-modelId data accumulates so a
555
- // future refactor can switch the selector over.
556
- if (agent.modelId) {
557
- recordModelOutcomeByModelId(input.prompt, agent.modelId, outcome);
558
- }
559
- // ADR-149 iter 17 — close the production-data side of the feedback loop.
560
- // The trajectory recorder (CLAUDE_FLOW_ROUTER_TRAJECTORY=1) writes
561
- // per-decision rows to .swarm/model-router-trajectories.jsonl from the
562
- // model-router itself. Now we ALSO write outcome rows here so future
563
- // training (scripts/train-from-trajectories.mjs, follow-up) can pair
564
- // decision+outcome by task_hash and produce DRACO-shaped retraining
565
- // rows from real production traffic. quality = 1.0 on success, 0.0 on
566
- // failure (coarse signal; finer-grained quality from user ratings or
567
- // regression-detection is a separate hook).
568
- if (process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY === '1') {
569
- try {
570
- const { recordTrajectoryOutcome } = await import('../ruvector/router-trajectory.js');
571
- const scores = agent.modelId
572
- ? { [agent.modelId]: outcome === 'success' ? 1.0 : 0.0 }
573
- : undefined;
574
- // iter 31 — pass token usage + modelId so the recorder can compute
575
- // USD spend at write time. Consumers (`router decisions`, cost-
576
- // savings reports) get cost without their own price table.
577
- recordTrajectoryOutcome({
578
- task: input.prompt,
579
- quality: outcome === 'success' ? 1.0 : 0.0,
580
- scores,
581
- source: 'agent-execute',
582
- tokens: result.usage ? { input: result.usage.inputTokens, output: result.usage.outputTokens } : undefined,
583
- modelId: agent.modelId,
584
- });
560
+ // ADR-149 — close the bandit feedback loop. `recordModelOutcome` updates
561
+ // the Beta(α,β) prior for the agent's tier so the Thompson sampler learns
562
+ // from production traffic instead of staying frozen at install-day priors.
563
+ // This is best-effort: any error here must NOT break the agent execution.
564
+ // For now, "success" = the model returned a response without an API error.
565
+ // A finer-grained signal (user-accepted output / regression-detected) is a
566
+ // follow-up; this commit closes the bandit's basic learning loop.
567
+ try {
568
+ const { recordModelOutcome, recordModelOutcomeByModelId } = await import('../ruvector/model-router.js');
569
+ // Bandit priors are keyed on the 3 canonical tiers (haiku/sonnet/opus/inherit);
570
+ // collapse opus-4.7 → opus before recording so the bandit's per-tier Beta
571
+ // updates correctly.
572
+ const tier = agent.model === 'opus-4.7' ? 'opus' :
573
+ agent.model ?? 'sonnet';
574
+ const outcome = result.success ? 'success' : 'failure';
575
+ for (const attempt of failedAttempts) {
576
+ recordModelOutcome(input.prompt, attempt.model === 'opus-4.7' ? 'opus' : attempt.model, 'failure');
577
+ recordModelOutcomeByModelId(input.prompt, attempt.modelId, 'failure');
585
578
  }
586
- catch { /* never break execution */ }
587
- }
588
- // ADR-150 weight-eft capture seam — the ONLY place a run's full outcome is
589
- // known here: prompt (issue), assistant transcript, model, tier, and a
590
- // resolved PROXY. Gated behind CLAUDE_FLOW_RUN_TRANSCRIPTS=1 (off by
591
- // default; PII/retention surface, mirrors the router trajectory recorder).
592
- // HONESTY: `resolved` here is the WEAKEST proxy — 'api-success' means only
593
- // that the model returned without an API error, NOT that the output is
594
- // correct (ruflo has no SWE-bench gold oracle). model_patch is '' because
595
- // this single-shot execute path produces no unified diff. Both facts are
596
- // stamped so no downstream weight-eft export mistakes this for gold data.
597
- if (process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS === '1') {
598
- try {
599
- const { recordRunTranscript, tierForModel } = await import('../ruvector/run-transcript-recorder.js');
600
- const modelId = agent.modelId ?? String(agent.model ?? 'unknown');
601
- const messages = [];
602
- if (systemPrompt)
603
- messages.push({ role: 'system', content: systemPrompt });
604
- messages.push({ role: 'user', content: input.prompt });
605
- if (result.success && typeof result.output === 'string') {
606
- messages.push({ role: 'assistant', content: result.output });
579
+ recordModelOutcome(input.prompt, tier, outcome);
580
+ // ADR-149 — also write to the shadow per-modelId priors when the cost-
581
+ // optimal neural backend picked a concrete model id. Selection logic
582
+ // still uses tier priors, but the per-modelId data accumulates so a
583
+ // future refactor can switch the selector over.
584
+ if (agent.modelId) {
585
+ recordModelOutcomeByModelId(input.prompt, agent.modelId, outcome);
586
+ }
587
+ // ADR-149 iter 17 — close the production-data side of the feedback loop.
588
+ // The trajectory recorder (CLAUDE_FLOW_ROUTER_TRAJECTORY=1) writes
589
+ // per-decision rows to .swarm/model-router-trajectories.jsonl from the
590
+ // model-router itself. Now we ALSO write outcome rows here so future
591
+ // training (scripts/train-from-trajectories.mjs, follow-up) can pair
592
+ // decision+outcome by task_hash and produce DRACO-shaped retraining
593
+ // rows from real production traffic. quality = 1.0 on success, 0.0 on
594
+ // failure (coarse signal; finer-grained quality from user ratings or
595
+ // regression-detection is a separate hook).
596
+ if (process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY === '1') {
597
+ try {
598
+ const { recordTrajectoryOutcome } = await import('../ruvector/router-trajectory.js');
599
+ const scores = agent.modelId
600
+ ? { [agent.modelId]: outcome === 'success' ? 1.0 : 0.0 }
601
+ : undefined;
602
+ // iter 31 — pass token usage + modelId so the recorder can compute
603
+ // USD spend at write time. Consumers (`router decisions`, cost-
604
+ // savings reports) get cost without their own price table.
605
+ recordTrajectoryOutcome({
606
+ task: input.prompt,
607
+ quality: outcome === 'success' ? 1.0 : 0.0,
608
+ scores,
609
+ source: 'agent-execute',
610
+ tokens: result.usage ? { input: result.usage.inputTokens, output: result.usage.outputTokens } : undefined,
611
+ modelId: agent.modelId,
612
+ });
607
613
  }
608
- recordRunTranscript({
609
- task: input.prompt,
610
- model: modelId,
611
- tier: tierForModel(modelId),
612
- resolved: outcome === 'success',
613
- resolvedSource: 'api-success',
614
- messages,
615
- source: 'agent-execute',
616
- tokens: result.usage ? { input: result.usage.inputTokens, output: result.usage.outputTokens } : undefined,
617
- });
614
+ catch { /* never break execution */ }
615
+ }
616
+ // ADR-150 weight-eft capture seam — the ONLY place a run's full outcome is
617
+ // known here: prompt (issue), assistant transcript, model, tier, and a
618
+ // resolved PROXY. Gated behind CLAUDE_FLOW_RUN_TRANSCRIPTS=1 (off by
619
+ // default; PII/retention surface, mirrors the router trajectory recorder).
620
+ // HONESTY: `resolved` here is the WEAKEST proxy — 'api-success' means only
621
+ // that the model returned without an API error, NOT that the output is
622
+ // correct (ruflo has no SWE-bench gold oracle). model_patch is '' because
623
+ // this single-shot execute path produces no unified diff. Both facts are
624
+ // stamped so no downstream weight-eft export mistakes this for gold data.
625
+ if (process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS === '1') {
626
+ try {
627
+ const { recordRunTranscript, tierForModel } = await import('../ruvector/run-transcript-recorder.js');
628
+ const modelId = agent.modelId ?? String(agent.model ?? 'unknown');
629
+ const messages = [];
630
+ if (systemPrompt)
631
+ messages.push({ role: 'system', content: systemPrompt });
632
+ messages.push({ role: 'user', content: input.prompt });
633
+ if (result.success && typeof result.output === 'string') {
634
+ messages.push({ role: 'assistant', content: result.output });
635
+ }
636
+ recordRunTranscript({
637
+ task: input.prompt,
638
+ model: modelId,
639
+ tier: tierForModel(modelId),
640
+ resolved: outcome === 'success',
641
+ resolvedSource: 'api-success',
642
+ messages,
643
+ source: 'agent-execute',
644
+ tokens: result.usage ? { input: result.usage.inputTokens, output: result.usage.outputTokens } : undefined,
645
+ });
646
+ }
647
+ catch { /* never break execution */ }
618
648
  }
619
- catch { /* never break execution */ }
620
649
  }
621
- }
622
- catch {
623
- // Silent — bandit feedback must never block routing.
624
- }
625
- if (result.success) {
626
- const out = {
627
- success: true,
650
+ catch {
651
+ // Silent — bandit feedback must never block routing.
652
+ }
653
+ const remaining = (activeExecutions.get(executionKey) || 1) - 1;
654
+ const persistCompletion = (out) => {
655
+ // Awaited provider/feedback work may have overlapped registration, updates,
656
+ // termination or deletion. Never replace that newer registry snapshot.
657
+ const latest = loadAgentStore();
658
+ const current = latest.agents[input.agentId];
659
+ if (!current || current.createdAt !== agent.createdAt)
660
+ return;
661
+ if (current.status === 'busy')
662
+ current.status = remaining > 0 ? 'busy' : 'idle';
663
+ if (agent.modelId !== initialModelId && current.modelId === initialModelId) {
664
+ // A fallback may also change its tier. Carry that change with the model
665
+ // ID, but preserve any selection the user edited during the request.
666
+ if (agent.model !== initialModel && current.model === initialModel)
667
+ current.model = agent.model;
668
+ current.modelId = agent.modelId;
669
+ }
670
+ if (out)
671
+ current.lastResult = out;
672
+ saveAgentStore(latest);
673
+ };
674
+ if (result.success) {
675
+ const out = {
676
+ success: true,
677
+ agentId: input.agentId,
678
+ messageId: result.messageId,
679
+ model: result.model,
680
+ stopReason: result.stopReason,
681
+ output: result.output,
682
+ usage: result.usage,
683
+ durationMs: result.durationMs ?? Date.now() - startedAt,
684
+ ...(fallbackHistory.length > 0 ? { fallbackHistory } : {}),
685
+ };
686
+ persistCompletion(out);
687
+ return out;
688
+ }
689
+ persistCompletion();
690
+ // No-provider-configured error → surface the same actionable message
691
+ // the router built, with a #2042-aware remediation pointer.
692
+ const noProvider = (result.error || '').includes('No LLM provider configured');
693
+ return {
694
+ success: false,
628
695
  agentId: input.agentId,
629
- messageId: result.messageId,
630
- model: result.model,
631
- stopReason: result.stopReason,
632
- output: result.output,
633
- usage: result.usage,
696
+ model: anthropicModel,
697
+ error: result.error || 'agent_execute failed',
634
698
  durationMs: result.durationMs ?? Date.now() - startedAt,
635
699
  ...(fallbackHistory.length > 0 ? { fallbackHistory } : {}),
700
+ ...(noProvider && {
701
+ remediation: 'Set one of ANTHROPIC_API_KEY, OPENROUTER_API_KEY (+ optional OPENROUTER_BASE_URL), or OLLAMA_API_KEY. ' +
702
+ 'Or set RUFLO_PROVIDER=openrouter|ollama to force a specific provider.',
703
+ }),
636
704
  };
637
- agent.lastResult = out;
638
- saveAgentStore(store);
639
- return out;
640
705
  }
641
- saveAgentStore(store);
642
- // No-provider-configured error → surface the same actionable message
643
- // the router built, with a #2042-aware remediation pointer.
644
- const noProvider = (result.error || '').includes('No LLM provider configured');
645
- return {
646
- success: false,
647
- agentId: input.agentId,
648
- model: anthropicModel,
649
- error: result.error || 'agent_execute failed',
650
- durationMs: result.durationMs ?? Date.now() - startedAt,
651
- ...(fallbackHistory.length > 0 ? { fallbackHistory } : {}),
652
- ...(noProvider && {
653
- remediation: 'Set one of ANTHROPIC_API_KEY, OPENROUTER_API_KEY (+ optional OPENROUTER_BASE_URL), or OLLAMA_API_KEY. ' +
654
- 'Or set RUFLO_PROVIDER=openrouter|ollama to force a specific provider.',
655
- }),
656
- };
706
+ finally {
707
+ const pending = (activeExecutions.get(executionKey) || 1) - 1;
708
+ if (pending > 0)
709
+ activeExecutions.set(executionKey, pending);
710
+ else
711
+ activeExecutions.delete(executionKey);
712
+ }
657
713
  }
658
714
  //# sourceMappingURL=agent-execute-core.js.map