@mcp-abap-adt/llm-agent-server-libs 20.6.0 → 20.7.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.
- 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 +246 -34
- 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 +25 -1
- package/dist/smart-agent/controller/types.d.ts.map +1 -1
- 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).
|
|
@@ -814,7 +885,8 @@ export class ControllerCoordinatorHandler {
|
|
|
814
885
|
seq: inFlight.seq,
|
|
815
886
|
};
|
|
816
887
|
}
|
|
817
|
-
|
|
888
|
+
logDecision(ctx, 'control-failure', noteFor(reason));
|
|
889
|
+
return settle('failed', noteFor(reason));
|
|
818
890
|
};
|
|
819
891
|
// Inner loop handles tool routing / error retries until the executor
|
|
820
892
|
// produces content for this step (or the step suspends on an external tool).
|
|
@@ -855,6 +927,7 @@ export class ControllerCoordinatorHandler {
|
|
|
855
927
|
hint: deps.config.subagents.reviewer?.hint,
|
|
856
928
|
logUsage,
|
|
857
929
|
maxDigestChars: cfg.maxDigestChars ?? 500,
|
|
930
|
+
callOptions: diagnosticCallOptions(ctx.options),
|
|
858
931
|
})
|
|
859
932
|
: {
|
|
860
933
|
kind: 'outcome',
|
|
@@ -878,14 +951,19 @@ export class ControllerCoordinatorHandler {
|
|
|
878
951
|
// planner replans, rather than aborting the whole run — the terminal
|
|
879
952
|
// backstop is maxStepAttempts/maxSteps, not a single unverifiable verdict.
|
|
880
953
|
bundle.budgets.stepsUsed++;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
954
|
+
const unverifiableReason = `reviewer unverifiable after ${cfg.maxReviewRetries ?? 2} retries: ${review.reason}`;
|
|
955
|
+
await writeControlFailure(unverifiableReason);
|
|
956
|
+
bundle.plannerPrivate += `\n[seq ${bundle.inFlightStep?.seq ?? bundle.nextSeq ?? 0} ${step.name} failed] ${unverifiableReason}`;
|
|
957
|
+
logDecision(ctx, 'reviewer-unverifiable', unverifiableReason, {
|
|
958
|
+
retries: reviewRetries,
|
|
959
|
+
});
|
|
960
|
+
return settle('failed', unverifiableReason);
|
|
884
961
|
}
|
|
885
962
|
review = await deps.reviewer.review(step, evidence, res.content, {
|
|
886
963
|
hint: deps.config.subagents.reviewer?.hint,
|
|
887
964
|
logUsage,
|
|
888
965
|
maxDigestChars: cfg.maxDigestChars ?? 500,
|
|
966
|
+
callOptions: diagnosticCallOptions(ctx.options),
|
|
889
967
|
});
|
|
890
968
|
}
|
|
891
969
|
const outcome = review.outcome;
|
|
@@ -917,6 +995,13 @@ export class ControllerCoordinatorHandler {
|
|
|
917
995
|
note: outcome.note,
|
|
918
996
|
remainder: outcome.remainder,
|
|
919
997
|
});
|
|
998
|
+
if (mapped === 'failed') {
|
|
999
|
+
const rejectReason = outcome.note || `reviewer rejected step "${step.name}"`;
|
|
1000
|
+
logDecision(ctx, 'reviewer-reject', rejectReason, {
|
|
1001
|
+
status: outcome.status,
|
|
1002
|
+
});
|
|
1003
|
+
return settle(mapped, rejectReason);
|
|
1004
|
+
}
|
|
920
1005
|
return settle(mapped);
|
|
921
1006
|
}
|
|
922
1007
|
if (res.kind === 'error') {
|
|
@@ -932,9 +1017,10 @@ export class ControllerCoordinatorHandler {
|
|
|
932
1017
|
// Retries exhausted — feed the error back as the step result so the
|
|
933
1018
|
// planner can replan on the next iteration.
|
|
934
1019
|
bundle.budgets.stepsUsed++;
|
|
935
|
-
|
|
1020
|
+
const executorErrorReason = `executor error: ${res.error}`;
|
|
1021
|
+
await writeControlFailure(executorErrorReason);
|
|
936
1022
|
bundle.plannerPrivate += `\n[step ${step.name} failed] ${res.error}`;
|
|
937
|
-
return settle('failed');
|
|
1023
|
+
return settle('failed', executorErrorReason);
|
|
938
1024
|
}
|
|
939
1025
|
// res.kind === 'tool_call' → route the FIRST tool call.
|
|
940
1026
|
const firstCall = res.toolCalls[0];
|
|
@@ -952,7 +1038,7 @@ export class ControllerCoordinatorHandler {
|
|
|
952
1038
|
bundle.budgets.stepsUsed++;
|
|
953
1039
|
await writeControlFailure('empty tool call');
|
|
954
1040
|
bundle.plannerPrivate += `\n[step ${step.name} failed] empty tool call`;
|
|
955
|
-
return settle('failed');
|
|
1041
|
+
return settle('failed', `step "${step.name}" produced an empty tool call`);
|
|
956
1042
|
}
|
|
957
1043
|
// Normalize the StreamToolCall (full or delta) into an LlmToolCall inline.
|
|
958
1044
|
const call = 'arguments' in firstCall &&
|
|
@@ -1024,9 +1110,10 @@ export class ControllerCoordinatorHandler {
|
|
|
1024
1110
|
continue;
|
|
1025
1111
|
}
|
|
1026
1112
|
bundle.budgets.stepsUsed++;
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1113
|
+
const unavailableToolReason = `requested unavailable tool ${name}`;
|
|
1114
|
+
await writeControlFailure(unavailableToolReason);
|
|
1115
|
+
bundle.plannerPrivate += `\n[step ${step.name} failed] ${unavailableToolReason}`;
|
|
1116
|
+
return settle('failed', unavailableToolReason);
|
|
1030
1117
|
}
|
|
1031
1118
|
// Prospective count gate BEFORE the increment (the before-increment model:
|
|
1032
1119
|
// the increment happens only after canExecuteTool allows the call).
|
|
@@ -1049,10 +1136,18 @@ export class ControllerCoordinatorHandler {
|
|
|
1049
1136
|
// decision → rethrow → outer catch swallow → (no response)). A non-McpError
|
|
1050
1137
|
// is a genuine unexpected error and is re-thrown for the outer handler.
|
|
1051
1138
|
let result;
|
|
1139
|
+
const mcpCallStartedAt = Date.now();
|
|
1052
1140
|
try {
|
|
1053
1141
|
result = await deps.callMcp(name, args, callSignal);
|
|
1054
1142
|
}
|
|
1055
1143
|
catch (mcpErr) {
|
|
1144
|
+
ctx.options?.sessionLogger?.logStep('mcp_tool_call', {
|
|
1145
|
+
name,
|
|
1146
|
+
args,
|
|
1147
|
+
result: undefined,
|
|
1148
|
+
isError: true,
|
|
1149
|
+
durationMs: Date.now() - mcpCallStartedAt,
|
|
1150
|
+
}, 'mcp');
|
|
1056
1151
|
// A step-timeout cancellation aborts the merged signal → the bridge rejects.
|
|
1057
1152
|
// Map that to a step-timeout control-failure BEFORE the McpError escalate so
|
|
1058
1153
|
// it is NOT mis-classified as MCP-unavailable (20.4.0 escalate order is
|
|
@@ -1067,6 +1162,28 @@ export class ControllerCoordinatorHandler {
|
|
|
1067
1162
|
}
|
|
1068
1163
|
throw mcpErr;
|
|
1069
1164
|
}
|
|
1165
|
+
ctx.options?.sessionLogger?.logStep('mcp_tool_call', {
|
|
1166
|
+
name,
|
|
1167
|
+
args,
|
|
1168
|
+
result: result.text,
|
|
1169
|
+
// The REAL tool-level isError, threaded through the bridge (#213).
|
|
1170
|
+
// Previously hardcoded false, so a locked-object error looked like a
|
|
1171
|
+
// delivered result and the executor retried it forever.
|
|
1172
|
+
isError: result.isError,
|
|
1173
|
+
durationMs: Date.now() - mcpCallStartedAt,
|
|
1174
|
+
}, 'mcp');
|
|
1175
|
+
// #213 immediate cut: a delivered tool-level error ends the step NOW.
|
|
1176
|
+
// The executor tool-loop does NOT continue (no further tool call, no
|
|
1177
|
+
// reviewer for this step); reuse cutControlFailure so the step settles
|
|
1178
|
+
// 'failed' with the tool's error text and the planner replans / surfaces
|
|
1179
|
+
// it. Read result.isError directly here — BEFORE the round reaches the
|
|
1180
|
+
// context strategy — so no Message/meta replay is needed. The durable
|
|
1181
|
+
// failed step-result + plannerPrivate note (written by cutControlFailure)
|
|
1182
|
+
// ARE the resume carrier; the mcp-result artifact is intentionally not
|
|
1183
|
+
// relied on (the cut may never call strategy.record).
|
|
1184
|
+
if (result.isError) {
|
|
1185
|
+
return cutControlFailure(result.text);
|
|
1186
|
+
}
|
|
1070
1187
|
// Record this exchange as a coherent assistant→tool ROUND (OpenAI protocol)
|
|
1071
1188
|
// via the context strategy so the executor LLM continues from its own tool
|
|
1072
1189
|
// call. The strategy owns the per-round context (Window keeps a bounded
|
|
@@ -1094,14 +1211,19 @@ export class ControllerCoordinatorHandler {
|
|
|
1094
1211
|
{
|
|
1095
1212
|
role: 'tool',
|
|
1096
1213
|
tool_call_id: call.id,
|
|
1097
|
-
content: result,
|
|
1214
|
+
content: result.text,
|
|
1098
1215
|
},
|
|
1099
1216
|
],
|
|
1100
|
-
// Stable fetch identity (tool+args) for run-scoped recall dedup
|
|
1101
|
-
//
|
|
1102
|
-
// server aborts BEFORE record; a returned
|
|
1217
|
+
// Stable fetch identity (tool+args) for run-scoped recall dedup, plus
|
|
1218
|
+
// the tool-level isError threaded from the bridge (#213). An unavailable
|
|
1219
|
+
// MCP server still aborts BEFORE record; a returned result carries its
|
|
1220
|
+
// real isError so the executor sees a failed call as failed, not as a
|
|
1221
|
+
// delivered success it retries forever.
|
|
1103
1222
|
meta: [
|
|
1104
|
-
{
|
|
1223
|
+
{
|
|
1224
|
+
identityKey: externalToolCallId(name, args),
|
|
1225
|
+
isError: result.isError,
|
|
1226
|
+
},
|
|
1105
1227
|
],
|
|
1106
1228
|
ordinal: bundle.writeOrdinal,
|
|
1107
1229
|
roundId: undefined,
|
|
@@ -1118,6 +1240,95 @@ export class ControllerCoordinatorHandler {
|
|
|
1118
1240
|
budget.dispose();
|
|
1119
1241
|
}
|
|
1120
1242
|
}
|
|
1243
|
+
// -- Wait steps (served by the controller itself) -----------------------
|
|
1244
|
+
/** Serve a `type: 'wait'` step: the controller waits itself — no executor,
|
|
1245
|
+
* no reviewer, no MCP, no tokens. A fresh wait clamps the planner duration by
|
|
1246
|
+
* the engine wait bounds, charges `waitMsUsed`, and persists the deadline
|
|
1247
|
+
* (`waitStartedAt` + `appliedWaitMs`) BEFORE sleeping, so a crash mid-sleep
|
|
1248
|
+
* resumes against a FIXED deadline. Returns 'served' when the step settled and
|
|
1249
|
+
* the loop should continue, 'aborted' when the wait was cancelled (deadline
|
|
1250
|
+
* stays persisted, no artifact, no advance — the next resume serves the
|
|
1251
|
+
* remainder), or 'not-a-wait' so the caller falls through to `runStep`. */
|
|
1252
|
+
async serveWaitStep(args) {
|
|
1253
|
+
const { bundle, step, cfg } = args;
|
|
1254
|
+
if (!isWaitStep(step) || !bundle.inFlightStep)
|
|
1255
|
+
return 'not-a-wait';
|
|
1256
|
+
// ONE clock read for BOTH the plan decision and the persisted waitStartedAt,
|
|
1257
|
+
// so the deadline is not skewed by two Date.now() reads (and clock injection
|
|
1258
|
+
// works in tests). deps.now returns ISO → parse to epoch ms.
|
|
1259
|
+
const waitNow = Date.parse(args.nowIso());
|
|
1260
|
+
const plan = planWait({
|
|
1261
|
+
step,
|
|
1262
|
+
inFlight: bundle.inFlightStep,
|
|
1263
|
+
maxWaitMs: cfg.maxWaitMs ?? 600_000,
|
|
1264
|
+
maxTotalWaitMs: cfg.maxTotalWaitMs ?? 1_800_000,
|
|
1265
|
+
waitMsUsed: bundle.budgets.waitMsUsed ?? 0,
|
|
1266
|
+
now: waitNow,
|
|
1267
|
+
});
|
|
1268
|
+
// Torn write (exactly one deadline field persisted) → a control-failure +
|
|
1269
|
+
// replan, handled BEFORE any describeWait/settling path (describeWait must
|
|
1270
|
+
// never render a torn plan). Mirror cutControlFailure's bookkeeping exactly.
|
|
1271
|
+
if (plan.kind === 'torn') {
|
|
1272
|
+
const reason = `wait deadline half-written: missing ${plan.missing}`;
|
|
1273
|
+
bundle.budgets.stepsUsed++;
|
|
1274
|
+
await this.writeWaitArtifact(args, 'failed', `Wait deadline half-written: missing ${plan.missing}. The step never ran.`, reason);
|
|
1275
|
+
bundle.plannerPrivate += `\n[seq ${bundle.inFlightStep.seq} ${step.name} control-failed] ${reason}`;
|
|
1276
|
+
bundle.inFlightStep.controlFailure = {
|
|
1277
|
+
reason: 'control-failure', // generic; NOT widened for waits
|
|
1278
|
+
seq: bundle.inFlightStep.seq,
|
|
1279
|
+
};
|
|
1280
|
+
await settleStep(this.deps.backend, args.sessionId, bundle, 'failed', args.onCommit);
|
|
1281
|
+
return 'served'; // planner replans
|
|
1282
|
+
}
|
|
1283
|
+
if (plan.kind === 'fresh') {
|
|
1284
|
+
bundle.budgets.waitMsUsed =
|
|
1285
|
+
(bundle.budgets.waitMsUsed ?? 0) + plan.applied;
|
|
1286
|
+
bundle.inFlightStep.waitStartedAt = waitNow; // SAME reading as planWait
|
|
1287
|
+
bundle.inFlightStep.appliedWaitMs = plan.applied;
|
|
1288
|
+
// Durable BEFORE the sleep — one extra write, on wait steps only.
|
|
1289
|
+
await persistBundle(this.deps.backend, args.sessionId, bundle);
|
|
1290
|
+
}
|
|
1291
|
+
const toSleep = plan.kind === 'fresh' ? plan.applied : plan.remaining;
|
|
1292
|
+
const waiter = this.deps.waitStrategy ?? new DefaultWaitStrategy();
|
|
1293
|
+
const outcome = await waiter.wait(toSleep, args.ctx.options?.signal);
|
|
1294
|
+
if (outcome === 'aborted')
|
|
1295
|
+
return 'aborted'; // no artifact, no advance
|
|
1296
|
+
const { text, note } = describeWait(plan, step);
|
|
1297
|
+
await this.writeWaitArtifact(args, 'ok', text, note);
|
|
1298
|
+
bundle.budgets.stepsUsed++;
|
|
1299
|
+
recordStepControl(bundle, {
|
|
1300
|
+
seq: bundle.inFlightStep.seq,
|
|
1301
|
+
name: step.name,
|
|
1302
|
+
status: 'ok',
|
|
1303
|
+
note,
|
|
1304
|
+
remainder: '',
|
|
1305
|
+
});
|
|
1306
|
+
await settleStep(this.deps.backend, args.sessionId, bundle, 'advanced', args.onCommit);
|
|
1307
|
+
return 'served';
|
|
1308
|
+
}
|
|
1309
|
+
/** Persist a `wait` step's step-result artifact using the same metadata shape
|
|
1310
|
+
* the executed-step path uses. `content` is NEVER empty (an empty-content 'ok'
|
|
1311
|
+
* risks surfacing as a blank executed step). Owns its own write because
|
|
1312
|
+
* runStep's `writeControlFailure` is local to that method. */
|
|
1313
|
+
async writeWaitArtifact(args, status, text, note) {
|
|
1314
|
+
const { bundle, step } = args;
|
|
1315
|
+
bundle.writeOrdinal = (bundle.writeOrdinal ?? 0) + 1;
|
|
1316
|
+
await writeArtifact(args.rag, {
|
|
1317
|
+
...args.meta,
|
|
1318
|
+
artifactType: 'step-result',
|
|
1319
|
+
task: step.name,
|
|
1320
|
+
runId: bundle.runId,
|
|
1321
|
+
seq: bundle.inFlightStep?.seq ?? 0,
|
|
1322
|
+
attempt: bundle.inFlightStep?.attempt ?? 0,
|
|
1323
|
+
status,
|
|
1324
|
+
note,
|
|
1325
|
+
remainder: '',
|
|
1326
|
+
stepId: step.stepId,
|
|
1327
|
+
digest: text.slice(0, this.deps.config.budgets.maxDigestChars ?? 500),
|
|
1328
|
+
writeOrdinal: bundle.writeOrdinal,
|
|
1329
|
+
content: text,
|
|
1330
|
+
}, args.ctx.options);
|
|
1331
|
+
}
|
|
1121
1332
|
// -- Escalation & surfacing (mirror StepperCoordinatorHandler) ----------
|
|
1122
1333
|
async escalate(ctx, sessionId, bundle, question, usage) {
|
|
1123
1334
|
bundle.pending = { kind: 'clarify', question, position: 'loop' };
|
|
@@ -1194,6 +1405,7 @@ export class ControllerCoordinatorHandler {
|
|
|
1194
1405
|
logUsage,
|
|
1195
1406
|
log: (m) => dlog(m),
|
|
1196
1407
|
skillsBlock,
|
|
1408
|
+
callOptions: diagnosticCallOptions(ctx.options),
|
|
1197
1409
|
});
|
|
1198
1410
|
// Empty-but-ok finalizer output is a JUDGE failure (spec), not a valid
|
|
1199
1411
|
// answer → throw so it retries within maxFinalizeRetries.
|