@mcp-abap-adt/llm-agent-server-libs 19.1.2 → 19.2.0

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 (107) hide show
  1. package/dist/factories/controller-factory.d.ts +18 -4
  2. package/dist/factories/controller-factory.d.ts.map +1 -1
  3. package/dist/factories/controller-factory.js +50 -10
  4. package/dist/factories/controller-factory.js.map +1 -1
  5. package/dist/generated/version.d.ts +1 -1
  6. package/dist/generated/version.js +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/pipelines/controller.d.ts.map +1 -1
  12. package/dist/pipelines/controller.js +41 -0
  13. package/dist/pipelines/controller.js.map +1 -1
  14. package/dist/pipelines/dag.d.ts.map +1 -1
  15. package/dist/pipelines/dag.js +2 -1
  16. package/dist/pipelines/dag.js.map +1 -1
  17. package/dist/pipelines/flat.d.ts.map +1 -1
  18. package/dist/pipelines/flat.js +2 -1
  19. package/dist/pipelines/flat.js.map +1 -1
  20. package/dist/pipelines/linear.d.ts.map +1 -1
  21. package/dist/pipelines/linear.js +2 -1
  22. package/dist/pipelines/linear.js.map +1 -1
  23. package/dist/pipelines/register-skill-sources.d.ts +21 -0
  24. package/dist/pipelines/register-skill-sources.d.ts.map +1 -0
  25. package/dist/pipelines/register-skill-sources.js +57 -0
  26. package/dist/pipelines/register-skill-sources.js.map +1 -0
  27. package/dist/pipelines/server-context.d.ts +37 -1
  28. package/dist/pipelines/server-context.d.ts.map +1 -1
  29. package/dist/pipelines/server-context.js.map +1 -1
  30. package/dist/smart-agent/config.d.ts.map +1 -1
  31. package/dist/smart-agent/config.js +4 -0
  32. package/dist/smart-agent/config.js.map +1 -1
  33. package/dist/smart-agent/controller/artifacts.d.ts +116 -0
  34. package/dist/smart-agent/controller/artifacts.d.ts.map +1 -0
  35. package/dist/smart-agent/controller/artifacts.js +192 -0
  36. package/dist/smart-agent/controller/artifacts.js.map +1 -0
  37. package/dist/smart-agent/controller/board.d.ts +73 -0
  38. package/dist/smart-agent/controller/board.d.ts.map +1 -0
  39. package/dist/smart-agent/controller/board.js +233 -0
  40. package/dist/smart-agent/controller/board.js.map +1 -0
  41. package/dist/smart-agent/controller/controller-coordinator-handler.d.ts +67 -7
  42. package/dist/smart-agent/controller/controller-coordinator-handler.d.ts.map +1 -1
  43. package/dist/smart-agent/controller/controller-coordinator-handler.js +923 -61
  44. package/dist/smart-agent/controller/controller-coordinator-handler.js.map +1 -1
  45. package/dist/smart-agent/controller/finalizer.d.ts +48 -0
  46. package/dist/smart-agent/controller/finalizer.d.ts.map +1 -0
  47. package/dist/smart-agent/controller/finalizer.js +126 -0
  48. package/dist/smart-agent/controller/finalizer.js.map +1 -0
  49. package/dist/smart-agent/controller/memorizer.d.ts +2 -2
  50. package/dist/smart-agent/controller/memorizer.d.ts.map +1 -1
  51. package/dist/smart-agent/controller/memorizer.js +2 -2
  52. package/dist/smart-agent/controller/memorizer.js.map +1 -1
  53. package/dist/smart-agent/controller/outcome.d.ts +28 -0
  54. package/dist/smart-agent/controller/outcome.d.ts.map +1 -0
  55. package/dist/smart-agent/controller/outcome.js +30 -0
  56. package/dist/smart-agent/controller/outcome.js.map +1 -0
  57. package/dist/smart-agent/controller/planner.d.ts +28 -5
  58. package/dist/smart-agent/controller/planner.d.ts.map +1 -1
  59. package/dist/smart-agent/controller/planner.js +111 -29
  60. package/dist/smart-agent/controller/planner.js.map +1 -1
  61. package/dist/smart-agent/controller/reviewer.d.ts +48 -0
  62. package/dist/smart-agent/controller/reviewer.d.ts.map +1 -0
  63. package/dist/smart-agent/controller/reviewer.js +116 -0
  64. package/dist/smart-agent/controller/reviewer.js.map +1 -0
  65. package/dist/smart-agent/controller/run-scope.d.ts +50 -0
  66. package/dist/smart-agent/controller/run-scope.d.ts.map +1 -0
  67. package/dist/smart-agent/controller/run-scope.js +99 -0
  68. package/dist/smart-agent/controller/run-scope.js.map +1 -0
  69. package/dist/smart-agent/controller/session-bundle.d.ts +5 -0
  70. package/dist/smart-agent/controller/session-bundle.d.ts.map +1 -1
  71. package/dist/smart-agent/controller/session-bundle.js +27 -0
  72. package/dist/smart-agent/controller/session-bundle.js.map +1 -1
  73. package/dist/smart-agent/controller/types.d.ts +117 -7
  74. package/dist/smart-agent/controller/types.d.ts.map +1 -1
  75. package/dist/smart-agent/controller/types.js +30 -1
  76. package/dist/smart-agent/controller/types.js.map +1 -1
  77. package/dist/smart-agent/embedder-knowledge-index.d.ts +28 -0
  78. package/dist/smart-agent/embedder-knowledge-index.d.ts.map +1 -0
  79. package/dist/smart-agent/embedder-knowledge-index.js +71 -0
  80. package/dist/smart-agent/embedder-knowledge-index.js.map +1 -0
  81. package/dist/smart-agent/jsonl-knowledge-backend.d.ts +26 -10
  82. package/dist/smart-agent/jsonl-knowledge-backend.d.ts.map +1 -1
  83. package/dist/smart-agent/jsonl-knowledge-backend.js +81 -9
  84. package/dist/smart-agent/jsonl-knowledge-backend.js.map +1 -1
  85. package/dist/smart-agent/pg-pool.d.ts +58 -0
  86. package/dist/smart-agent/pg-pool.d.ts.map +1 -0
  87. package/dist/smart-agent/pg-pool.js +149 -0
  88. package/dist/smart-agent/pg-pool.js.map +1 -0
  89. package/dist/smart-agent/skill-plugins-config.d.ts +97 -0
  90. package/dist/smart-agent/skill-plugins-config.d.ts.map +1 -0
  91. package/dist/smart-agent/skill-plugins-config.js +318 -0
  92. package/dist/smart-agent/skill-plugins-config.js.map +1 -0
  93. package/dist/smart-agent/skill-plugins-config.test.d.ts +2 -0
  94. package/dist/smart-agent/skill-plugins-config.test.d.ts.map +1 -0
  95. package/dist/smart-agent/skill-plugins-config.test.js.map +1 -0
  96. package/dist/smart-agent/skill-plugins-host-factory.d.ts +98 -0
  97. package/dist/smart-agent/skill-plugins-host-factory.d.ts.map +1 -0
  98. package/dist/smart-agent/skill-plugins-host-factory.js +284 -0
  99. package/dist/smart-agent/skill-plugins-host-factory.js.map +1 -0
  100. package/dist/smart-agent/skill-plugins-host-factory.test.d.ts +2 -0
  101. package/dist/smart-agent/skill-plugins-host-factory.test.d.ts.map +1 -0
  102. package/dist/smart-agent/skill-plugins-host-factory.test.js.map +1 -0
  103. package/dist/smart-agent/smart-server.d.ts +25 -0
  104. package/dist/smart-agent/smart-server.d.ts.map +1 -1
  105. package/dist/smart-agent/smart-server.js +139 -6
  106. package/dist/smart-agent/smart-server.js.map +1 -1
  107. package/package.json +9 -7
@@ -1,11 +1,16 @@
1
1
  import { externalToolCallId, } from '@mcp-abap-adt/llm-agent';
2
2
  import { summaryToUsage, } from '@mcp-abap-adt/llm-agent-libs';
3
+ import { cosine } from '../embedder-knowledge-index.js';
4
+ import { readClaims, readPlanDecisions, writePlanDecision, } from './artifacts.js';
5
+ import { BoardOverBudgetError, reconstructBoard, renderBoard, } from './board.js';
3
6
  import { writeArtifact } from './memorizer.js';
4
- import { resolveNeed } from './need-resolver.js';
7
+ import { resolveByPrecedence } from './outcome.js';
5
8
  import { makePlanner } from './planner.js';
6
9
  import { appendHint } from './prompts.js';
7
- import { hydrateBundle, persistBundle } from './session-bundle.js';
10
+ import { classifyRequest, readTerminal, writeTerminal } from './run-scope.js';
11
+ import { hydrateBundle, persistBundle, resetRun } from './session-bundle.js';
8
12
  import { establishTargetState } from './target-state.js';
13
+ import { validateRequires, } from './types.js';
9
14
  // ---------------------------------------------------------------------------
10
15
  // Debug logging — gated behind DEBUG_CONTROLLER (e.g. DEBUG_CONTROLLER=1).
11
16
  // Surfaces the steps the planner delegates and per-role/total token usage to
