@mcp-abap-adt/llm-agent-server-libs 20.6.0 → 20.7.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.
- package/README.md +1 -0
- package/dist/factories/controller-factory.d.ts +5 -1
- package/dist/factories/controller-factory.d.ts.map +1 -1
- package/dist/factories/controller-factory.js +1 -0
- package/dist/factories/controller-factory.js.map +1 -1
- package/dist/factories/cyclic-factory.d.ts.map +1 -1
- package/dist/factories/cyclic-factory.js +4 -1
- package/dist/factories/cyclic-factory.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/mcp/compose-auxiliary.d.ts +6 -5
- package/dist/mcp/compose-auxiliary.d.ts.map +1 -1
- package/dist/mcp/compose-auxiliary.js +10 -6
- package/dist/mcp/compose-auxiliary.js.map +1 -1
- package/dist/pipelines/controller.d.ts.map +1 -1
- package/dist/pipelines/controller.js +13 -0
- package/dist/pipelines/controller.js.map +1 -1
- package/dist/pipelines/linear.js +1 -1
- package/dist/pipelines/linear.js.map +1 -1
- package/dist/pipelines/stepper.js +1 -1
- package/dist/pipelines/stepper.js.map +1 -1
- package/dist/smart-agent/controller/controller-coordinator-handler.d.ts +23 -3
- package/dist/smart-agent/controller/controller-coordinator-handler.d.ts.map +1 -1
- package/dist/smart-agent/controller/controller-coordinator-handler.js +255 -37
- package/dist/smart-agent/controller/controller-coordinator-handler.js.map +1 -1
- package/dist/smart-agent/controller/finalizer.d.ts +5 -1
- package/dist/smart-agent/controller/finalizer.d.ts.map +1 -1
- package/dist/smart-agent/controller/finalizer.js +1 -1
- package/dist/smart-agent/controller/finalizer.js.map +1 -1
- package/dist/smart-agent/controller/parser.d.ts.map +1 -1
- package/dist/smart-agent/controller/parser.js +2 -0
- package/dist/smart-agent/controller/parser.js.map +1 -1
- package/dist/smart-agent/controller/planner.d.ts +3 -2
- package/dist/smart-agent/controller/planner.d.ts.map +1 -1
- package/dist/smart-agent/controller/planner.js +64 -9
- package/dist/smart-agent/controller/planner.js.map +1 -1
- package/dist/smart-agent/controller/reviewer.d.ts +5 -1
- package/dist/smart-agent/controller/reviewer.d.ts.map +1 -1
- package/dist/smart-agent/controller/reviewer.js +1 -1
- package/dist/smart-agent/controller/reviewer.js.map +1 -1
- package/dist/smart-agent/controller/session-bundle.d.ts +10 -0
- package/dist/smart-agent/controller/session-bundle.d.ts.map +1 -1
- package/dist/smart-agent/controller/session-bundle.js +27 -0
- package/dist/smart-agent/controller/session-bundle.js.map +1 -1
- package/dist/smart-agent/controller/subagent-client.d.ts +9 -0
- package/dist/smart-agent/controller/subagent-client.d.ts.map +1 -1
- package/dist/smart-agent/controller/subagent-client.js +34 -1
- package/dist/smart-agent/controller/subagent-client.js.map +1 -1
- package/dist/smart-agent/controller/target-state.d.ts +1 -1
- package/dist/smart-agent/controller/target-state.d.ts.map +1 -1
- package/dist/smart-agent/controller/target-state.js +2 -1
- package/dist/smart-agent/controller/target-state.js.map +1 -1
- package/dist/smart-agent/controller/types.d.ts +30 -3
- package/dist/smart-agent/controller/types.d.ts.map +1 -1
- package/dist/smart-agent/controller/types.js +5 -2
- package/dist/smart-agent/controller/types.js.map +1 -1
- package/dist/smart-agent/controller/wait-step.d.ts +40 -0
- package/dist/smart-agent/controller/wait-step.d.ts.map +1 -0
- package/dist/smart-agent/controller/wait-step.js +76 -0
- package/dist/smart-agent/controller/wait-step.js.map +1 -0
- package/dist/smart-agent/http/chat-route-handler.d.ts.map +1 -1
- package/dist/smart-agent/http/chat-route-handler.js +3 -1
- package/dist/smart-agent/http/chat-route-handler.js.map +1 -1
- package/dist/smart-agent/http/debug-trace-sink.d.ts +9 -0
- package/dist/smart-agent/http/debug-trace-sink.d.ts.map +1 -0
- package/dist/smart-agent/http/debug-trace-sink.js +17 -0
- package/dist/smart-agent/http/debug-trace-sink.js.map +1 -0
- package/dist/smart-agent/smart-server.d.ts +8 -3
- package/dist/smart-agent/smart-server.d.ts.map +1 -1
- package/dist/smart-agent/smart-server.js +12 -5
- package/dist/smart-agent/smart-server.js.map +1 -1
- package/package.json +7 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { externalToolCallId, McpError, } from '@mcp-abap-adt/llm-agent';
|
|
1
|
+
import { DefaultWaitStrategy, externalToolCallId, McpError, } from '@mcp-abap-adt/llm-agent';
|
|
2
2
|
import { LegacyAccumulateContextStrategy, LegacyTranscriptContextStrategy, summaryToUsage, } from '@mcp-abap-adt/llm-agent-libs';
|
|
3
3
|
import { writePlanDecision } from './artifacts.js';
|
|
4
4
|
import { BoardOverBudgetError, renderLiveBoard, } from './board.js';
|
|
@@ -9,9 +9,11 @@ import { makeControllerPlanner } from './planner.js';
|
|
|
9
9
|
import { appendHint } from './prompts.js';
|
|
10
10
|
import { buildRecallBlock, collectApproved, RECALL_ARTIFACT_TYPES, RECALL_EVIDENCE_CHARS, RECALL_K_STEP, RECALL_MAX_CHARS_STEP, relevantExtract, runScopedRecall, } from './recall.js';
|
|
11
11
|
import { classifyRequest, readTerminal, writeTerminal } from './run-scope.js';
|
|
12
|
-
import { hydrateBundle, persistBundle, resetRun } from './session-bundle.js';
|
|
12
|
+
import { hydrateBundle, persistBundle, resetRun, settleStep, } from './session-bundle.js';
|
|
13
|
+
import { diagnosticCallOptions, } from './subagent-client.js';
|
|
13
14
|
import { establishTargetState } from './target-state.js';
|
|
14
15
|
import { makeLogUsage } from './usage-logging.js';
|
|
16
|
+
import { describeWait, isWaitStep, planWait } from './wait-step.js';
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Debug logging — gated behind DEBUG_CONTROLLER (e.g. DEBUG_CONTROLLER=1).
|
|
17
19
|
// Surfaces the steps the planner delegates and per-role/total token usage to
|
|
@@ -22,6 +24,14 @@ function dlog(msg) {
|
|
|
22
24
|
if (process.env.DEBUG_CONTROLLER)
|
|
23
25
|
console.error(`[controller] ${msg}`);
|
|
24
26
|
}
|
|
27
|
+
/** Decision-point capture (area `controller`): mirrors `dlog`'s stderr
|
|
28
|
+
* breadcrumb into a per-request session-log record so a DEBUG_CONTROLLER run
|
|
29
|
+
* shows WHY the controller looped/replanned/rejected, not just THAT it did.
|
|
30
|
+
* A no-op when no sessionLogger is wired (observability only). */
|
|
31
|
+
function logDecision(ctx, kind, reason, extra) {
|
|
32
|
+
dlog(`decision ${kind}: ${reason}`);
|
|
33
|
+
ctx.options?.sessionLogger?.logStep(`controller_decision_${kind}`, { kind, reason, ...extra }, 'controller');
|
|
34
|
+
}
|
|
25
35
|
// ---------------------------------------------------------------------------
|
|
26
36
|
// Re-exported for import-path stability (helpers moved to sibling modules).
|
|
27
37
|
// ---------------------------------------------------------------------------
|
|
@@ -211,6 +221,8 @@ export class ControllerCoordinatorHandler {
|
|
|
211
221
|
if (resolved.status === 'failed') {
|
|
212
222
|
if (bundle.inFlightStep)
|
|
213
223
|
bundle.inFlightStep.phase = 'awaiting-replan';
|
|
224
|
+
logDecision(ctx, 'replan', resolved.note ||
|
|
225
|
+
`step "${bundle.inFlightStep?.step.name ?? 'step'}" resolved failed (adopted artifact)`);
|
|
214
226
|
}
|
|
215
227
|
else {
|
|
216
228
|
bundle.nextSeq = (bundle.nextSeq ?? 0) + 1;
|
|
@@ -326,6 +338,9 @@ export class ControllerCoordinatorHandler {
|
|
|
326
338
|
bundle.evalCallInFlight = false;
|
|
327
339
|
bundle.evalResumeCount = 0;
|
|
328
340
|
logUsage('evaluator', outcome.usage);
|
|
341
|
+
logDecision(ctx, 'target-state', outcome.kind === 'established'
|
|
342
|
+
? `goal established: ${outcome.goal}`
|
|
343
|
+
: `needs confirmation: ${outcome.question}`, { kind: outcome.kind });
|
|
329
344
|
if (outcome.kind === 'needs-confirmation') {
|
|
330
345
|
// Persist the proposed target with the pending marker so a confirmation
|
|
331
346
|
// on resume commits IT (not a bare "yes"). See the clarify-resume above.
|
|
@@ -398,6 +413,8 @@ export class ControllerCoordinatorHandler {
|
|
|
398
413
|
});
|
|
399
414
|
if (resolved.status === 'failed') {
|
|
400
415
|
inf.phase = 'awaiting-replan';
|
|
416
|
+
logDecision(ctx, 'replan', resolved.note ||
|
|
417
|
+
`step "${inf.step.name}" resolved failed (adopted artifact)`);
|
|
401
418
|
}
|
|
402
419
|
else {
|
|
403
420
|
bundle.nextSeq = inf.seq + 1;
|
|
@@ -410,6 +427,27 @@ export class ControllerCoordinatorHandler {
|
|
|
410
427
|
// No artifact for this attempt → re-run the SAME step directly. Distinguish a
|
|
411
428
|
// live external CONTINUATION (bounded by toolCallCount) from a crash-replay
|
|
412
429
|
// (charged to resumeCount).
|
|
430
|
+
//
|
|
431
|
+
// A `wait` step is served here FIRST — BEFORE the resumeCount accounting
|
|
432
|
+
// below. A wait remainder is a continuation against an already-durable
|
|
433
|
+
// deadline, NOT a crash replay: charging it to resumeCount would let
|
|
434
|
+
// repeated abort/resume of a long wait terminally kill the run. Non-wait
|
|
435
|
+
// steps fall through to the resume/replay accounting unchanged.
|
|
436
|
+
const resumeWait = await this.serveWaitStep({
|
|
437
|
+
ctx,
|
|
438
|
+
sessionId,
|
|
439
|
+
bundle,
|
|
440
|
+
rag,
|
|
441
|
+
meta,
|
|
442
|
+
step: inf.step,
|
|
443
|
+
cfg,
|
|
444
|
+
nowIso: now,
|
|
445
|
+
onCommit: (o) => planner.commit?.(bundle, o),
|
|
446
|
+
});
|
|
447
|
+
if (resumeWait === 'aborted')
|
|
448
|
+
return true;
|
|
449
|
+
if (resumeWait === 'served')
|
|
450
|
+
continue;
|
|
413
451
|
if (externalContinuation) {
|
|
414
452
|
externalContinuation = false;
|
|
415
453
|
}
|
|
@@ -500,6 +538,16 @@ export class ControllerCoordinatorHandler {
|
|
|
500
538
|
}
|
|
501
539
|
planParseRetries = 0;
|
|
502
540
|
resumedExternal = false; // a valid decision consumed any external-resume replan
|
|
541
|
+
if (next.kind === 'error') {
|
|
542
|
+
// The planner saw a failure it cannot fix within the consumer's
|
|
543
|
+
// constraints (a pinned name that is taken, an unauthorized op, a lock
|
|
544
|
+
// that will not clear). Terminate the run and return the REAL tool error
|
|
545
|
+
// to the consumer — distinct from the generic abortTerminal reasons and
|
|
546
|
+
// never (no response). (#213)
|
|
547
|
+
logDecision(ctx, 'planner-error', next.error);
|
|
548
|
+
await this.abortTerminal(ctx, sessionId, bundle, next.error, now, terminalTtlMs, usageNow());
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
503
551
|
if (next.kind === 'done') {
|
|
504
552
|
// Pass next.result as the legacy answer: used only when no finalizer is
|
|
505
553
|
// injected (3-role config) — the plan-first planner already composed it.
|
|
@@ -526,7 +574,9 @@ export class ControllerCoordinatorHandler {
|
|
|
526
574
|
const prev = bundle.inFlightStep;
|
|
527
575
|
const attempt = prev && prev.seq === seq ? prev.attempt + 1 : 0;
|
|
528
576
|
if (attempt >= (cfg.maxStepAttempts ?? 5)) {
|
|
529
|
-
|
|
577
|
+
const reason = `step "${next.step.name}" exceeded maxStepAttempts`;
|
|
578
|
+
logDecision(ctx, 'retry-exhausted', reason);
|
|
579
|
+
await this.abortTerminal(ctx, sessionId, bundle, reason, now, terminalTtlMs, usageNow());
|
|
530
580
|
return true;
|
|
531
581
|
}
|
|
532
582
|
bundle.inFlightStep = {
|
|
@@ -540,6 +590,24 @@ export class ControllerCoordinatorHandler {
|
|
|
540
590
|
};
|
|
541
591
|
bundle.runPhase = 'executing';
|
|
542
592
|
await persistBundle(deps.backend, sessionId, bundle);
|
|
593
|
+
// A `wait` step is served by the controller itself (no executor / reviewer /
|
|
594
|
+
// MCP). The in-flight step is already durable above, so serveWaitStep can
|
|
595
|
+
// persist its deadline and settle. Non-wait steps fall through to runStep.
|
|
596
|
+
const freshWait = await this.serveWaitStep({
|
|
597
|
+
ctx,
|
|
598
|
+
sessionId,
|
|
599
|
+
bundle,
|
|
600
|
+
rag,
|
|
601
|
+
meta,
|
|
602
|
+
step: next.step,
|
|
603
|
+
cfg,
|
|
604
|
+
nowIso: now,
|
|
605
|
+
onCommit: (o) => planner.commit?.(bundle, o),
|
|
606
|
+
});
|
|
607
|
+
if (freshWait === 'aborted')
|
|
608
|
+
return true;
|
|
609
|
+
if (freshWait === 'served')
|
|
610
|
+
continue;
|
|
543
611
|
const completed = await this.runStep(ctx, sessionId, bundle, rag, meta, next.step, isExternalTool, logUsage, usageNow, (o) => planner.commit?.(bundle, o));
|
|
544
612
|
if (completed === 'suspended' || completed === 'aborted')
|
|
545
613
|
return true;
|
|
@@ -608,23 +676,19 @@ export class ControllerCoordinatorHandler {
|
|
|
608
676
|
// resume after a failed step replans instead of repeating it) AND advance the
|
|
609
677
|
// planner cursor (onCommit) in the SAME persistBundle that records the step
|
|
610
678
|
// result — never in a separate write, so a crash cannot replay a completed step.
|
|
611
|
-
const settle =
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
679
|
+
const settle = (outcome,
|
|
680
|
+
/** Human reason for a 'failed' settle — logged via logDecision (kind
|
|
681
|
+
* 'replan'); the caller usually already computed this string for the
|
|
682
|
+
* plannerPrivate note / writeControlFailure call. Absent → a generic
|
|
683
|
+
* fallback so the decision record is never missing a reason. */
|
|
684
|
+
reason) => {
|
|
685
|
+
// #228 debug-trace: a 'failed' settle induces a replan — record it.
|
|
686
|
+
// The state mutation + atomic persist is the extracted settleStep (#229);
|
|
687
|
+
// logDecision is a trace side-effect, order vs persist is immaterial.
|
|
688
|
+
if (outcome === 'failed') {
|
|
689
|
+
logDecision(ctx, 'replan', reason ?? `step "${step.name}" failed — awaiting replan`);
|
|
618
690
|
}
|
|
619
|
-
|
|
620
|
-
// 'failed' — keep the same seq, mark awaiting-replan in the SAME persist so
|
|
621
|
-
// recovery routes by durable phase.
|
|
622
|
-
if (bundle.inFlightStep)
|
|
623
|
-
bundle.inFlightStep.phase = 'awaiting-replan';
|
|
624
|
-
bundle.runPhase = 'executing';
|
|
625
|
-
}
|
|
626
|
-
await persistBundle(deps.backend, sessionId, bundle);
|
|
627
|
-
return outcome;
|
|
691
|
+
return settleStep(deps.backend, sessionId, bundle, outcome, onCommit);
|
|
628
692
|
};
|
|
629
693
|
// The IMMUTABLE per-round prefix: system + step user message + the step-result
|
|
630
694
|
// recall block. Re-emitted verbatim every round via strategy.form(); the dynamic
|
|
@@ -653,6 +717,13 @@ export class ControllerCoordinatorHandler {
|
|
|
653
717
|
if (stepBlock) {
|
|
654
718
|
staticPrefix.push({ role: 'user', content: stepBlock });
|
|
655
719
|
}
|
|
720
|
+
ctx.options?.sessionLogger?.logStep('rag_recall', {
|
|
721
|
+
query: recallText,
|
|
722
|
+
extracts: recalledSteps.map((e) => ({
|
|
723
|
+
task: e.metadata.task,
|
|
724
|
+
content: e.content,
|
|
725
|
+
})),
|
|
726
|
+
}, 'rag');
|
|
656
727
|
// Per-step tool-loop context strategy (record/form). Absent factory →
|
|
657
728
|
// LegacyAccumulateContextStrategy (byte-identical to the historical growing
|
|
658
729
|
// transcript).
|
|
@@ -744,9 +815,15 @@ export class ControllerCoordinatorHandler {
|
|
|
744
815
|
// any-candidate flag. Gathered SEQUENTIALLY (NOT Promise.all): each
|
|
745
816
|
// relevantExtract is itself bounded-sequential, so the outer sequential loop
|
|
746
817
|
// keeps at most ONE embed request in flight at a time (rate-limit-safe).
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
818
|
+
// ONLY declared dependencies produce evidence. A step with no `requires`
|
|
819
|
+
// is a leaf — it consumes nothing, so there is nothing to attest and the
|
|
820
|
+
// reviewer judges it from the executor's result alone. Falling back to a
|
|
821
|
+
// recall on the step's OWN text (as this once did) asks whether an
|
|
822
|
+
// artifact for the step exists BEFORE the step has produced one: always
|
|
823
|
+
// MISSING, and the reviewer is instructed to fail a missing required
|
|
824
|
+
// reference. That rejected correct work and triggered an endless
|
|
825
|
+
// replan loop — the token balloon in issue #213.
|
|
826
|
+
const refs = step.requires ?? [];
|
|
750
827
|
const evBound = RECALL_K_STEP * (maxAttempts + 1) +
|
|
751
828
|
cfg.maxSteps * maxAttempts * (cfg.maxToolCalls ?? 10);
|
|
752
829
|
const evidence = [];
|
|
@@ -814,7 +891,8 @@ export class ControllerCoordinatorHandler {
|
|
|
814
891
|
seq: inFlight.seq,
|
|
815
892
|
};
|
|
816
893
|
}
|
|
817
|
-
|
|
894
|
+
logDecision(ctx, 'control-failure', noteFor(reason));
|
|
895
|
+
return settle('failed', noteFor(reason));
|
|
818
896
|
};
|
|
819
897
|
// Inner loop handles tool routing / error retries until the executor
|
|
820
898
|
// produces content for this step (or the step suspends on an external tool).
|
|
@@ -855,6 +933,7 @@ export class ControllerCoordinatorHandler {
|
|
|
855
933
|
hint: deps.config.subagents.reviewer?.hint,
|
|
856
934
|
logUsage,
|
|
857
935
|
maxDigestChars: cfg.maxDigestChars ?? 500,
|
|
936
|
+
callOptions: diagnosticCallOptions(ctx.options),
|
|
858
937
|
})
|
|
859
938
|
: {
|
|
860
939
|
kind: 'outcome',
|
|
@@ -878,14 +957,19 @@ export class ControllerCoordinatorHandler {
|
|
|
878
957
|
// planner replans, rather than aborting the whole run — the terminal
|
|
879
958
|
// backstop is maxStepAttempts/maxSteps, not a single unverifiable verdict.
|
|
880
959
|
bundle.budgets.stepsUsed++;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
960
|
+
const unverifiableReason = `reviewer unverifiable after ${cfg.maxReviewRetries ?? 2} retries: ${review.reason}`;
|
|
961
|
+
await writeControlFailure(unverifiableReason);
|
|
962
|
+
bundle.plannerPrivate += `\n[seq ${bundle.inFlightStep?.seq ?? bundle.nextSeq ?? 0} ${step.name} failed] ${unverifiableReason}`;
|
|
963
|
+
logDecision(ctx, 'reviewer-unverifiable', unverifiableReason, {
|
|
964
|
+
retries: reviewRetries,
|
|
965
|
+
});
|
|
966
|
+
return settle('failed', unverifiableReason);
|
|
884
967
|
}
|
|
885
968
|
review = await deps.reviewer.review(step, evidence, res.content, {
|
|
886
969
|
hint: deps.config.subagents.reviewer?.hint,
|
|
887
970
|
logUsage,
|
|
888
971
|
maxDigestChars: cfg.maxDigestChars ?? 500,
|
|
972
|
+
callOptions: diagnosticCallOptions(ctx.options),
|
|
889
973
|
});
|
|
890
974
|
}
|
|
891
975
|
const outcome = review.outcome;
|
|
@@ -917,6 +1001,13 @@ export class ControllerCoordinatorHandler {
|
|
|
917
1001
|
note: outcome.note,
|
|
918
1002
|
remainder: outcome.remainder,
|
|
919
1003
|
});
|
|
1004
|
+
if (mapped === 'failed') {
|
|
1005
|
+
const rejectReason = outcome.note || `reviewer rejected step "${step.name}"`;
|
|
1006
|
+
logDecision(ctx, 'reviewer-reject', rejectReason, {
|
|
1007
|
+
status: outcome.status,
|
|
1008
|
+
});
|
|
1009
|
+
return settle(mapped, rejectReason);
|
|
1010
|
+
}
|
|
920
1011
|
return settle(mapped);
|
|
921
1012
|
}
|
|
922
1013
|
if (res.kind === 'error') {
|
|
@@ -932,9 +1023,10 @@ export class ControllerCoordinatorHandler {
|
|
|
932
1023
|
// Retries exhausted — feed the error back as the step result so the
|
|
933
1024
|
// planner can replan on the next iteration.
|
|
934
1025
|
bundle.budgets.stepsUsed++;
|
|
935
|
-
|
|
1026
|
+
const executorErrorReason = `executor error: ${res.error}`;
|
|
1027
|
+
await writeControlFailure(executorErrorReason);
|
|
936
1028
|
bundle.plannerPrivate += `\n[step ${step.name} failed] ${res.error}`;
|
|
937
|
-
return settle('failed');
|
|
1029
|
+
return settle('failed', executorErrorReason);
|
|
938
1030
|
}
|
|
939
1031
|
// res.kind === 'tool_call' → route the FIRST tool call.
|
|
940
1032
|
const firstCall = res.toolCalls[0];
|
|
@@ -952,7 +1044,7 @@ export class ControllerCoordinatorHandler {
|
|
|
952
1044
|
bundle.budgets.stepsUsed++;
|
|
953
1045
|
await writeControlFailure('empty tool call');
|
|
954
1046
|
bundle.plannerPrivate += `\n[step ${step.name} failed] empty tool call`;
|
|
955
|
-
return settle('failed');
|
|
1047
|
+
return settle('failed', `step "${step.name}" produced an empty tool call`);
|
|
956
1048
|
}
|
|
957
1049
|
// Normalize the StreamToolCall (full or delta) into an LlmToolCall inline.
|
|
958
1050
|
const call = 'arguments' in firstCall &&
|
|
@@ -1024,9 +1116,10 @@ export class ControllerCoordinatorHandler {
|
|
|
1024
1116
|
continue;
|
|
1025
1117
|
}
|
|
1026
1118
|
bundle.budgets.stepsUsed++;
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1119
|
+
const unavailableToolReason = `requested unavailable tool ${name}`;
|
|
1120
|
+
await writeControlFailure(unavailableToolReason);
|
|
1121
|
+
bundle.plannerPrivate += `\n[step ${step.name} failed] ${unavailableToolReason}`;
|
|
1122
|
+
return settle('failed', unavailableToolReason);
|
|
1030
1123
|
}
|
|
1031
1124
|
// Prospective count gate BEFORE the increment (the before-increment model:
|
|
1032
1125
|
// the increment happens only after canExecuteTool allows the call).
|
|
@@ -1049,10 +1142,18 @@ export class ControllerCoordinatorHandler {
|
|
|
1049
1142
|
// decision → rethrow → outer catch swallow → (no response)). A non-McpError
|
|
1050
1143
|
// is a genuine unexpected error and is re-thrown for the outer handler.
|
|
1051
1144
|
let result;
|
|
1145
|
+
const mcpCallStartedAt = Date.now();
|
|
1052
1146
|
try {
|
|
1053
1147
|
result = await deps.callMcp(name, args, callSignal);
|
|
1054
1148
|
}
|
|
1055
1149
|
catch (mcpErr) {
|
|
1150
|
+
ctx.options?.sessionLogger?.logStep('mcp_tool_call', {
|
|
1151
|
+
name,
|
|
1152
|
+
args,
|
|
1153
|
+
result: undefined,
|
|
1154
|
+
isError: true,
|
|
1155
|
+
durationMs: Date.now() - mcpCallStartedAt,
|
|
1156
|
+
}, 'mcp');
|
|
1056
1157
|
// A step-timeout cancellation aborts the merged signal → the bridge rejects.
|
|
1057
1158
|
// Map that to a step-timeout control-failure BEFORE the McpError escalate so
|
|
1058
1159
|
// it is NOT mis-classified as MCP-unavailable (20.4.0 escalate order is
|
|
@@ -1067,6 +1168,28 @@ export class ControllerCoordinatorHandler {
|
|
|
1067
1168
|
}
|
|
1068
1169
|
throw mcpErr;
|
|
1069
1170
|
}
|
|
1171
|
+
ctx.options?.sessionLogger?.logStep('mcp_tool_call', {
|
|
1172
|
+
name,
|
|
1173
|
+
args,
|
|
1174
|
+
result: result.text,
|
|
1175
|
+
// The REAL tool-level isError, threaded through the bridge (#213).
|
|
1176
|
+
// Previously hardcoded false, so a locked-object error looked like a
|
|
1177
|
+
// delivered result and the executor retried it forever.
|
|
1178
|
+
isError: result.isError,
|
|
1179
|
+
durationMs: Date.now() - mcpCallStartedAt,
|
|
1180
|
+
}, 'mcp');
|
|
1181
|
+
// #213 immediate cut: a delivered tool-level error ends the step NOW.
|
|
1182
|
+
// The executor tool-loop does NOT continue (no further tool call, no
|
|
1183
|
+
// reviewer for this step); reuse cutControlFailure so the step settles
|
|
1184
|
+
// 'failed' with the tool's error text and the planner replans / surfaces
|
|
1185
|
+
// it. Read result.isError directly here — BEFORE the round reaches the
|
|
1186
|
+
// context strategy — so no Message/meta replay is needed. The durable
|
|
1187
|
+
// failed step-result + plannerPrivate note (written by cutControlFailure)
|
|
1188
|
+
// ARE the resume carrier; the mcp-result artifact is intentionally not
|
|
1189
|
+
// relied on (the cut may never call strategy.record).
|
|
1190
|
+
if (result.isError) {
|
|
1191
|
+
return cutControlFailure(result.text);
|
|
1192
|
+
}
|
|
1070
1193
|
// Record this exchange as a coherent assistant→tool ROUND (OpenAI protocol)
|
|
1071
1194
|
// via the context strategy so the executor LLM continues from its own tool
|
|
1072
1195
|
// call. The strategy owns the per-round context (Window keeps a bounded
|
|
@@ -1094,14 +1217,19 @@ export class ControllerCoordinatorHandler {
|
|
|
1094
1217
|
{
|
|
1095
1218
|
role: 'tool',
|
|
1096
1219
|
tool_call_id: call.id,
|
|
1097
|
-
content: result,
|
|
1220
|
+
content: result.text,
|
|
1098
1221
|
},
|
|
1099
1222
|
],
|
|
1100
|
-
// Stable fetch identity (tool+args) for run-scoped recall dedup
|
|
1101
|
-
//
|
|
1102
|
-
// server aborts BEFORE record; a returned
|
|
1223
|
+
// Stable fetch identity (tool+args) for run-scoped recall dedup, plus
|
|
1224
|
+
// the tool-level isError threaded from the bridge (#213). An unavailable
|
|
1225
|
+
// MCP server still aborts BEFORE record; a returned result carries its
|
|
1226
|
+
// real isError so the executor sees a failed call as failed, not as a
|
|
1227
|
+
// delivered success it retries forever.
|
|
1103
1228
|
meta: [
|
|
1104
|
-
{
|
|
1229
|
+
{
|
|
1230
|
+
identityKey: externalToolCallId(name, args),
|
|
1231
|
+
isError: result.isError,
|
|
1232
|
+
},
|
|
1105
1233
|
],
|
|
1106
1234
|
ordinal: bundle.writeOrdinal,
|
|
1107
1235
|
roundId: undefined,
|
|
@@ -1118,6 +1246,95 @@ export class ControllerCoordinatorHandler {
|
|
|
1118
1246
|
budget.dispose();
|
|
1119
1247
|
}
|
|
1120
1248
|
}
|
|
1249
|
+
// -- Wait steps (served by the controller itself) -----------------------
|
|
1250
|
+
/** Serve a `type: 'wait'` step: the controller waits itself — no executor,
|
|
1251
|
+
* no reviewer, no MCP, no tokens. A fresh wait clamps the planner duration by
|
|
1252
|
+
* the engine wait bounds, charges `waitMsUsed`, and persists the deadline
|
|
1253
|
+
* (`waitStartedAt` + `appliedWaitMs`) BEFORE sleeping, so a crash mid-sleep
|
|
1254
|
+
* resumes against a FIXED deadline. Returns 'served' when the step settled and
|
|
1255
|
+
* the loop should continue, 'aborted' when the wait was cancelled (deadline
|
|
1256
|
+
* stays persisted, no artifact, no advance — the next resume serves the
|
|
1257
|
+
* remainder), or 'not-a-wait' so the caller falls through to `runStep`. */
|
|
1258
|
+
async serveWaitStep(args) {
|
|
1259
|
+
const { bundle, step, cfg } = args;
|
|
1260
|
+
if (!isWaitStep(step) || !bundle.inFlightStep)
|
|
1261
|
+
return 'not-a-wait';
|
|
1262
|
+
// ONE clock read for BOTH the plan decision and the persisted waitStartedAt,
|
|
1263
|
+
// so the deadline is not skewed by two Date.now() reads (and clock injection
|
|
1264
|
+
// works in tests). deps.now returns ISO → parse to epoch ms.
|
|
1265
|
+
const waitNow = Date.parse(args.nowIso());
|
|
1266
|
+
const plan = planWait({
|
|
1267
|
+
step,
|
|
1268
|
+
inFlight: bundle.inFlightStep,
|
|
1269
|
+
maxWaitMs: cfg.maxWaitMs ?? 600_000,
|
|
1270
|
+
maxTotalWaitMs: cfg.maxTotalWaitMs ?? 1_800_000,
|
|
1271
|
+
waitMsUsed: bundle.budgets.waitMsUsed ?? 0,
|
|
1272
|
+
now: waitNow,
|
|
1273
|
+
});
|
|
1274
|
+
// Torn write (exactly one deadline field persisted) → a control-failure +
|
|
1275
|
+
// replan, handled BEFORE any describeWait/settling path (describeWait must
|
|
1276
|
+
// never render a torn plan). Mirror cutControlFailure's bookkeeping exactly.
|
|
1277
|
+
if (plan.kind === 'torn') {
|
|
1278
|
+
const reason = `wait deadline half-written: missing ${plan.missing}`;
|
|
1279
|
+
bundle.budgets.stepsUsed++;
|
|
1280
|
+
await this.writeWaitArtifact(args, 'failed', `Wait deadline half-written: missing ${plan.missing}. The step never ran.`, reason);
|
|
1281
|
+
bundle.plannerPrivate += `\n[seq ${bundle.inFlightStep.seq} ${step.name} control-failed] ${reason}`;
|
|
1282
|
+
bundle.inFlightStep.controlFailure = {
|
|
1283
|
+
reason: 'control-failure', // generic; NOT widened for waits
|
|
1284
|
+
seq: bundle.inFlightStep.seq,
|
|
1285
|
+
};
|
|
1286
|
+
await settleStep(this.deps.backend, args.sessionId, bundle, 'failed', args.onCommit);
|
|
1287
|
+
return 'served'; // planner replans
|
|
1288
|
+
}
|
|
1289
|
+
if (plan.kind === 'fresh') {
|
|
1290
|
+
bundle.budgets.waitMsUsed =
|
|
1291
|
+
(bundle.budgets.waitMsUsed ?? 0) + plan.applied;
|
|
1292
|
+
bundle.inFlightStep.waitStartedAt = waitNow; // SAME reading as planWait
|
|
1293
|
+
bundle.inFlightStep.appliedWaitMs = plan.applied;
|
|
1294
|
+
// Durable BEFORE the sleep — one extra write, on wait steps only.
|
|
1295
|
+
await persistBundle(this.deps.backend, args.sessionId, bundle);
|
|
1296
|
+
}
|
|
1297
|
+
const toSleep = plan.kind === 'fresh' ? plan.applied : plan.remaining;
|
|
1298
|
+
const waiter = this.deps.waitStrategy ?? new DefaultWaitStrategy();
|
|
1299
|
+
const outcome = await waiter.wait(toSleep, args.ctx.options?.signal);
|
|
1300
|
+
if (outcome === 'aborted')
|
|
1301
|
+
return 'aborted'; // no artifact, no advance
|
|
1302
|
+
const { text, note } = describeWait(plan, step);
|
|
1303
|
+
await this.writeWaitArtifact(args, 'ok', text, note);
|
|
1304
|
+
bundle.budgets.stepsUsed++;
|
|
1305
|
+
recordStepControl(bundle, {
|
|
1306
|
+
seq: bundle.inFlightStep.seq,
|
|
1307
|
+
name: step.name,
|
|
1308
|
+
status: 'ok',
|
|
1309
|
+
note,
|
|
1310
|
+
remainder: '',
|
|
1311
|
+
});
|
|
1312
|
+
await settleStep(this.deps.backend, args.sessionId, bundle, 'advanced', args.onCommit);
|
|
1313
|
+
return 'served';
|
|
1314
|
+
}
|
|
1315
|
+
/** Persist a `wait` step's step-result artifact using the same metadata shape
|
|
1316
|
+
* the executed-step path uses. `content` is NEVER empty (an empty-content 'ok'
|
|
1317
|
+
* risks surfacing as a blank executed step). Owns its own write because
|
|
1318
|
+
* runStep's `writeControlFailure` is local to that method. */
|
|
1319
|
+
async writeWaitArtifact(args, status, text, note) {
|
|
1320
|
+
const { bundle, step } = args;
|
|
1321
|
+
bundle.writeOrdinal = (bundle.writeOrdinal ?? 0) + 1;
|
|
1322
|
+
await writeArtifact(args.rag, {
|
|
1323
|
+
...args.meta,
|
|
1324
|
+
artifactType: 'step-result',
|
|
1325
|
+
task: step.name,
|
|
1326
|
+
runId: bundle.runId,
|
|
1327
|
+
seq: bundle.inFlightStep?.seq ?? 0,
|
|
1328
|
+
attempt: bundle.inFlightStep?.attempt ?? 0,
|
|
1329
|
+
status,
|
|
1330
|
+
note,
|
|
1331
|
+
remainder: '',
|
|
1332
|
+
stepId: step.stepId,
|
|
1333
|
+
digest: text.slice(0, this.deps.config.budgets.maxDigestChars ?? 500),
|
|
1334
|
+
writeOrdinal: bundle.writeOrdinal,
|
|
1335
|
+
content: text,
|
|
1336
|
+
}, args.ctx.options);
|
|
1337
|
+
}
|
|
1121
1338
|
// -- Escalation & surfacing (mirror StepperCoordinatorHandler) ----------
|
|
1122
1339
|
async escalate(ctx, sessionId, bundle, question, usage) {
|
|
1123
1340
|
bundle.pending = { kind: 'clarify', question, position: 'loop' };
|
|
@@ -1194,6 +1411,7 @@ export class ControllerCoordinatorHandler {
|
|
|
1194
1411
|
logUsage,
|
|
1195
1412
|
log: (m) => dlog(m),
|
|
1196
1413
|
skillsBlock,
|
|
1414
|
+
callOptions: diagnosticCallOptions(ctx.options),
|
|
1197
1415
|
});
|
|
1198
1416
|
// Empty-but-ok finalizer output is a JUDGE failure (spec), not a valid
|
|
1199
1417
|
// answer → throw so it retries within maxFinalizeRetries.
|