@@ -27,10 +32,12 @@ export function makeLogUsage(requestLogger, requestId, models) {
27
32
  if (!u)
28
33
  return;
29
34
  const model = role === 'finalizer'
30
- ? models.planner
31
- : role === 'embedding'
32
- ? 'embedder'
33
- : (models[role] ?? 'unknown');
35
+ ? (models.finalizer ?? models.planner)
36
+ : role === 'reviewer'
37
+ ? (models.reviewer ?? models.planner)
38
+ : role === 'embedding'
39
+ ? 'embedder'
40
+ : (models[role] ?? 'unknown');
34
41
  requestLogger.logLlmCall({
35
42
  component: role,
36
43
  model,
@@ -73,6 +80,11 @@ export class ControllerCoordinatorHandler {
73
80
  }
74
81
  async execute(ctx, _config, _span) {
75
82
  const deps = this.deps;
83
+ // Seams resolved once per execute(); consumed by Task 11+ (reviewer/finalizer/run-scope).
84
+ const now = deps.now ?? (() => new Date().toISOString());
85
+ const mintRunId = deps.runIdMinter ??
86
+ (() => `run-${now()}-${Math.round(Math.random() * 1e9)}`);
87
+ const terminalTtlMs = deps.terminalTtlMs ?? 24 * 60 * 60 * 1000;
76
88
  const sessionId = ctx.sessionId;
77
89
  const prompt = extractPrompt(ctx.textOrMessages);
78
90
  const rag = await deps.knowledgeRagFor(sessionId);
@@ -98,30 +110,196 @@ export class ControllerCoordinatorHandler {
98
110
  // with it rather than blindly re-running the suspended step. Set in the
99
111
  // external-tool resume branch below.
100
112
  let resumedExternal = false;
113
+ // Marks a LIVE external-tool continuation (the result is injected into the
114
+ // in-flight step's transcript and the step re-runs, bounded by toolCallCount —
115
+ // NOT charged to resumeCount). Block (A) consumes it; Task 14 SETS it from the
116
+ // artifact-first external-resume path. Until then it stays false and every
117
+ // re-run of an in-flight executing step is charged as a crash-replay (correct).
118
+ let externalContinuation = false;
119
+ // -- Classification + three-stage recovery ------------------------------
120
+ // Strict ordered classification (newRun > explicit-key strict > fingerprint of
121
+ // an in-flight active run). STAGE 1 of recovery is the terminal-store check for
122
+ // the resolved runId, run for ANY phase BEFORE consuming pending or routing by
123
+ // runPhase — so a crash between the store-first terminal write and the bundle
124
+ // flip can never re-run an already-finished run. A 'fresh' classification wipes
125
+ // all run-scoped state and mints a new runId; a 'resume' keeps everything and
126
+ // falls through to the pending/phase routing below.
127
+ const explicitKey = ctx.options?.runId;
128
+ const newRun = ctx.options?.newRun ?? false;
129
+ const keyForTerminal = explicitKey ?? bundle.runId;
130
+ const terminalExists = keyForTerminal
131
+ ? (await readTerminal(deps.backend, sessionId, keyForTerminal, now())) !==
132
+ undefined
133
+ : false;
134
+ const cls = classifyRequest({
135
+ bundle,
136
+ incomingRequest: prompt,
137
+ explicitKey,
138
+ newRun,
139
+ terminalExists,
140
+ });
141
+ if (cls.kind === 'replay') {
142
+ const out = await readTerminal(deps.backend, sessionId, cls.runId, now());
143
+ if (out) {
144
+ if (out.kind === 'success')
145
+ this.surfaceFinal(ctx, out.answer, usageNow());
146
+ else
147
+ this.surfaceFinal(ctx, `Error: ${out.error}`, usageNow());
148
+ return true;
149
+ }
150
+ // Expired between classify and read → fall through to a fresh run.
151
+ resetRun(bundle, prompt);
152
+ bundle.runId = mintRunId();
153
+ await persistBundle(deps.backend, sessionId, bundle);
154
+ }
155
+ else if (cls.kind === 'not-found') {
156
+ return this.escalate(ctx, sessionId, bundle, 'this run is no longer resumable — start a new request', usageNow());
157
+ }
158
+ else if (cls.kind === 'fresh') {
159
+ resetRun(bundle, prompt);
160
+ bundle.runId = mintRunId();
161
+ await persistBundle(deps.backend, sessionId, bundle);
162
+ }
163
+ else if (cls.kind === 'resume' && bundle.runId) {
164
+ // STAGE 1 (terminal-first, any phase): a stored terminal outcome wins over the
165
+ // persisted runPhase — adopt it and STOP, never re-run the phase.
166
+ const term = await readTerminal(deps.backend, sessionId, bundle.runId, now());
167
+ if (term) {
168
+ bundle.runState = 'terminal';
169
+ await persistBundle(deps.backend, sessionId, bundle);
170
+ if (term.kind === 'success')
171
+ this.surfaceFinal(ctx, term.answer, usageNow());
172
+ else
173
+ this.surfaceFinal(ctx, `Error: ${term.error}`, usageNow());
174
+ return true;
175
+ }
176
+ // No terminal → STAGE 2 (consume pending) / STAGE 3 (route by phase) are the
177
+ // existing pending-resume block + the main loop's block (A) below.
178
+ }
179
+ // Finalizing-phase crash recovery: a resume in runPhase 'finalizing' with NO
180
+ // terminal entry (stage-1 above already checked) means the finalizer never
181
+ // completed → re-run it (finalize() charges finalizeAttempt under
182
+ // finalizeCallInFlight, checks the cap, applies onFinalizeExhausted).
183
+ if (cls.kind === 'resume' &&
184
+ bundle.runState === 'active' &&
185
+ bundle.runPhase === 'finalizing') {
186
+ return this.finalize(ctx, sessionId, bundle, rag, prompt, logUsage, usageNow, now, terminalTtlMs);
187
+ }
101
188
  // -- Resume from a persisted pending marker -----------------------------
189
+ // Planner is constructed BEFORE the resume preamble: the artifact-first
190
+ // external-resume adopt below calls planner.commit() to keep the adaptive
191
+ // planCursor in lockstep with nextSeq. Stateless construction; the main loop
192
+ // reuses this same instance.
193
+ const planner = makePlanner(deps.config.planner ?? 'incremental', deps.planner, deps.config.subagents.planner?.hint, deps.skillsRecall);
102
194
  if (bundle.pending?.kind === 'external-tool') {
103
195
  const { extId, toolName } = bundle.pending;
104
- const result = ctx.externalResults?.get(extId);
105
- if (result === undefined) {
106
- // No result yet → re-surface the same external tool call and suspend.
107
- this.surfaceToolCall(ctx, {
108
- id: extId,
109
- name: toolName,
110
- arguments: (bundle.pending.args ?? {}),
111
- }, usageNow());
112
- return true;
196
+ const seq = bundle.inFlightStep?.seq;
197
+ const attempt = bundle.inFlightStep?.attempt;
198
+ // STAGE 1 artifact-first: did THIS attempt already commit a result (e.g.
199
+ // a crash AFTER the step finished but BEFORE the bundle flip)? Adopt it and
200
+ // skip the re-call entirely.
201
+ if (bundle.runId !== undefined &&
202
+ seq !== undefined &&
203
+ attempt !== undefined) {
204
+ const existing = await rag.list({
205
+ runId: bundle.runId,
206
+ seq,
207
+ attempt,
208
+ artifactType: 'step-result',
209
+ });
210
+ const resolved = resolveByPrecedence(existing.map((e) => ({
211
+ status: (e.metadata.status ?? 'failed'),
212
+ approved: e.content,
213
+ remainder: e.metadata.remainder ?? '',
214
+ note: e.metadata.note ?? '',
215
+ })));
216
+ if (resolved) {
217
+ bundle.pending = undefined;
218
+ bundle.runState = 'active';
219
+ // Same commit side effects as settle(), incl. planner.commit() so the
220
+ // adaptive planCursor advances with nextSeq.
221
+ const mapped = mapOutcome(resolved.status);
222
+ bundle.lastOutcome = mapped;
223
+ planner.commit?.(bundle, mapped);
224
+ recordStepControl(bundle, {
225
+ seq,
226
+ name: bundle.inFlightStep?.step.name ?? 'step',
227
+ status: resolved.status,
228
+ note: resolved.note,
229
+ remainder: resolved.remainder,
230
+ });
231
+ if (resolved.status === 'failed') {
232
+ if (bundle.inFlightStep)
233
+ bundle.inFlightStep.phase = 'awaiting-replan';
234
+ }
235
+ else {
236
+ bundle.nextSeq = (bundle.nextSeq ?? 0) + 1;
237
+ bundle.inFlightStep = undefined;
238
+ bundle.runPhase = 'planning';
239
+ }
240
+ await persistBundle(deps.backend, sessionId, bundle);
241
+ }
242
+ }
243
+ // STAGE 2 — no adopted artifact: route by the external result.
244
+ if (bundle.pending?.kind === 'external-tool') {
245
+ const result = ctx.externalResults?.get(extId);
246
+ if (result === undefined) {
247
+ // No result yet → re-surface the same external tool call and suspend.
248
+ this.surfaceToolCall(ctx, {
249
+ id: extId,
250
+ name: toolName,
251
+ arguments: (bundle.pending.args ?? {}),
252
+ }, usageNow());
253
+ return true;
254
+ }
255
+ bundle.writeOrdinal = (bundle.writeOrdinal ?? 0) + 1;
256
+ await writeArtifact(rag, {
257
+ ...meta,
258
+ artifactType: 'mcp-result',
259
+ toolName,
260
+ task: bundle.pending.position,
261
+ runId: bundle.runId,
262
+ seq: bundle.inFlightStep?.seq,
263
+ attempt: bundle.inFlightStep?.attempt,
264
+ // Stable fetch identity (tool+args) so run-scoped recall dedups
265
+ // duplicate fetches of the same object across attempts.
266
+ identityKey: extId,
267
+ writeOrdinal: bundle.writeOrdinal,
268
+ content: result,
269
+ }, ctx.options);
270
+ if (bundle.inFlightStep) {
271
+ // External CONTINUATION: inject the tool result into the durable
272
+ // transcript so the loop RE-RUNS the in-flight step (the executor
273
+ // continues from its own tool call). Bounded by toolCallCount, NOT a
274
+ // crash-replay — externalContinuation tells block (A) not to charge
275
+ // resumeCount when it re-runs the step this invocation.
276
+ bundle.inFlightStep.transcript.push({
277
+ role: 'assistant',
278
+ content: null,
279
+ tool_calls: [
280
+ {
281
+ id: extId,
282
+ type: 'function',
283
+ function: {
284
+ name: toolName,
285
+ arguments: JSON.stringify(bundle.pending.args ?? {}),
286
+ },
287
+ },
288
+ ],
289
+ }, { role: 'tool', tool_call_id: extId, content: result });
290
+ bundle.pending = undefined;
291
+ bundle.runState = 'active';
292
+ externalContinuation = true;
293
+ }
294
+ else {
295
+ // Legacy path (no inFlightStep — e.g. a seeded adaptive bundle): feed the
296
+ // result via plannerPrivate and let the planner replan.
297
+ bundle.plannerPrivate += `\n[external tool ${toolName} result] ${result}`;
298
+ bundle.pending = undefined;
299
+ resumedExternal = true;
300
+ }
301
+ await persistBundle(deps.backend, sessionId, bundle);
113
302
  }
114
- // Tool result arrived — record it and let the loop continue planning.
115
- await writeArtifact(rag, {
116
- ...meta,
117
- artifactType: 'mcp-result',
118
- toolName,
119
- task: bundle.pending.position,
120
- content: result,
121
- });
122
- bundle.plannerPrivate += `\n[external tool ${toolName} result] ${result}`;
123
- bundle.pending = undefined;
124
- resumedExternal = true;
125
303
  }
126
304
  else if (bundle.pending?.kind === 'clarify') {
127
305
  // The incoming prompt is the human's answer to the clarify question.
@@ -132,15 +310,41 @@ export class ControllerCoordinatorHandler {
132
310
  // treated as a refinement and becomes the goal verbatim.
133
311
  if (bundle.pending.position === 'goal') {
134
312
  const answer = prompt.trim();
313
+ if (answer.length === 0) {
314
+ // Empty/whitespace is not an established goal — stay suspended, re-ask
315
+ // (deterministic clarify-resume: never commit an empty goal).
316
+ this.surfaceClarify(ctx, bundle.pending.question, usageNow());
317
+ return true;
318
+ }
135
319
  const proposed = bundle.pending.proposedTarget;
136
320
  bundle.goal = proposed && isAffirmation(answer) ? proposed : answer;
321
+ bundle.runState = 'active';
322
+ bundle.runPhase = 'planning';
137
323
  }
138
324
  bundle.plannerPrivate += `\n[clarify answer] ${prompt}`;
139
325
  bundle.pending = undefined;
326
+ await persistBundle(deps.backend, sessionId, bundle);
140
327
  }
141
328
  // -- Establish the goal (evaluator) -------------------------------------
142
329
  if (!bundle.goal) {
330
+ // Evaluator crash-guard: a prior crash mid-call left evalCallInFlight set →
331
+ // charge evalResumeCount; exhausting maxEvalResumes is a TERMINAL abort
332
+ // (store-first), NOT an escalate — a durable resume budget, like the planner.
333
+ if (bundle.evalCallInFlight) {
334
+ bundle.evalResumeCount = (bundle.evalResumeCount ?? 0) + 1;
335
+ if (bundle.evalResumeCount > (deps.config.budgets.maxEvalResumes ?? 3)) {
336
+ await this.abortTerminal(ctx, sessionId, bundle, 'evaluator resume budget exhausted', now, terminalTtlMs, usageNow());
337
+ return true;
338
+ }
339
+ }
340
+ bundle.evalCallInFlight = true;
341
+ bundle.runPhase = 'evaluating';
342
+ await persistBundle(deps.backend, sessionId, bundle);
143
343
  const outcome = await establishTargetState({ evaluator: deps.evaluator, embedder: deps.embedder }, prompt, deps.config.targetState, ctx.options, deps.config.subagents.evaluator?.hint);
344
+ // The call completed (a malformed/needs-confirmation result is still a
345
+ // completed call) → clear the in-flight marker + reset the resume counter.
346
+ bundle.evalCallInFlight = false;
347
+ bundle.evalResumeCount = 0;
144
348
  logUsage('evaluator', outcome.usage);
145
349
  if (outcome.kind === 'needs-confirmation') {
146
350
  // Persist the proposed target with the pending marker so a confirmation
@@ -151,12 +355,15 @@ export class ControllerCoordinatorHandler {
151
355
  position: 'goal',
152
356
  proposedTarget: outcome.proposedTarget,
153
357
  };
358
+ bundle.runState = 'suspended';
154
359
  await persistBundle(deps.backend, sessionId, bundle);
155
360
  this.surfaceClarify(ctx, outcome.question, usageNow());
156
361
  return true;
157
362
  }
158
363
  bundle.goal = outcome.goal;
159
364
  }
365
+ // (runId is guaranteed by the classification preamble: a fresh/expired-replay
366
+ // run mints one, a resume already has one — so no separate mint guard here.)
160
367
  // -- Main loop ----------------------------------------------------------
161
368
  // The planner plans by INTENT — it is NOT shown a tool catalog. A prompt-level
162
369
  // catalog (selected once from goal+prompt) was too coarse: it mis-surfaced
@@ -166,14 +373,111 @@ export class ControllerCoordinatorHandler {
166
373
  // runs (see runStep → selectTools). The agnostic planner prompt already tells
167
374
  // it to plan fetch steps ("the executor picks the exact one").
168
375
  const cfg = deps.config.budgets;
376
+ const boardBudget = {
377
+ maxDigestChars: cfg.maxDigestChars ?? 500,
378
+ maxIntentChars: cfg.maxIntentChars ?? 120,
379
+ maxActiveSteps: cfg.maxActiveSteps ?? 16,
380
+ maxBoardChars: cfg.maxBoardChars ?? 12000,
381
+ keepRecentDigests: cfg.keepRecentDigests ?? 8,
382
+ };
169
383
  let planParseRetries = 0;
170
- const planner = makePlanner(deps.config.planner ?? 'incremental', deps.planner, deps.config.subagents.planner?.hint);
171
384
  // bundle.lastOutcome is the SINGLE source of truth for the last step's
172
385
  // outcome — durable, so a resume after a FAILED step replans instead of
173
386
  // repeating it. runStep.settle() sets it; the adaptive replan branch clears it
174
387
  // once the failure has been consumed into a new plan (so a crash after the
175
388
  // replan, or a finalizer retry after an empty replan, does NOT replan again).
176
389
  while (bundle.budgets.stepsUsed < cfg.maxSteps) {
390
+ const inf = bundle.inFlightStep;
391
+ if (inf && inf.phase === 'executing' && !resumedExternal) {
392
+ // Reconcile by THIS attempt's resolved artifact first.
393
+ const committed = await rag.list({
394
+ runId: bundle.runId,
395
+ seq: inf.seq,
396
+ attempt: inf.attempt,
397
+ artifactType: 'step-result',
398
+ });
399
+ const resolved = resolveByPrecedence(committed.map((e) => ({
400
+ status: (e.metadata.status ?? 'failed'),
401
+ approved: e.content,
402
+ remainder: e.metadata.remainder ?? '',
403
+ note: e.metadata.note ?? '',
404
+ })));
405
+ if (resolved) {
406
+ // Already committed → adopt, do NOT re-run. Same commit side effects as
407
+ // settle(), including planner.commit() so the adaptive planCursor advances
408
+ // in lockstep with nextSeq.
409
+ const mapped = mapOutcome(resolved.status);
410
+ bundle.lastOutcome = mapped;
411
+ planner.commit?.(bundle, mapped);
412
+ recordStepControl(bundle, {
413
+ seq: inf.seq,
414
+ name: inf.step.name,
415
+ status: resolved.status,
416
+ note: resolved.note,
417
+ remainder: resolved.remainder,
418
+ });
419
+ if (resolved.status === 'failed') {
420
+ inf.phase = 'awaiting-replan';
421
+ }
422
+ else {
423
+ bundle.nextSeq = inf.seq + 1;
424
+ bundle.inFlightStep = undefined;
425
+ bundle.runPhase = 'planning';
426
+ }
427
+ await persistBundle(deps.backend, sessionId, bundle);
428
+ continue;
429
+ }
430
+ // No artifact for this attempt → re-run the SAME step directly. Distinguish a
431
+ // live external CONTINUATION (bounded by toolCallCount) from a crash-replay
432
+ // (charged to resumeCount).
433
+ if (externalContinuation) {
434
+ externalContinuation = false;
435
+ }
436
+ else {
437
+ inf.resumeCount += 1;
438
+ if (inf.resumeCount > (cfg.maxStepResumes ?? 3)) {
439
+ await this.abortTerminal(ctx, sessionId, bundle, `step "${inf.step.name}" exceeded maxStepResumes`, now, terminalTtlMs, usageNow());
440
+ return true;
441
+ }
442
+ }
443
+ await persistBundle(deps.backend, sessionId, bundle);
444
+ // COORDINATOR OVERRIDE — use the ACTUAL runStep param order (now/terminalTtlMs
445
+ // BEFORE logUsage). The plan example shows them last; that is WRONG.
446
+ const completed = await this.runStep(ctx, sessionId, bundle, rag, meta, inf.step, isExternalTool, logUsage, usageNow, (o) => planner.commit?.(bundle, o));
447
+ if (completed === 'suspended' || completed === 'aborted')
448
+ return true;
449
+ continue;
450
+ }
451
+ // Planner crash-guard: a prior crash mid-call left plannerCallInFlight set →
452
+ // charge plannerResumeCount; exhausting maxPlannerResumes is a TERMINAL abort
453
+ // (store-first). The adaptive replan runs through planner.next too, so this one
454
+ // guard covers the awaiting-replan replan with no separate site.
455
+ if (bundle.plannerCallInFlight) {
456
+ bundle.plannerResumeCount = (bundle.plannerResumeCount ?? 0) + 1;
457
+ if (bundle.plannerResumeCount > (cfg.maxPlannerResumes ?? 3)) {
458
+ await this.abortTerminal(ctx, sessionId, bundle, 'planner resume budget exhausted', now, terminalTtlMs, usageNow());
459
+ return true;
460
+ }
461
+ }
462
+ bundle.plannerCallInFlight = true;
463
+ // Reaching the planner guard means we are about to plan (block (A) handles
464
+ // any in-flight executing step earlier and continues), so the phase is
465
+ // 'planning' regardless of the prior 'evaluating'/'executing' value.
466
+ bundle.runPhase = 'planning';
467
+ await persistBundle(deps.backend, sessionId, bundle);
468
+ // (B) Render the live board BEFORE the planner call (fail-loud on over-budget).
469
+ let boardText;
470
+ try {
471
+ boardText = await renderLiveBoard(rag, bundle, boardBudget);
472
+ }
473
+ catch (err) {
474
+ if (err instanceof BoardOverBudgetError) {
475
+ bundle.plannerPrivate += `\n[board over budget] ${err.message}`;
476
+ await this.abortTerminal(ctx, sessionId, bundle, `board exceeds maxBoardChars: ${err.message}`, now, terminalTtlMs, usageNow());
477
+ return true;
478
+ }
479
+ throw err;
480
+ }
177
481
  const next = await planner.next({
178
482
  bundle,
179
483
  prompt,
@@ -181,7 +485,23 @@ export class ControllerCoordinatorHandler {
181
485
  resumedExternal,
182
486
  retrying: planParseRetries > 0,
183
487
  logUsage,
488
+ // Same request CallOptions the handler threads into every other LLM/RAG
489
+ // call (subagents, knowledgeRagFor, target-state) so the skills-recall
490
+ // embedding is metered, cancellable, and joins the request trace.
491
+ options: ctx.options,
492
+ boardText,
184
493
  });
494
+ // (A) Drain + persist plan decisions the planner queued during next().
495
+ const drained = bundle.pendingPlanDecisions ?? [];
496
+ bundle.pendingPlanDecisions = [];
497
+ for (const decision of drained) {
498
+ bundle.writeOrdinal = (bundle.writeOrdinal ?? 0) + 1;
499
+ await writePlanDecision(deps.backend, sessionId, decision, JSON.stringify(decision.steps), now(), bundle.writeOrdinal);
500
+ }
501
+ // The call completed → clear the in-flight marker + reset the resume counter
502
+ // (a malformed reply is still a completed call; parse-retry is handled below).
503
+ bundle.plannerCallInFlight = false;
504
+ bundle.plannerResumeCount = 0;
185
505
  // NB: do NOT reset resumedExternal here — if this replan reply was malformed
186
506
  // (next === null), the parse-retry below must keep replanning. It is reset
187
507
  // only after a VALID decision (beside planParseRetries = 0;).
@@ -201,10 +521,9 @@ export class ControllerCoordinatorHandler {
201
521
  planParseRetries = 0;
202
522
  resumedExternal = false; // a valid decision consumed any external-resume replan
203
523
  if (next.kind === 'done') {
204
- bundle.pending = undefined;
205
- await persistBundle(deps.backend, sessionId, bundle);
206
- this.surfaceFinal(ctx, next.result, usageNow());
207
- return true;
524
+ // Pass next.result as the legacy answer: used only when no finalizer is
525
+ // injected (3-role config) — the adaptive planner already composed it.
526
+ return this.finalize(ctx, sessionId, bundle, rag, prompt, logUsage, usageNow, now, terminalTtlMs, next.result);
208
527
  }
209
528
  if (next.kind === 'rewind') {
210
529
  bundle.budgets.rewindsUsed++;
@@ -216,10 +535,33 @@ export class ControllerCoordinatorHandler {
216
535
  await persistBundle(deps.backend, sessionId, bundle);
217
536
  continue;
218
537
  }
219
- // next.kind === 'next' → execute the step.
538
+ // next.kind === 'next' → open a fresh attempt and run it. Crash-replay/
539
+ // continuation of an executing step is handled by block (A), so this site only
540
+ // opens a NEW seq (attempt 0) or a revised step after awaiting-replan (attempt+1).
220
541
  dlog(`delegate step "${next.step.name}"${next.step.type ? ` (${next.step.type})` : ''}: ${next.step.instructions}`);
542
+ const seq = bundle.nextSeq ?? 0;
543
+ // Usually phase 'awaiting-replan' (a revised step after a failed attempt);
544
+ // on an external resume it may still be 'executing' (block (A) was skipped
545
+ // while resumedExternal). Same-seq → attempt+1 either way.
546
+ const prev = bundle.inFlightStep;
547
+ const attempt = prev && prev.seq === seq ? prev.attempt + 1 : 0;
548
+ if (attempt >= (cfg.maxStepAttempts ?? 5)) {
549
+ await this.abortTerminal(ctx, sessionId, bundle, `step "${next.step.name}" exceeded maxStepAttempts`, now, terminalTtlMs, usageNow());
550
+ return true;
551
+ }
552
+ bundle.inFlightStep = {
553
+ seq,
554
+ step: next.step,
555
+ attempt,
556
+ resumeCount: 0,
557
+ phase: 'executing',
558
+ transcript: [],
559
+ toolCallCount: 0,
560
+ };
561
+ bundle.runPhase = 'executing';
562
+ await persistBundle(deps.backend, sessionId, bundle);
221
563
  const completed = await this.runStep(ctx, sessionId, bundle, rag, meta, next.step, isExternalTool, logUsage, usageNow, (o) => planner.commit?.(bundle, o));
222
- if (completed === 'suspended')
564
+ if (completed === 'suspended' || completed === 'aborted')
223
565
  return true;
224
566
  // runStep.settle() already persisted the outcome ATOMICALLY (bundle.lastOutcome
225
567
  // + cursor advance via onCommit + step result, in one persistBundle). The next
@@ -231,14 +573,17 @@ export class ControllerCoordinatorHandler {
231
573
  }
232
574
  // -- Step execution -----------------------------------------------------
233
575
  /** Returns 'advanced' (step succeeded — continue loop), 'failed' (retries/
234
- * tool-call budget exhausted; the failure note is in plannerPrivate so the
235
- * planner can replan), or 'suspended' (external round-trip surfaced — caller
236
- * must return true). */
576
+ * tool-call budget OR reviewer-unverifiable budget exhausted; the failure note
577
+ * is in plannerPrivate so the planner can replan), 'partial' (reviewer approved
578
+ * part, remainder replans), or 'suspended' (external round-trip surfaced — caller
579
+ * must return true). ('aborted' remains in the return union for the caller's
580
+ * guard but is no longer produced here — a judge-failure now degrades to 'failed'
581
+ * rather than aborting the run.) */
237
582
  async runStep(ctx, sessionId, bundle, rag, meta, step, isExternalTool, logUsage, usageNow, onCommit) {
238
583
  const deps = this.deps;
239
584
  const cfg = deps.config.budgets;
240
585
  const maxToolCalls = cfg.maxToolCalls ?? 10;
241
- let toolCalls = 0;
586
+ const inFlight = bundle.inFlightStep; // set by the caller (block A or B)
242
587
  // Persist the step outcome ATOMICALLY: record lastOutcome (durable, so a
243
588
  // resume after a failed step replans instead of repeating it) AND advance the
244
589
  // planner cursor (onCommit) in the SAME persistBundle that records the step
@@ -246,6 +591,18 @@ export class ControllerCoordinatorHandler {
246
591
  const settle = async (outcome) => {
247
592
  bundle.lastOutcome = outcome;
248
593
  onCommit?.(outcome);
594
+ if (outcome === 'advanced' || outcome === 'partial') {
595
+ bundle.nextSeq = (bundle.nextSeq ?? 0) + 1;
596
+ bundle.inFlightStep = undefined;
597
+ bundle.runPhase = 'planning';
598
+ }
599
+ else {
600
+ // 'failed' — keep the same seq, mark awaiting-replan in the SAME persist so
601
+ // recovery routes by durable phase.
602
+ if (bundle.inFlightStep)
603
+ bundle.inFlightStep.phase = 'awaiting-replan';
604
+ bundle.runPhase = 'executing';
605
+ }
249
606
  await persistBundle(deps.backend, sessionId, bundle);
250
607
  return outcome;
251
608
  };
@@ -264,13 +621,61 @@ export class ControllerCoordinatorHandler {
264
621
  // the bundle backend, so restrict to artifact types (excludes the
265
622
  // 'controller-bundle' infrastructure record). Bounded by k and length.
266
623
  const recallText = step.instructions || step.name;
267
- const recalled = await resolveNeed(rag, recallText, RECALL_K, {
268
- artifactType: RECALL_ARTIFACT_TYPES,
269
- });
270
- const recallBlock = buildRecallBlock(recalled);
624
+ const maxAttempts = cfg.maxStepAttempts ?? 5;
625
+ const maxTool = cfg.maxToolCalls ?? 10;
626
+ // Per-kind run-scoped recall with GUARANTEED over-fetch bounds: step-result
627
+ // retries are bounded by maxStepAttempts (k×(maxStepAttempts+1)); mcp-results
628
+ // are NOT deduped on re-fetch here and toolCallCount RESETS per attempt, so one
629
+ // run can emit up to maxSteps × maxStepAttempts × maxToolCalls of them — over-
630
+ // fetch that full run bound so every distinct identityKey is seen before the cap.
631
+ const mcpBound = cfg.maxSteps * maxAttempts * maxTool;
632
+ const recalledSteps = await runScopedRecall(rag, recallText, RECALL_K_STEP, bundle.runId, RECALL_K_STEP * (maxAttempts + 1), ['step-result'], ctx.options);
633
+ const recalledMcp = await runScopedRecall(rag, recallText, RECALL_K_MCP, bundle.runId, mcpBound, ['mcp-result'], ctx.options);
634
+ // SEPARATE character budgets per kind: a single huge step-result cannot consume
635
+ // the whole budget and starve the MCP context (and vice-versa).
636
+ const stepBlock = buildRecallBlock(recalledSteps, RECALL_MAX_CHARS_STEP);
637
+ const mcpBlock = buildRecallBlock(recalledMcp, RECALL_MAX_CHARS_MCP);
638
+ const recallBlock = [stepBlock, mcpBlock].filter(Boolean).join('\n\n');
271
639
  if (recallBlock) {
272
640
  messages.push({ role: 'user', content: recallBlock });
273
641
  }
642
+ // Durable transcript = static prefix (system/user/recall) + the dynamic
643
+ // executor/tool turns. On a resume/continuation the dynamic tail is rebuilt
644
+ // from inFlightStep.transcript so the executor sees the FULL exchange it had
645
+ // (prior tool rounds + the injected external result), not just a fragment.
646
+ const staticLen = messages.length;
647
+ if (inFlight && inFlight.transcript.length > 0) {
648
+ messages.push(...inFlight.transcript);
649
+ }
650
+ // Persist the dynamic tail after every executor/tool exchange so a suspend or
651
+ // crash never rebuilds with a shorter conversation than the executor saw.
652
+ const syncTranscript = async () => {
653
+ if (inFlight) {
654
+ inFlight.transcript = messages.slice(staticLen);
655
+ await persistBundle(deps.backend, sessionId, bundle);
656
+ }
657
+ };
658
+ // Per-reference evidence: one recall per requires[] reference. A non-empty
659
+ // top-K does NOT prove the dependency is present — semantic recall returns the
660
+ // NEAREST artifact even at low relevance — so we hand the reviewer the TOP
661
+ // artifact's relevant fragment (Evidence.topArtifact) and let IT (the judging
662
+ // role) decide whether the ref is actually satisfied. `hit` is a coarse
663
+ // any-candidate flag. Gathered SEQUENTIALLY (NOT Promise.all): each
664
+ // relevantExtract is itself bounded-sequential, so the outer sequential loop
665
+ // keeps at most ONE embed request in flight at a time (rate-limit-safe).
666
+ const refs = step.requires && step.requires.length > 0 ? step.requires : [recallText];
667
+ const evBound = RECALL_K_STEP * (maxAttempts + 1) +
668
+ cfg.maxSteps * maxAttempts * (cfg.maxToolCalls ?? 10);
669
+ const evidence = [];
670
+ for (const ref of refs) {
671
+ const hits = await runScopedRecall(rag, ref, 1, bundle.runId, evBound, RECALL_ARTIFACT_TYPES, ctx.options);
672
+ const topArtifact = hits[0]
673
+ ? await relevantExtract(hits[0].content, ref, RECALL_EVIDENCE_CHARS,
674
+ // biome-ignore lint/style/noNonNullAssertion: distance strategies require an embedder; the factory enforces it (Task 17).
675
+ deps.embedder, ctx.options)
676
+ : undefined;
677
+ evidence.push({ ref, hit: hits.length > 0, topArtifact });
678
+ }
274
679
  // Tools offered to the executor = the INTERNAL (MCP) tools semantically
275
680
  // relevant to THIS step (top-K from toolsRag) PLUS the per-request external
276
681
  // (consumer-supplied) tools. The executor decides which to call; internal
@@ -283,21 +688,105 @@ export class ControllerCoordinatorHandler {
283
688
  // so the semantic exposure boundary actually bounds what runs.
284
689
  const offeredInternalNames = new Set(relevant.map((t) => t.name));
285
690
  let retries = 0;
691
+ // (D) Persist a 'failed' step-result artifact for controller-level failures
692
+ // (reviewer unverifiable, executor error exhausted, maxToolCalls, unavailable
693
+ // tool) so the board can project the step's terminal state from artifacts alone.
694
+ const writeControlFailure = async (reason) => {
695
+ const seq = bundle.inFlightStep?.seq ?? bundle.nextSeq ?? 0;
696
+ const attempt = bundle.inFlightStep?.attempt ?? 0;
697
+ bundle.writeOrdinal = (bundle.writeOrdinal ?? 0) + 1;
698
+ await writeArtifact(rag, {
699
+ ...meta,
700
+ artifactType: 'step-result',
701
+ task: step.name,
702
+ runId: bundle.runId,
703
+ seq,
704
+ attempt,
705
+ status: 'failed',
706
+ note: reason,
707
+ remainder: '',
708
+ stepId: step.stepId,
709
+ digest: reason.slice(0, cfg.maxDigestChars ?? 500),
710
+ writeOrdinal: bundle.writeOrdinal,
711
+ content: '',
712
+ }, ctx.options);
713
+ };
286
714
  // Inner loop handles tool routing / error retries until the executor
287
715
  // produces content for this step (or the step suspends on an external tool).
288
716
  while (true) {
289
717
  const res = await deps.executor.send(messages, offeredTools);
290
718
  logUsage?.('executor', res.usage);
291
719
  if (res.kind === 'content') {
720
+ // Hold the executor's result; the reviewer (NOT the executor) decides the
721
+ // outcome. Default reviewer (no deps.reviewer) approves as 'ok' (legacy).
722
+ let review = deps.reviewer
723
+ ? await deps.reviewer.review(step, evidence, res.content, {
724
+ hint: deps.config.subagents.reviewer?.hint,
725
+ logUsage,
726
+ maxDigestChars: cfg.maxDigestChars ?? 500,
727
+ })
728
+ : {
729
+ kind: 'outcome',
730
+ outcome: {
731
+ status: 'ok',
732
+ approved: res.content,
733
+ remainder: '',
734
+ note: '',
735
+ digest: res.content.slice(0, cfg.maxDigestChars ?? 500),
736
+ },
737
+ };
738
+ // Judge failure (provider error / malformed / contradictory ok-with-empty)
739
+ // is NOT a step failure: re-ask within maxReviewRetries, then ABORT (the
740
+ // outcome is unverifiable). Never mapped to settle('failed')/replan.
741
+ let reviewRetries = 0;
742
+ while (review.kind === 'judge-failure') {
743
+ reviewRetries++;
744
+ if (reviewRetries > (cfg.maxReviewRetries ?? 2)) {
745
+ // The reviewer could not produce a usable verdict within the retry
746
+ // budget (provider error / unparsable). DEGRADE to a failed step so the
747
+ // planner replans, rather than aborting the whole run — the terminal
748
+ // backstop is maxStepAttempts/maxSteps, not a single unverifiable verdict.
749
+ bundle.budgets.stepsUsed++;
750
+ await writeControlFailure(`reviewer unverifiable after ${cfg.maxReviewRetries ?? 2} retries: ${review.reason}`);
751
+ bundle.plannerPrivate += `\n[seq ${bundle.inFlightStep?.seq ?? bundle.nextSeq ?? 0} ${step.name} failed] reviewer unverifiable after ${cfg.maxReviewRetries ?? 2} retries: ${review.reason}`;
752
+ return settle('failed');
753
+ }
754
+ review = await deps.reviewer.review(step, evidence, res.content, {
755
+ hint: deps.config.subagents.reviewer?.hint,
756
+ logUsage,
757
+ maxDigestChars: cfg.maxDigestChars ?? 500,
758
+ });
759
+ }
760
+ const outcome = review.outcome;
761
+ const seq = bundle.inFlightStep?.seq ?? bundle.nextSeq ?? 0;
762
+ const attempt = bundle.inFlightStep?.attempt ?? 0;
763
+ // ONE post-review write carrying the COMPLETE Outcome + identity.
764
+ bundle.writeOrdinal = (bundle.writeOrdinal ?? 0) + 1;
292
765
  await writeArtifact(rag, {
293
766
  ...meta,
294
767
  artifactType: 'step-result',
295
768
  task: step.name,
296
- content: res.content,
297
- });
769
+ runId: bundle.runId,
770
+ seq,
771
+ attempt,
772
+ status: outcome.status,
773
+ note: outcome.note,
774
+ remainder: outcome.remainder,
775
+ stepId: step.stepId,
776
+ digest: outcome.digest,
777
+ writeOrdinal: bundle.writeOrdinal,
778
+ content: outcome.approved,
779
+ }, ctx.options);
298
780
  bundle.budgets.stepsUsed++;
299
- bundle.plannerPrivate += `\n[step ${step.name}] ${res.content}`;
300
- return settle('advanced');
781
+ const mapped = mapOutcome(outcome.status);
782
+ recordStepControl(bundle, {
783
+ seq: bundle.inFlightStep?.seq ?? seq,
784
+ name: step.name,
785
+ status: outcome.status,
786
+ note: outcome.note,
787
+ remainder: outcome.remainder,
788
+ });
789
+ return settle(mapped);
301
790
  }
302
791
  if (res.kind === 'error') {
303
792
  retries++;
@@ -306,11 +795,13 @@ export class ControllerCoordinatorHandler {
306
795
  role: 'user',
307
796
  content: `The previous attempt failed: ${res.error}. Retry the step.`,
308
797
  });
798
+ await syncTranscript();
309
799
  continue;
310
800
  }
311
801
  // Retries exhausted — feed the error back as the step result so the
312
802
  // planner can replan on the next iteration.
313
803
  bundle.budgets.stepsUsed++;
804
+ await writeControlFailure(`executor error: ${res.error}`);
314
805
  bundle.plannerPrivate += `\n[step ${step.name} failed] ${res.error}`;
315
806
  return settle('failed');
316
807
  }
@@ -324,9 +815,11 @@ export class ControllerCoordinatorHandler {
324
815
  role: 'user',
325
816
  content: 'The previous attempt produced an empty tool call. Retry the step.',
326
817
  });
818
+ await syncTranscript();
327
819
  continue;
328
820
  }
329
821
  bundle.budgets.stepsUsed++;
822
+ await writeControlFailure('empty tool call');
330
823
  bundle.plannerPrivate += `\n[step ${step.name} failed] empty tool call`;
331
824
  return settle('failed');
332
825
  }
@@ -334,7 +827,27 @@ export class ControllerCoordinatorHandler {
334
827
  const name = call.name;
335
828
  const args = call.arguments;
336
829
  if (isExternalTool(name)) {
830
+ // External round-trips share the SAME durable toolCallCount/maxToolCalls
831
+ // bound as internal calls; check BEFORE surfacing so an external tool
832
+ // cannot exceed the cap. Exhausted → control-failed replan at the same seq.
833
+ if (inFlight && inFlight.toolCallCount + 1 > maxToolCalls) {
834
+ bundle.budgets.stepsUsed++;
835
+ await writeControlFailure('tool-call budget exhausted (maxToolCalls)');
836
+ bundle.plannerPrivate += `\n[seq ${inFlight.seq} ${step.name} control-failed] tool-call budget exhausted (maxToolCalls)`;
837
+ inFlight.phase = 'awaiting-replan';
838
+ inFlight.controlFailure = {
839
+ reason: 'maxToolCalls',
840
+ seq: inFlight.seq,
841
+ };
842
+ return settle('failed');
843
+ }
844
+ // Sync the executor turns SO FAR into the durable transcript before we
845
+ // suspend (the resume injection appends the external assistant/tool pair).
846
+ await syncTranscript();
337
847
  const extId = externalToolCallId(name, args);
848
+ if (inFlight)
849
+ inFlight.toolCallCount += 1;
850
+ // The new marker REPLACES any prior pending (a fresh extId).
338
851
  bundle.pending = {
339
852
  kind: 'external-tool',
340
853
  extId,
@@ -342,6 +855,7 @@ export class ControllerCoordinatorHandler {
342
855
  args,
343
856
  position: step.name,
344
857
  };
858
+ bundle.runState = 'suspended';
345
859
  await persistBundle(deps.backend, sessionId, bundle);
346
860
  this.surfaceToolCall(ctx, { id: extId, name, arguments: args }, usageNow?.());
347
861
  return 'suspended';
@@ -357,29 +871,50 @@ export class ControllerCoordinatorHandler {
357
871
  role: 'user',
358
872
  content: `Tool "${name}" is not available for this step. Use only the tools provided to you.`,
359
873
  });
874
+ await syncTranscript();
360
875
  continue;
361
876
  }
362
877
  bundle.budgets.stepsUsed++;
878
+ await writeControlFailure(`requested unavailable tool ${name}`);
363
879
  bundle.plannerPrivate += `\n[step ${step.name} failed] requested unavailable tool ${name}`;
364
880
  return settle('failed');
365
881
  }
366
- // Internal MCP tool bound the inner loop so a stuck executor cannot
367
- // spin forever issuing unbounded callMcp iterations.
368
- toolCalls++;
369
- if (toolCalls > maxToolCalls) {
882
+ // Durable round-trip count: ++ and persist BEFORE surfacing so it survives a
883
+ // resume (never a per-resume local).
884
+ if (inFlight) {
885
+ inFlight.toolCallCount += 1;
886
+ await persistBundle(deps.backend, sessionId, bundle);
887
+ }
888
+ if ((inFlight?.toolCallCount ?? 0) > maxToolCalls) {
889
+ // Controller-level failure (NOT a reviewer status): record durably and replan.
370
890
  bundle.budgets.stepsUsed++;
371
- bundle.plannerPrivate += `\n[step ${step.name} aborted] tool-call budget exhausted`;
891
+ await writeControlFailure('tool-call budget exhausted (maxToolCalls)');
892
+ bundle.plannerPrivate += `\n[seq ${inFlight?.seq ?? bundle.nextSeq ?? 0} ${step.name} control-failed] tool-call budget exhausted (maxToolCalls)`;
893
+ if (inFlight) {
894
+ inFlight.phase = 'awaiting-replan';
895
+ inFlight.controlFailure = {
896
+ reason: 'maxToolCalls',
897
+ seq: inFlight.seq,
898
+ };
899
+ }
372
900
  return settle('failed');
373
901
  }
374
902
  // Execute locally, memorize, re-send to the executor.
375
903
  const result = await deps.callMcp(name, args);
904
+ bundle.writeOrdinal = (bundle.writeOrdinal ?? 0) + 1;
376
905
  await writeArtifact(rag, {
377
906
  ...meta,
378
907
  artifactType: 'mcp-result',
379
908
  toolName: name,
380
909
  task: step.name,
910
+ runId: bundle.runId,
911
+ seq: inFlight?.seq,
912
+ attempt: inFlight?.attempt,
913
+ // Stable fetch identity (tool+args) for run-scoped recall dedup.
914
+ identityKey: externalToolCallId(name, args),
915
+ writeOrdinal: bundle.writeOrdinal,
381
916
  content: result,
382
- });
917
+ }, ctx.options);
383
918
  // Feed the result back as a coherent assistant→tool turn (OpenAI protocol)
384
919
  // so the executor LLM continues from its own tool call rather than seeing a
385
920
  // bare user message. The assistant message carries the tool_call it made;
@@ -400,6 +935,8 @@ export class ControllerCoordinatorHandler {
400
935
  tool_call_id: call.id,
401
936
  content: result,
402
937
  });
938
+ // The executor saw these turns → make them durable before the next round.
939
+ await syncTranscript();
403
940
  }
404
941
  }
405
942
  // -- Escalation & surfacing (mirror StepperCoordinatorHandler) ----------
@@ -409,6 +946,110 @@ export class ControllerCoordinatorHandler {
409
946
  this.surfaceClarify(ctx, question, usage);
410
947
  return true;
411
948
  }
949
+ /** Store-first terminal ERROR: write the terminal outcome to the TTL store
950
+ * FIRST (keyed by runId), THEN flip the bundle terminal and surface the error.
951
+ * Store-first makes the abort idempotent across a crash between the two writes. */
952
+ async abortTerminal(ctx, sessionId, bundle, error, now, terminalTtlMs, usage) {
953
+ await writeTerminal(this.deps.backend, sessionId, bundle.runId ?? sessionId, { kind: 'error', error }, terminalTtlMs, now());
954
+ bundle.pending = undefined;
955
+ bundle.inFlightStep = undefined;
956
+ bundle.finalizeCallInFlight = false;
957
+ bundle.runState = 'terminal';
958
+ await persistBundle(this.deps.backend, sessionId, bundle);
959
+ this.surfaceFinal(ctx, `Error: ${error}`, usage);
960
+ }
961
+ async finalize(ctx, sessionId, bundle, rag, prompt, logUsage, usageNow, now, terminalTtlMs,
962
+ /** Used ONLY when no finalizer is injected (3-role config): the adaptive
963
+ * planner's already-composed done.result. */
964
+ legacyAnswer) {
965
+ const deps = this.deps;
966
+ const cfg = deps.config.budgets;
967
+ const maxFinalizeRetries = cfg.maxFinalizeRetries ?? 2;
968
+ // The finalizer reads the run's approved results + the DURABLE originalRequest
969
+ // (the verbatim request that started the run), never the live resume prompt.
970
+ const request = bundle.originalRequest ?? prompt;
971
+ const approved = deps.finalizer && bundle.runId
972
+ ? await collectApproved(rag, bundle.runId)
973
+ : [];
974
+ // Shared exhaustion handler (pre-call AND in-catch): apply onFinalizeExhausted.
975
+ const onExhausted = async (reason) => {
976
+ if ((deps.config.onFinalizeExhausted ?? 'error') === 'best-effort') {
977
+ return (approved.map((a) => `[#${a.seq}] ${a.content}`).join('\n\n') +
978
+ '\n\n[incomplete: the final answer could not be composed]');
979
+ }
980
+ await this.abortTerminal(ctx, sessionId, bundle, reason, now, terminalTtlMs, usageNow());
981
+ return null;
982
+ };
983
+ // Crash-replay charge: a prior finalize call in flight → this re-entry is a
984
+ // replay; charge finalizeAttempt and CHECK the cap BEFORE re-invoking.
985
+ if (bundle.finalizeCallInFlight) {
986
+ bundle.finalizeAttempt = (bundle.finalizeAttempt ?? 0) + 1;
987
+ if ((bundle.finalizeAttempt ?? 0) > maxFinalizeRetries) {
988
+ const best = await onExhausted('finalizer retry budget exhausted on recovery');
989
+ if (best === null)
990
+ return true;
991
+ await this.commitTerminalSuccess(ctx, sessionId, bundle, best, now, terminalTtlMs, usageNow());
992
+ return true;
993
+ }
994
+ }
995
+ // Legacy (no-finalizer) path: persist the planner's composed answer DURABLY in
996
+ // the SAME write that enters 'finalizing', so a crash before the terminal write
997
+ // can recover it rather than emitting empty.
998
+ if (!deps.finalizer && legacyAnswer !== undefined) {
999
+ bundle.legacyFinalAnswer = legacyAnswer;
1000
+ }
1001
+ bundle.runPhase = 'finalizing';
1002
+ bundle.finalizeCallInFlight = true;
1003
+ await persistBundle(deps.backend, sessionId, bundle);
1004
+ let answer;
1005
+ if (deps.finalizer && bundle.runId) {
1006
+ while (answer === undefined) {
1007
+ try {
1008
+ const composed = await deps.finalizer.finalize(bundle.goal, request, approved, {
1009
+ hint: deps.config.subagents.finalizer?.hint,
1010
+ logUsage,
1011
+ log: (m) => dlog(m),
1012
+ });
1013
+ // Empty-but-ok finalizer output is a JUDGE failure (spec), not a valid
1014
+ // answer → throw so it retries within maxFinalizeRetries.
1015
+ if (composed.trim().length === 0) {
1016
+ throw new Error('finalizer returned an empty answer');
1017
+ }
1018
+ answer = composed;
1019
+ }
1020
+ catch (e) {
1021
+ bundle.finalizeAttempt = (bundle.finalizeAttempt ?? 0) + 1;
1022
+ await persistBundle(deps.backend, sessionId, bundle);
1023
+ if ((bundle.finalizeAttempt ?? 0) > maxFinalizeRetries) {
1024
+ const best = await onExhausted(`finalizer failed after ${maxFinalizeRetries} retries: ${String(e)}`);
1025
+ if (best === null)
1026
+ return true; // 'error' policy aborted terminally
1027
+ answer = best; // 'best-effort'
1028
+ break;
1029
+ }
1030
+ // else: loop and retry the finalizer.
1031
+ }
1032
+ }
1033
+ }
1034
+ else {
1035
+ // Legacy: the adaptive planner already composed the answer in done.result.
1036
+ // Prefer the live param, else the durable copy persisted on finalizing-entry.
1037
+ answer = legacyAnswer ?? bundle.legacyFinalAnswer ?? '';
1038
+ }
1039
+ await this.commitTerminalSuccess(ctx, sessionId, bundle, answer ?? '', now, terminalTtlMs, usageNow());
1040
+ return true;
1041
+ }
1042
+ /** Store-first terminal SUCCESS: write the terminal store FIRST, then flip the
1043
+ * bundle to terminal and surface the answer (mirror of abortTerminal). */
1044
+ async commitTerminalSuccess(ctx, sessionId, bundle, answer, now, terminalTtlMs, usage) {
1045
+ await writeTerminal(this.deps.backend, sessionId, bundle.runId ?? sessionId, { kind: 'success', answer }, terminalTtlMs, now());
1046
+ bundle.pending = undefined;
1047
+ bundle.finalizeCallInFlight = false;
1048
+ bundle.runState = 'terminal';
1049
+ bundle.inFlightStep = undefined;
1050
+ await persistBundle(this.deps.backend, sessionId, bundle);
1051
+ this.surfaceFinal(ctx, answer, usage);
1052
+ }
412
1053
  surfaceClarify(ctx, question, usage) {
413
1054
  ctx.yield({
414
1055
  ok: true,
@@ -484,18 +1125,23 @@ const EXECUTOR_SYSTEM = 'You are the executor. You have tools that read the live
484
1125
  'for per-item details.';
485
1126
  const TOOL_SELECT_K = 20;
486
1127
  /** Top-k recalled artifacts injected into the executor context per step. */
487
- const RECALL_K = 5;
488
1128
  /** Artifact types eligible for recall (excludes the 'controller-bundle' record
489
1129
  * that shares the same backend). */
490
1130
  const RECALL_ARTIFACT_TYPES = ['step-result', 'mcp-result'];
491
- /** Hard cap on the total injected recall length (chars). */
492
- const RECALL_MAX_CHARS = 4000;
1131
+ /** Per-kind recall counts (distinct artifacts kept after dedup + cap). */
1132
+ const RECALL_K_STEP = 4;
1133
+ const RECALL_K_MCP = 4;
1134
+ /** SEPARATE char budgets per kind, so a huge step-result cannot starve MCP context. */
1135
+ const RECALL_MAX_CHARS_STEP = 2000;
1136
+ const RECALL_MAX_CHARS_MCP = 2000;
1137
+ /** Char budget for a single per-`requires` evidence extract handed to the reviewer. */
1138
+ const RECALL_EVIDENCE_CHARS = 800;
493
1139
  // ---------------------------------------------------------------------------
494
1140
  // Pure helpers
495
1141
  // ---------------------------------------------------------------------------
496
- /** Build a bounded "Relevant prior context" block from recalled artifacts, or
497
- * undefined when there is nothing to inject. */
498
- function buildRecallBlock(hits) {
1142
+ /** Build a bounded "Relevant prior context" block from recalled artifacts under
1143
+ * the given char budget, or undefined when there is nothing to inject. */
1144
+ function buildRecallBlock(hits, maxChars) {
499
1145
  if (hits.length === 0)
500
1146
  return undefined;
501
1147
  const parts = [];
@@ -504,8 +1150,8 @@ function buildRecallBlock(hits) {
504
1150
  const c = h.content ?? '';
505
1151
  if (c.length === 0)
506
1152
  continue;
507
- if (used + c.length > RECALL_MAX_CHARS) {
508
- parts.push(c.slice(0, RECALL_MAX_CHARS - used));
1153
+ if (used + c.length > maxChars) {
1154
+ parts.push(c.slice(0, maxChars - used));
509
1155
  break;
510
1156
  }
511
1157
  parts.push(c);
@@ -540,8 +1186,26 @@ export function parseNextStep(content) {
540
1186
  return { kind: 'done', result: obj.result };
541
1187
  if (obj.kind === 'rewind' && typeof obj.reason === 'string')
542
1188
  return { kind: 'rewind', reason: obj.reason };
543
- if (obj.kind === 'next' && obj.step && typeof obj.step.name === 'string')
544
- return { kind: 'next', step: obj.step };
1189
+ if (obj.kind === 'next' &&
1190
+ obj.step &&
1191
+ typeof obj.step.name === 'string' &&
1192
+ typeof obj.step.instructions === 'string') {
1193
+ // Validate requires[] so a non-string / empty / oversized reference never
1194
+ // reaches the semantic query / embedder; a malformed value is a parse
1195
+ // failure that drives the existing parse-retry.
1196
+ const req = validateRequires(obj.step.requires);
1197
+ if (req === false)
1198
+ return null;
1199
+ return {
1200
+ kind: 'next',
1201
+ step: {
1202
+ name: obj.step.name,
1203
+ instructions: obj.step.instructions,
1204
+ ...(obj.step.type ? { type: obj.step.type } : {}),
1205
+ ...(req ? { requires: req } : {}),
1206
+ },
1207
+ };
1208
+ }
545
1209
  }
546
1210
  catch {
547
1211
  // fall through
@@ -612,6 +1276,26 @@ function toLlmToolCall(c) {
612
1276
  arguments: args,
613
1277
  };
614
1278
  }
1279
+ /** Reconstruct and render the live step-state board from artifacts.
1280
+ * Returns '' when there is no runId (the board has nothing to show yet). */
1281
+ async function renderLiveBoard(rag, bundle, budget) {
1282
+ const runId = bundle.runId;
1283
+ if (!runId)
1284
+ return '';
1285
+ const [structure, claims] = await Promise.all([
1286
+ readPlanDecisions(rag, runId),
1287
+ readClaims(rag, runId),
1288
+ ]);
1289
+ const stepResults = await rag.list({ runId, artifactType: 'step-result' });
1290
+ const board = reconstructBoard({
1291
+ structure,
1292
+ stepResults,
1293
+ claims,
1294
+ inFlight: bundle.inFlightStep,
1295
+ pending: bundle.pending,
1296
+ });
1297
+ return renderBoard(board, budget);
1298
+ }
615
1299
  /** Synthesize the strict KnowledgeEntryMetadata for controller artifacts. */
616
1300
  function synthMeta(ctx, sessionId) {
617
1301
  const traceId = ctx.options?.trace?.traceId ?? sessionId;
@@ -624,4 +1308,182 @@ function synthMeta(ctx, sessionId) {
624
1308
  createdAt: new Date().toISOString(),
625
1309
  };
626
1310
  }
1311
+ /** Map a reviewer status to the planner transition. ok/exists advance; partial
1312
+ * advances the accepted part AND forces a remainder replan; failed replans. */
1313
+ function mapOutcome(status) {
1314
+ if (status === 'ok' || status === 'exists')
1315
+ return 'advanced';
1316
+ if (status === 'partial')
1317
+ return 'partial';
1318
+ return 'failed';
1319
+ }
1320
+ /** Append ONE payload-free control record to plannerPrivate (the cache holds
1321
+ * {seq,status,note,remainder}, never the approved content). Used by both normal
1322
+ * settle and crash/external reconciliation so plannerPrivate is identical
1323
+ * whichever path committed the step. */
1324
+ function recordStepControl(bundle, rec) {
1325
+ bundle.plannerPrivate +=
1326
+ `\n[seq ${rec.seq} ${rec.name} ${rec.status}]` +
1327
+ (rec.note ? ` ${rec.note}` : '') +
1328
+ (rec.remainder ? ` remainder: ${rec.remainder}` : '');
1329
+ }
1330
+ /** Gather the run's approved results, one per seq, resolved by outcome precedence
1331
+ * (ok/exists > partial > failed), ordered by seq. Reconstructs the complete
1332
+ * Outcome from artifact metadata (status/note/remainder) + content. */
1333
+ async function collectApproved(rag, runId) {
1334
+ const all = await rag.list({ runId, artifactType: 'step-result' });
1335
+ const bySeq = new Map();
1336
+ for (const e of all) {
1337
+ const seq = e.metadata.seq ?? 0;
1338
+ const o = {
1339
+ status: (e.metadata.status ?? 'failed'),
1340
+ approved: e.content,
1341
+ remainder: e.metadata.remainder ?? '',
1342
+ note: e.metadata.note ?? '',
1343
+ };
1344
+ const arr = bySeq.get(seq);
1345
+ if (arr)
1346
+ arr.push(o);
1347
+ else
1348
+ bySeq.set(seq, [o]);
1349
+ }
1350
+ const out = [];
1351
+ for (const [seq, outcomes] of [...bySeq.entries()].sort((a, b) => a[0] - b[0])) {
1352
+ const resolved = resolveByPrecedence(outcomes);
1353
+ if (resolved && resolved.status !== 'failed')
1354
+ out.push({ seq, content: resolved.approved });
1355
+ }
1356
+ return out;
1357
+ }
1358
+ /** The ONE run-scoped results-RAG recall primitive — used by BOTH the whole-step
1359
+ * recall AND the per-`requires` evidence. EMBEDDING-based similarity via the
1360
+ * backend's semantic query (NO homemade lexical scoring): the backend embeds the
1361
+ * query + ranks by vector similarity, with the `runId` filter applied PRE-cap.
1362
+ * Over-fetch `kPrime` (caller-supplied so the duplication bound is justified PER
1363
+ * KIND), then dedup and cap to `k`. Dedup: step-results (have `seq`) →
1364
+ * precedence-winner per seq; mcp-results → by `identityKey`. Embedding rank order
1365
+ * is preserved through the dedup.
1366
+ * `options` is forwarded into the embedder so recall-time embeds are metered. */
1367
+ export async function runScopedRecall(rag, text, k, runId, kPrime, artifactType, options) {
1368
+ const hits = await rag.query(text, {
1369
+ k: kPrime,
1370
+ filter: { runId, artifactType },
1371
+ options,
1372
+ });
1373
+ const bestStep = new Map();
1374
+ const bestMcp = new Map();
1375
+ for (const e of hits) {
1376
+ if (e.metadata.seq !== undefined && e.metadata.status !== undefined) {
1377
+ const prev = bestStep.get(e.metadata.seq);
1378
+ if (!prev || isBetterStep(e, prev))
1379
+ bestStep.set(e.metadata.seq, e);
1380
+ }
1381
+ else if (e.metadata.identityKey) {
1382
+ const prev = bestMcp.get(e.metadata.identityKey);
1383
+ if (!prev || isBetterMcp(e, prev))
1384
+ bestMcp.set(e.metadata.identityKey, e);
1385
+ }
1386
+ }
1387
+ // Walk hits in embedding-rank order; emit each (runId,seq) / identityKey once.
1388
+ const out = [];
1389
+ const seenSeq = new Set();
1390
+ const seenMcp = new Set();
1391
+ for (const e of hits) {
1392
+ if (e.metadata.seq !== undefined && e.metadata.status !== undefined) {
1393
+ if (seenSeq.has(e.metadata.seq))
1394
+ continue;
1395
+ seenSeq.add(e.metadata.seq);
1396
+ // biome-ignore lint/style/noNonNullAssertion: bestStep has this seq (set above).
1397
+ out.push(bestStep.get(e.metadata.seq));
1398
+ }
1399
+ else if (e.metadata.identityKey) {
1400
+ if (seenMcp.has(e.metadata.identityKey))
1401
+ continue;
1402
+ seenMcp.add(e.metadata.identityKey);
1403
+ // biome-ignore lint/style/noNonNullAssertion: bestMcp has this key (set above).
1404
+ out.push(bestMcp.get(e.metadata.identityKey));
1405
+ }
1406
+ else {
1407
+ out.push(e);
1408
+ }
1409
+ if (out.length >= k)
1410
+ break;
1411
+ }
1412
+ return out.slice(0, k);
1413
+ }
1414
+ /** Outcome-precedence rank for step-result dedup (ok/exists > partial > failed). */
1415
+ function rankStatus(s) {
1416
+ return s === 'ok' || s === 'exists'
1417
+ ? 3
1418
+ : s === 'partial'
1419
+ ? 2
1420
+ : s === 'failed'
1421
+ ? 1
1422
+ : 0;
1423
+ }
1424
+ /** True when candidate `a` is a better winner than current `b` for step-result
1425
+ * dedup. Latest-wins by EXECUTION IDENTITY, not by semantic-rank position:
1426
+ * 1. Higher status rank wins; on tie →
1427
+ * 2. Higher attempt wins; on further tie →
1428
+ * 3. Higher writeOrdinal wins (tie-breaks same-timestamp artifacts from one run); on tie →
1429
+ * 4. Later createdAt wins (missing = older: compare with '' as sentinel). */
1430
+ function isBetterStep(a, b) {
1431
+ const ra = rankStatus(a.metadata.status);
1432
+ const rb = rankStatus(b.metadata.status);
1433
+ if (ra !== rb)
1434
+ return ra > rb;
1435
+ const aa = a.metadata.attempt ?? 0;
1436
+ const ba = b.metadata.attempt ?? 0;
1437
+ if (aa !== ba)
1438
+ return aa > ba;
1439
+ const ao = a.metadata.writeOrdinal ?? -1;
1440
+ const bo = b.metadata.writeOrdinal ?? -1;
1441
+ if (ao !== bo)
1442
+ return ao > bo;
1443
+ return (a.metadata.createdAt ?? '') > (b.metadata.createdAt ?? '');
1444
+ }
1445
+ /** True when candidate `a` is a better winner than current `b` for mcp-result
1446
+ * dedup. Latest-fetch wins by writeOrdinal first (handles same-timestamp), then
1447
+ * falls back to createdAt (missing = older). */
1448
+ function isBetterMcp(a, b) {
1449
+ const ao = a.metadata.writeOrdinal ?? -1;
1450
+ const bo = b.metadata.writeOrdinal ?? -1;
1451
+ if (ao !== bo)
1452
+ return ao > bo;
1453
+ return (a.metadata.createdAt ?? '') > (b.metadata.createdAt ?? '');
1454
+ }
1455
+ const MAX_EXTRACT_WINDOWS = 64;
1456
+ /** Return the ≤`maxChars` fragment of `content` most similar to `ref` by EMBEDDING
1457
+ * (NOT ASCII lexical overlap). DIRECT single-pass ranking: every candidate is
1458
+ * scored on its own. The SCORED window IS the RETURNED body: candidates are
1459
+ * `body = maxChars - 2` chars (head+tail '…' reserved up front), so the
1460
+ * highest-scoring fragment is never truncated by the markers. Stride is 50%
1461
+ * overlap, widened to span the whole content within MAX_EXTRACT_WINDOWS windows
1462
+ * (point coverage for content ≤ MAX_EXTRACT_WINDOWS×maxChars; larger thins to
1463
+ * non-overlapping, best-effort). Embeds are SEQUENTIAL and BOUNDED to ≤
1464
+ * MAX_EXTRACT_WINDOWS + 1 — touches NO public embedder API (batch is a deferred
1465
+ * optimization). Result STRICTLY ≤ maxChars; tiny maxChars (< 3) → bare slice.
1466
+ * The `requires` ref is English (planner invariant) → a normal embedder suffices. */
1467
+ export async function relevantExtract(content, ref, maxChars, embedder, options) {
1468
+ if (content.length <= maxChars)
1469
+ return content;
1470
+ if (maxChars < 3)
1471
+ return content.slice(0, Math.max(0, maxChars));
1472
+ const body = maxChars - 2;
1473
+ const stride = Math.max(Math.floor(body / 2), Math.ceil(content.length / MAX_EXTRACT_WINDOWS));
1474
+ const { vector: q } = await embedder.embed(ref, options);
1475
+ let bestStart = 0;
1476
+ let bestScore = Number.NEGATIVE_INFINITY;
1477
+ for (let s = 0; s < content.length; s += stride) {
1478
+ const { vector } = await embedder.embed(content.slice(s, s + body), options);
1479
+ const score = cosine(q, vector);
1480
+ if (score > bestScore) {
1481
+ bestScore = score;
1482
+ bestStart = s;
1483
+ }
1484
+ }
1485
+ const head = bestStart > 0 ? '…' : '';
1486
+ const tail = bestStart + body < content.length ? '…' : '';
1487
+ return head + content.slice(bestStart, bestStart + body) + tail;
1488
+ }
627
1489
  //# sourceMappingURL=controller-coordinator-handler.js.map