agent-nuvira 3.3.10 → 3.3.12
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 +32 -1
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +34 -12
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/cli/chat.d.ts +36 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +440 -44
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/config.d.ts +28 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +169 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/eval.d.ts +8 -0
- package/dist/cli/eval.d.ts.map +1 -1
- package/dist/cli/eval.js +64 -0
- package/dist/cli/eval.js.map +1 -1
- package/dist/cli/execute.d.ts.map +1 -1
- package/dist/cli/execute.js +8 -0
- package/dist/cli/execute.js.map +1 -1
- package/dist/cli/loop-executor.d.ts +13 -0
- package/dist/cli/loop-executor.d.ts.map +1 -1
- package/dist/cli/loop-executor.js +27 -18
- package/dist/cli/loop-executor.js.map +1 -1
- package/dist/cli/tool-install-prompt.d.ts +58 -0
- package/dist/cli/tool-install-prompt.d.ts.map +1 -1
- package/dist/cli/tool-install-prompt.js +137 -0
- package/dist/cli/tool-install-prompt.js.map +1 -1
- package/dist/cli/weak-model-prompt.d.ts +16 -7
- package/dist/cli/weak-model-prompt.d.ts.map +1 -1
- package/dist/cli/weak-model-prompt.js +24 -8
- package/dist/cli/weak-model-prompt.js.map +1 -1
- package/dist/config/capability-mode.d.ts +122 -0
- package/dist/config/capability-mode.d.ts.map +1 -0
- package/dist/config/capability-mode.js +132 -0
- package/dist/config/capability-mode.js.map +1 -0
- package/dist/config/limits.d.ts +47 -0
- package/dist/config/limits.d.ts.map +1 -0
- package/dist/config/limits.js +64 -0
- package/dist/config/limits.js.map +1 -0
- package/dist/config/process-env.d.ts.map +1 -1
- package/dist/config/process-env.js +44 -0
- package/dist/config/process-env.js.map +1 -1
- package/dist/config/types.d.ts +113 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/work-digest.d.ts +46 -0
- package/dist/config/work-digest.d.ts.map +1 -0
- package/dist/config/work-digest.js +80 -0
- package/dist/config/work-digest.js.map +1 -0
- package/dist/context/cache.d.ts +35 -1
- package/dist/context/cache.d.ts.map +1 -1
- package/dist/context/cache.js +31 -2
- package/dist/context/cache.js.map +1 -1
- package/dist/gateway/inbound-media.d.ts +18 -0
- package/dist/gateway/inbound-media.d.ts.map +1 -1
- package/dist/gateway/inbound-media.js +96 -2
- package/dist/gateway/inbound-media.js.map +1 -1
- package/dist/inference/anthropic-adapter.d.ts +6 -0
- package/dist/inference/anthropic-adapter.d.ts.map +1 -1
- package/dist/inference/anthropic-adapter.js +79 -8
- package/dist/inference/anthropic-adapter.js.map +1 -1
- package/dist/inference/gemini-adapter.d.ts +6 -0
- package/dist/inference/gemini-adapter.d.ts.map +1 -1
- package/dist/inference/gemini-adapter.js +73 -8
- package/dist/inference/gemini-adapter.js.map +1 -1
- package/dist/inference/groq-adapter.d.ts +3 -0
- package/dist/inference/groq-adapter.d.ts.map +1 -1
- package/dist/inference/groq-adapter.js +83 -40
- package/dist/inference/groq-adapter.js.map +1 -1
- package/dist/inference/interface.d.ts +7 -0
- package/dist/inference/interface.d.ts.map +1 -1
- package/dist/inference/model-catalog.d.ts +17 -0
- package/dist/inference/model-catalog.d.ts.map +1 -1
- package/dist/inference/model-catalog.js +42 -7
- package/dist/inference/model-catalog.js.map +1 -1
- package/dist/inference/model-probe.d.ts +17 -0
- package/dist/inference/model-probe.d.ts.map +1 -1
- package/dist/inference/model-probe.js +58 -1
- package/dist/inference/model-probe.js.map +1 -1
- package/dist/inference/model-validator.d.ts +16 -1
- package/dist/inference/model-validator.d.ts.map +1 -1
- package/dist/inference/model-validator.js +83 -2
- package/dist/inference/model-validator.js.map +1 -1
- package/dist/inference/nim-adapter.js +4 -4
- package/dist/inference/nim-adapter.js.map +1 -1
- package/dist/inference/openai-compat-adapter.d.ts +9 -0
- package/dist/inference/openai-compat-adapter.d.ts.map +1 -1
- package/dist/inference/openai-compat-adapter.js +101 -51
- package/dist/inference/openai-compat-adapter.js.map +1 -1
- package/dist/inference/openrouter-adapter.d.ts +3 -0
- package/dist/inference/openrouter-adapter.d.ts.map +1 -1
- package/dist/inference/openrouter-adapter.js +134 -57
- package/dist/inference/openrouter-adapter.js.map +1 -1
- package/dist/inference/reasoning-effort.d.ts +130 -0
- package/dist/inference/reasoning-effort.d.ts.map +1 -0
- package/dist/inference/reasoning-effort.js +237 -0
- package/dist/inference/reasoning-effort.js.map +1 -0
- package/dist/inference/route-resolver.d.ts +7 -0
- package/dist/inference/route-resolver.d.ts.map +1 -1
- package/dist/inference/route-resolver.js +2 -1
- package/dist/inference/route-resolver.js.map +1 -1
- package/dist/inference/sse.d.ts +6 -0
- package/dist/inference/sse.d.ts.map +1 -1
- package/dist/inference/sse.js +1 -0
- package/dist/inference/sse.js.map +1 -1
- package/dist/inference/tool-call-utils.d.ts +4 -3
- package/dist/inference/tool-call-utils.d.ts.map +1 -1
- package/dist/inference/tool-call-utils.js +16 -4
- package/dist/inference/tool-call-utils.js.map +1 -1
- package/dist/inference/tools.d.ts +13 -0
- package/dist/inference/tools.d.ts.map +1 -1
- package/dist/inference/tools.js +20 -2
- package/dist/inference/tools.js.map +1 -1
- package/dist/learning/agentic-route-gate.d.ts +104 -0
- package/dist/learning/agentic-route-gate.d.ts.map +1 -0
- package/dist/learning/agentic-route-gate.js +125 -0
- package/dist/learning/agentic-route-gate.js.map +1 -0
- package/dist/learning/auto-router.d.ts +36 -0
- package/dist/learning/auto-router.d.ts.map +1 -1
- package/dist/learning/auto-router.js +115 -8
- package/dist/learning/auto-router.js.map +1 -1
- package/dist/learning/autonomy-policy.d.ts +8 -0
- package/dist/learning/autonomy-policy.d.ts.map +1 -1
- package/dist/learning/autonomy-policy.js +34 -0
- package/dist/learning/autonomy-policy.js.map +1 -1
- package/dist/learning/build-prerequisites.d.ts +68 -0
- package/dist/learning/build-prerequisites.d.ts.map +1 -0
- package/dist/learning/build-prerequisites.js +267 -0
- package/dist/learning/build-prerequisites.js.map +1 -0
- package/dist/learning/capability-parity.d.ts +108 -0
- package/dist/learning/capability-parity.d.ts.map +1 -0
- package/dist/learning/capability-parity.js +154 -0
- package/dist/learning/capability-parity.js.map +1 -0
- package/dist/learning/continuation-intent.d.ts +43 -0
- package/dist/learning/continuation-intent.d.ts.map +1 -0
- package/dist/learning/continuation-intent.js +82 -0
- package/dist/learning/continuation-intent.js.map +1 -0
- package/dist/learning/cost-tracker.d.ts +28 -4
- package/dist/learning/cost-tracker.d.ts.map +1 -1
- package/dist/learning/cost-tracker.js +58 -8
- package/dist/learning/cost-tracker.js.map +1 -1
- package/dist/learning/eval-framework.d.ts +17 -0
- package/dist/learning/eval-framework.d.ts.map +1 -1
- package/dist/learning/eval-framework.js +63 -2
- package/dist/learning/eval-framework.js.map +1 -1
- package/dist/learning/model-capability.d.ts +94 -0
- package/dist/learning/model-capability.d.ts.map +1 -0
- package/dist/learning/model-capability.js +172 -0
- package/dist/learning/model-capability.js.map +1 -0
- package/dist/learning/model-harness.d.ts +19 -0
- package/dist/learning/model-harness.d.ts.map +1 -1
- package/dist/learning/model-harness.js +27 -0
- package/dist/learning/model-harness.js.map +1 -1
- package/dist/learning/model-registry.d.ts +33 -0
- package/dist/learning/model-registry.d.ts.map +1 -1
- package/dist/learning/model-registry.js +79 -0
- package/dist/learning/model-registry.js.map +1 -1
- package/dist/learning/prompt-budget.d.ts +91 -0
- package/dist/learning/prompt-budget.d.ts.map +1 -0
- package/dist/learning/prompt-budget.js +99 -0
- package/dist/learning/prompt-budget.js.map +1 -0
- package/dist/learning/reasoning-trace.d.ts +55 -2
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +48 -0
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/learning/resilient-call.d.ts.map +1 -1
- package/dist/learning/resilient-call.js +16 -3
- package/dist/learning/resilient-call.js.map +1 -1
- package/dist/learning/resolve-options.d.ts.map +1 -1
- package/dist/learning/resolve-options.js +16 -3
- package/dist/learning/resolve-options.js.map +1 -1
- package/dist/learning/routing-history.d.ts +13 -0
- package/dist/learning/routing-history.d.ts.map +1 -1
- package/dist/learning/routing-history.js.map +1 -1
- package/dist/learning/run-trace.d.ts +68 -0
- package/dist/learning/run-trace.d.ts.map +1 -1
- package/dist/learning/run-trace.js +101 -0
- package/dist/learning/run-trace.js.map +1 -1
- package/dist/learning/turn-report.d.ts +78 -0
- package/dist/learning/turn-report.d.ts.map +1 -0
- package/dist/learning/turn-report.js +140 -0
- package/dist/learning/turn-report.js.map +1 -0
- package/dist/tools/edit-verification.d.ts +2 -11
- package/dist/tools/edit-verification.d.ts.map +1 -1
- package/dist/tools/edit-verification.js +33 -1
- package/dist/tools/edit-verification.js.map +1 -1
- package/dist/tools/loop-skill-hint.d.ts +78 -0
- package/dist/tools/loop-skill-hint.d.ts.map +1 -1
- package/dist/tools/loop-skill-hint.js +274 -5
- package/dist/tools/loop-skill-hint.js.map +1 -1
- package/dist/tools/plan-store.d.ts +104 -3
- package/dist/tools/plan-store.d.ts.map +1 -1
- package/dist/tools/plan-store.js +250 -6
- package/dist/tools/plan-store.js.map +1 -1
- package/dist/tools/read-extract.d.ts +9 -3
- package/dist/tools/read-extract.d.ts.map +1 -1
- package/dist/tools/read-extract.js +70 -25
- package/dist/tools/read-extract.js.map +1 -1
- package/dist/tools/registry.d.ts +20 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +17 -4
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/run-terminal.d.ts.map +1 -1
- package/dist/tools/run-terminal.js +34 -0
- package/dist/tools/run-terminal.js.map +1 -1
- package/dist/tools/tool-loop.d.ts +149 -0
- package/dist/tools/tool-loop.d.ts.map +1 -1
- package/dist/tools/tool-loop.js +706 -13
- package/dist/tools/tool-loop.js.map +1 -1
- package/dist/utils/effect-verification.js +1 -1
- package/dist/utils/effect-verification.js.map +1 -1
- package/dist/web-dashboard/attachment-extract.d.ts +8 -1
- package/dist/web-dashboard/attachment-extract.d.ts.map +1 -1
- package/dist/web-dashboard/attachment-extract.js +14 -3
- package/dist/web-dashboard/attachment-extract.js.map +1 -1
- package/dist/web-dashboard/chat-console.d.ts +21 -1
- package/dist/web-dashboard/chat-console.d.ts.map +1 -1
- package/dist/web-dashboard/chat-console.js +33 -5
- package/dist/web-dashboard/chat-console.js.map +1 -1
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +198 -21
- package/dist/web-dashboard/server.js.map +1 -1
- package/dist/web-dashboard/src/types.d.ts +97 -0
- package/dist/web-dashboard/src/types.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.js +44 -4
- package/dist/web-dashboard/workspace-guard.js.map +1 -1
- package/package.json +4 -1
- package/src/web-dashboard/public/assets/index-BrZcYhy6.js +207 -0
- package/src/web-dashboard/public/assets/index-BrZcYhy6.js.map +1 -0
- package/src/web-dashboard/public/assets/index-DsWczTa6.css +1 -0
- package/src/web-dashboard/public/index.html +2 -2
- package/src/web-dashboard/public/assets/index-Beportyl.js +0 -207
- package/src/web-dashboard/public/assets/index-Beportyl.js.map +0 -1
- package/src/web-dashboard/public/assets/index-gtQyg9nm.css +0 -1
package/dist/tools/tool-loop.js
CHANGED
|
@@ -33,15 +33,24 @@ import { stepDigest } from '../learning/step-checkpoint.js';
|
|
|
33
33
|
// process declared no fault (`NUVIRA_INJECT_FAULT`), so an ordinary turn is
|
|
34
34
|
// unaffected.
|
|
35
35
|
import { faultAt } from '../runtime/fault-injection.js';
|
|
36
|
-
import { detectPermissionSeeking, isAffirmativeReply, replyAsksTheReader, requestAuthorizesWrites, stripTrailingPermissionSeek, } from '../learning/autonomy-policy.js';
|
|
36
|
+
import { detectPermissionSeeking, isAffirmativeReply, replyAsksTheReader, requestAuthorizesWrites, requestForbidsWrites, stripTrailingPermissionSeek, } from '../learning/autonomy-policy.js';
|
|
37
37
|
import { envelopeFromPlan, envelopeFromRequest, getEnvelope, grantEnvelope, isEnvelopeKey, } from '../learning/intent-envelope.js';
|
|
38
|
-
import { detectProcessComplaint, isTraceKey, repeatNudge, runTraceFor, RunTrace, } from '../learning/run-trace.js';
|
|
38
|
+
import { detectProcessComplaint, isTraceKey, noProgressNudge, repeatNudge, repeatedFailureNudge, runTraceFor, RunTrace, } from '../learning/run-trace.js';
|
|
39
39
|
import { wantsAuthoredArtifact } from '../learning/deliverable-class.js';
|
|
40
40
|
import { normalizeFollowups } from './followup-utils.js';
|
|
41
|
+
// The capability mode (balanced | max) — `max` widens the loop's own reasoning
|
|
42
|
+
// budget as well as routing, so "cost is not a concern" means the agent may
|
|
43
|
+
// keep working through a long build instead of stopping at the default bound.
|
|
44
|
+
import { isMaxCapability } from '../config/capability-mode.js';
|
|
45
|
+
// The digest's scope (all | max | off) — a user-facing control over whether the
|
|
46
|
+
// compaction digest is injected in every mode, only under `max`, or never.
|
|
47
|
+
import { isWorkDigestEnabled } from '../config/work-digest.js';
|
|
41
48
|
import { assessEditActivity, detectUnverifiedEditClaim, isVerificationTool, verificationNudgeFor, THINK_ONLY_ESCALATION, AUTHORIZED_WORK_NUDGE, deliverableNudge, } from './edit-verification.js';
|
|
42
49
|
import { effectiveToolJsonSchemas, coreToolJsonSchemas, isToolEnabled, toolsetForTool } from './toolsets.js';
|
|
43
50
|
import { deliverablesNamedIn, recordStepHandoff } from '../agents/step-handoff.js';
|
|
44
51
|
import { fenceUntrustedToolOutput } from './untrusted-content.js';
|
|
52
|
+
import { installableToolsFromFailure, resolveInstallCommand, toolTakeoverInstruction, } from '../cli/tool-install-prompt.js';
|
|
53
|
+
import { matchPrerequisiteSignatures, prerequisiteTakeoverInstruction, checkProjectPrerequisites, createNodePrereqFs, formatPreflightFindings, } from '../learning/build-prerequisites.js';
|
|
45
54
|
/**
|
|
46
55
|
* Tools that MUTATE the workspace. A refusal of one of these is the only
|
|
47
56
|
* refusal that leaves work undone — a declined `read_file` costs a step, a
|
|
@@ -54,12 +63,41 @@ const MUTATING_TOOL_NAMES = new Set([
|
|
|
54
63
|
'run_terminal',
|
|
55
64
|
'run_cli',
|
|
56
65
|
]);
|
|
66
|
+
/**
|
|
67
|
+
* The subset of mutating tools the PLAN gate HARD-BLOCKS on the first call —
|
|
68
|
+
* the ones that change the workspace's FILES. `run_terminal` / `run_cli` are
|
|
69
|
+
* deliberately excluded from the BLOCK (they still trigger the nudge): a
|
|
70
|
+
* terminal command is as often a test, a build or an inspection as it is an
|
|
71
|
+
* edit, and refusing it outright would stop the very step a plan is meant to
|
|
72
|
+
* reach. The nudge still tells the model to plan first.
|
|
73
|
+
*/
|
|
74
|
+
const PLAN_GATED_TOOL_NAMES = new Set([
|
|
75
|
+
'write_file',
|
|
76
|
+
'edit_file',
|
|
77
|
+
'propose_change',
|
|
78
|
+
]);
|
|
57
79
|
/** The path a mutating call was aimed at, when it named one. */
|
|
58
80
|
function mutatedPathOf(args) {
|
|
59
81
|
const a = args;
|
|
60
82
|
const p = a?.path ?? a?.file_path ?? a?.file;
|
|
61
83
|
return typeof p === 'string' && p.trim() ? p.trim() : undefined;
|
|
62
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The ACTION signature of a failed call — what it tried to do, so the identical
|
|
87
|
+
* retry is recognizable. Prefers the shell `command`, then the file `path`,
|
|
88
|
+
* then the tool name (a call that named neither still repeats as the same tool).
|
|
89
|
+
* Used by the self-diagnosis gate (see RunTrace.repeatedFailure).
|
|
90
|
+
*/
|
|
91
|
+
function failureActionOf(call) {
|
|
92
|
+
const a = call.arguments;
|
|
93
|
+
const command = typeof a?.command === 'string' && a.command.trim() ? a.command.trim() : undefined;
|
|
94
|
+
if (command)
|
|
95
|
+
return command;
|
|
96
|
+
const path = mutatedPathOf(call.arguments);
|
|
97
|
+
if (path)
|
|
98
|
+
return path;
|
|
99
|
+
return call.name;
|
|
100
|
+
}
|
|
63
101
|
/**
|
|
64
102
|
* Append what a tool call actually did to the turn's provenance ledger.
|
|
65
103
|
*
|
|
@@ -396,10 +434,47 @@ const MAX_PARALLEL_READS = 4;
|
|
|
396
434
|
* were well able to do (see `THINK_ONLY_ESCALATION`).
|
|
397
435
|
*/
|
|
398
436
|
export const MAX_THINK_CONTINUES = 3;
|
|
437
|
+
/**
|
|
438
|
+
* Consecutive steps that RAN tools and had NO success before the loop asks the
|
|
439
|
+
* model to diagnose the stall. This generalises {@link RunTrace.repeatedFailure}
|
|
440
|
+
* (the identical action retried): a weak model also loops by substituting a
|
|
441
|
+
* different command each step, every one failing the same underlying way — none
|
|
442
|
+
* of them repeats exactly, so the same-action gate never fires.
|
|
443
|
+
*
|
|
444
|
+
* The threshold is deliberately ABOVE the handful of distinct checks ordinary
|
|
445
|
+
* iteration tries (`npm test` → `npm run build` → `npm run lint`), so a short run
|
|
446
|
+
* of unrelated failures is left alone; only a sustained all-fail streak is a stall.
|
|
447
|
+
*/
|
|
448
|
+
export const NO_PROGRESS_STALL_STEPS = 4;
|
|
449
|
+
/**
|
|
450
|
+
* How substantial a turn must be before the SELF-REVIEW gate may fire (see
|
|
451
|
+
* `selfReviewNudge`). Short, single-edit turns already end with an obvious
|
|
452
|
+
* result the model just looked at; a long turn is where scope drift hides, and
|
|
453
|
+
* where an extra pass earns its latency.
|
|
454
|
+
*/
|
|
455
|
+
export const SELF_REVIEW_MIN_STEPS = 6;
|
|
456
|
+
/**
|
|
457
|
+
* S5 — how many `plan_todo` UPDATE calls one turn may make. The guard refuses
|
|
458
|
+
* repeated CREATES (the observed planner loop) but must allow updates, because
|
|
459
|
+
* an update is how the user's checklist advances; this cap only stops a model
|
|
460
|
+
* that replaces doing the work with spamming status changes. It sits well above
|
|
461
|
+
* a genuine multi-step job (the largest real plans in the tree are single
|
|
462
|
+
* digits) and far below a loop.
|
|
463
|
+
*/
|
|
464
|
+
export const PLAN_TODO_UPDATE_CAP = 64;
|
|
399
465
|
/** Default continuations granted per turn when the option is omitted. */
|
|
400
466
|
export const DEFAULT_MAX_CONTINUATIONS = 2;
|
|
401
467
|
/** Default extra steps granted per continuation. */
|
|
402
468
|
export const DEFAULT_CONTINUATION_STEPS = 8;
|
|
469
|
+
/**
|
|
470
|
+
* `max` capability mode — the loop's longest bounded reasoning budget. The
|
|
471
|
+
* defaults above are sized to keep an ordinary turn responsive; under `max` the
|
|
472
|
+
* user has said cost is not a concern, so the same session is allowed to run a
|
|
473
|
+
* long build / many-file edit to completion instead of stopping at the default
|
|
474
|
+
* bound. Still finite: the hard cap is `maxSteps + 4 * 16` extra steps.
|
|
475
|
+
*/
|
|
476
|
+
export const MAX_CAPABILITY_MAX_CONTINUATIONS = 4;
|
|
477
|
+
export const MAX_CAPABILITY_CONTINUATION_STEPS = 16;
|
|
403
478
|
/** Pause before re-attempting a failed step (lets a transient outage clear). */
|
|
404
479
|
export const CONTINUATION_DELAY_MS = 1_500;
|
|
405
480
|
/**
|
|
@@ -425,8 +500,12 @@ function sleep(ms) {
|
|
|
425
500
|
async function runToolLoopInner(opts, progress) {
|
|
426
501
|
const { messages, tools: toolNames, maxSteps = 16, context, deps } = opts;
|
|
427
502
|
// Bounded auto-continuation state (see ToolLoopOptions.maxContinuations).
|
|
428
|
-
|
|
429
|
-
|
|
503
|
+
// `max` capability mode raises the DEFAULT budget (an explicit option still
|
|
504
|
+
// wins, so callers that pin a bound keep it). Read once per turn from the
|
|
505
|
+
// config, exactly like routing, so a mode change applies to the next turn.
|
|
506
|
+
const maxCapability = isMaxCapability(context.configManager);
|
|
507
|
+
const maxContinuations = Math.max(0, opts.maxContinuations ?? (maxCapability ? MAX_CAPABILITY_MAX_CONTINUATIONS : DEFAULT_MAX_CONTINUATIONS));
|
|
508
|
+
const continuationSteps = Math.max(1, opts.continuationSteps ?? (maxCapability ? MAX_CAPABILITY_CONTINUATION_STEPS : DEFAULT_CONTINUATION_STEPS));
|
|
430
509
|
let continuations = 0;
|
|
431
510
|
// Bounded dangling-promise nudges spent this turn (see INTENT_PROMISE_RE).
|
|
432
511
|
let intentNudges = 0;
|
|
@@ -440,12 +519,26 @@ async function runToolLoopInner(opts, progress) {
|
|
|
440
519
|
// asks the model to PROCEED, this one asks it to stop REPEATING, and the two
|
|
441
520
|
// fire on independent evidence.
|
|
442
521
|
let repeatNudges = 0;
|
|
522
|
+
// The loop's SELF-DIAGNOSIS nudge: bounded once per turn, fired when the SAME
|
|
523
|
+
// action has failed more than once (see RunTrace.repeatedFailure). This is the
|
|
524
|
+
// capability that turns "retry the identical failing command 10 times" into
|
|
525
|
+
// "state the root cause and change the approach" — the live macOS-build turn
|
|
526
|
+
// re-issued `npx tauri build` again and again against the same missing Cargo.
|
|
527
|
+
let diagnosisNudges = 0;
|
|
528
|
+
// Stage 2 — consecutive tool-running steps with NO success (the generalized
|
|
529
|
+
// stall signal, see NO_PROGRESS_STALL_STEPS). Reset by any successful tool.
|
|
530
|
+
let failedToolSteps = 0;
|
|
531
|
+
// Stage 4 — bounded SELF-REVIEW nudge (see requireSelfReview / selfReviewNudge).
|
|
532
|
+
let selfReviewNudges = 0;
|
|
443
533
|
// Bounded think-only continuation (see THINK_ONLY_ESCALATION). Counts the
|
|
444
534
|
// consecutive reasoning-only steps so the loop cannot spin on them.
|
|
445
535
|
let thinkContinues = 0;
|
|
446
536
|
// G13b — bounded "the request asked for a file and none was written" nudges
|
|
447
537
|
// (see wantsAuthoredArtifact).
|
|
448
538
|
let deliverableNudges = 0;
|
|
539
|
+
// ZERO-ACTION — bounded "the request directed work and NOTHING was done"
|
|
540
|
+
// nudges (see requireAction / zeroActionGateApplies).
|
|
541
|
+
let actionNudges = 0;
|
|
449
542
|
/**
|
|
450
543
|
* G18 — the sink, wrapped so an observability failure can never become a
|
|
451
544
|
* turn failure (a recorder that throws is a bug in the instrument, not in
|
|
@@ -468,6 +561,12 @@ async function runToolLoopInner(opts, progress) {
|
|
|
468
561
|
const maxParallelReads = Math.max(1, opts.maxParallelReads ?? MAX_PARALLEL_READS);
|
|
469
562
|
const followups = [];
|
|
470
563
|
const toolCallsRun = [];
|
|
564
|
+
// S5 — the plan_todo loop guard is split by ACTION (see the guard below):
|
|
565
|
+
// repeated CREATES were the observed planner loop; UPDATES are the tracking
|
|
566
|
+
// that keeps the user's checklist honest. Counted across the whole turn, like
|
|
567
|
+
// `toolCallsRun`, because a multi-step job runs many steps in one turn.
|
|
568
|
+
let planTodoCreates = 0;
|
|
569
|
+
let planTodoUpdates = 0;
|
|
471
570
|
// Every collected suggestion passes through the shared normalizer, so the
|
|
472
571
|
// loop's output is ALWAYS clean + structured (1–3 items, deduped, no leaked
|
|
473
572
|
// tool JSON, capped prompt/label) regardless of what the model emitted —
|
|
@@ -646,6 +745,39 @@ async function runToolLoopInner(opts, progress) {
|
|
|
646
745
|
// with an empty/bounded response (the "where is the essay?" bug).
|
|
647
746
|
let lastContent = '';
|
|
648
747
|
let bounded = false;
|
|
748
|
+
// D2.2 — has this turn already pre-flighted the project's build prerequisites?
|
|
749
|
+
// Checked once, before the FIRST build command the turn plans to run.
|
|
750
|
+
let prerequisitesPreflighted = false;
|
|
751
|
+
// E2 — has this turn already spent its one bounded "declare a plan first"
|
|
752
|
+
// nudge? Bounded once, like every other gate.
|
|
753
|
+
let planNudged = false;
|
|
754
|
+
// E2 (hard) — has this turn already spent its one bounded BLOCK of the first
|
|
755
|
+
// mutation? The plan gate is a hard requirement the first time it fires and a
|
|
756
|
+
// pure nudge after that: the first mutating batch is refused (see the gate
|
|
757
|
+
// below), and the second attempt runs even without a plan — a bounded block,
|
|
758
|
+
// never a wall.
|
|
759
|
+
let planBlocked = false;
|
|
760
|
+
/**
|
|
761
|
+
* Should the SELF-REVIEW gate fire now? Returns the correction to inject, or
|
|
762
|
+
* null. Extracted so the two end-of-turn exits ask the question identically —
|
|
763
|
+
* nothing here is a function of which branch is ending.
|
|
764
|
+
*/
|
|
765
|
+
const selfReviewCorrection = () => {
|
|
766
|
+
if (selfReviewNudges >= 1)
|
|
767
|
+
return null;
|
|
768
|
+
if (opts.requireSelfReview === false)
|
|
769
|
+
return null;
|
|
770
|
+
if (steps < SELF_REVIEW_MIN_STEPS)
|
|
771
|
+
return null;
|
|
772
|
+
if (progress.mutatedPaths.length === 0)
|
|
773
|
+
return null;
|
|
774
|
+
// The verification gate owns "did you check it"; self-review only runs once
|
|
775
|
+
// that question is settled, so the two can never both fire in one turn.
|
|
776
|
+
const activity = assessEditActivity(progress.successfulToolCalls, progress.verificationEvidence, progress.mutatedPaths);
|
|
777
|
+
if (activity.needsVerification)
|
|
778
|
+
return null;
|
|
779
|
+
return selfReviewNudge(currentAsk(opts));
|
|
780
|
+
};
|
|
649
781
|
for (;;) {
|
|
650
782
|
// ── Bounded auto-continuation on the STEP BOUND ────────────────────────
|
|
651
783
|
// The model still wanted to act when the budget ran out (a long build, a
|
|
@@ -675,6 +807,19 @@ async function runToolLoopInner(opts, progress) {
|
|
|
675
807
|
if (trimmedResult.trimmed > 0) {
|
|
676
808
|
thread.length = 0;
|
|
677
809
|
thread.push(...trimmedResult.thread);
|
|
810
|
+
// Keep a memory of what the turn DID after its raw outputs are gone:
|
|
811
|
+
// without this, a long turn loses the VERDICTS (did the build pass?) and
|
|
812
|
+
// re-runs work it already finished. Deterministic and bounded — the same
|
|
813
|
+
// philosophy as trimThreadBudget itself (facts, no summarizer, no drift).
|
|
814
|
+
if (isWorkDigestEnabled(context.configManager)) {
|
|
815
|
+
const digest = buildWorkDigest({
|
|
816
|
+
successfulTools: progress.successfulToolCalls,
|
|
817
|
+
mutatedPaths: progress.mutatedPaths,
|
|
818
|
+
executedActions: progress.executedActions,
|
|
819
|
+
});
|
|
820
|
+
if (digest)
|
|
821
|
+
upsertWorkDigest(thread, digest);
|
|
822
|
+
}
|
|
678
823
|
deps.onEvent?.(` ✂️ ${trimmedResult.trimmed} old tool result(s) trimmed to fit the ${Math.round(budgetChars / 1000)}K-char context budget.`);
|
|
679
824
|
}
|
|
680
825
|
}
|
|
@@ -999,6 +1144,32 @@ async function runToolLoopInner(opts, progress) {
|
|
|
999
1144
|
thread.push({ role: 'user', content: repeatNudge(closing, runTrace.priorAnswer(closing)) });
|
|
1000
1145
|
continue;
|
|
1001
1146
|
}
|
|
1147
|
+
// ── ZERO-ACTION gate (bounded, once) ────────────────────────────────
|
|
1148
|
+
// The request DIRECTED work on the workspace and the turn is ending
|
|
1149
|
+
// without having run a single tool: not a dropped promise, not a
|
|
1150
|
+
// permission question, not an authored file — just prose where the work
|
|
1151
|
+
// should be. The gates above all key on a positive shape in the reply and
|
|
1152
|
+
// so miss a plain non-answer; this one keys on the REQUEST and the
|
|
1153
|
+
// ABSENCE of action, which is exactly the shape that ended a fully
|
|
1154
|
+
// specified four-part coding ask with nothing done. Bounded once; the
|
|
1155
|
+
// residual is reported by `noActionTaken` below, whether or not the nudge
|
|
1156
|
+
// is enabled.
|
|
1157
|
+
if (actionNudges < 1 &&
|
|
1158
|
+
zeroActionGateApplies(opts, progress, schemas.length, requestText, authorization.authorized)) {
|
|
1159
|
+
actionNudges += 1;
|
|
1160
|
+
stepLimit += 1;
|
|
1161
|
+
deps.onEvent?.(' 🛠️ The request asked for work and nothing was done — telling the model to do it now.');
|
|
1162
|
+
traceEvent({
|
|
1163
|
+
kind: 'gate',
|
|
1164
|
+
gate: 'action',
|
|
1165
|
+
summary: 'the request directed work on the workspace and the turn performed none — one bounded nudge to carry it out',
|
|
1166
|
+
});
|
|
1167
|
+
// The non-answer must not become the delivered answer either way.
|
|
1168
|
+
lastContent = '';
|
|
1169
|
+
thread.push({ role: 'assistant', content: response.content });
|
|
1170
|
+
thread.push({ role: 'user', content: zeroActionNudge(currentAsk(opts)) });
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1002
1173
|
// S1 (both exits): the MOST SUBSTANTIVE content seen wins here too —
|
|
1003
1174
|
// a short closing step ("Sent it to her! ✅") with no tool calls must
|
|
1004
1175
|
// not clobber the deliverable (poem/essay) the model composed in an
|
|
@@ -1055,6 +1226,22 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1055
1226
|
});
|
|
1056
1227
|
continue;
|
|
1057
1228
|
}
|
|
1229
|
+
// SELF-REVIEW (no-tools exit) — a substantial, already-verified turn is
|
|
1230
|
+
// ending; spend ONE bounded pass to check the result against the ask.
|
|
1231
|
+
const review = selfReviewCorrection();
|
|
1232
|
+
if (review) {
|
|
1233
|
+
selfReviewNudges += 1;
|
|
1234
|
+
stepLimit += 1;
|
|
1235
|
+
deps.onEvent?.(' 🧭 Substantial turn ending — asking the model to review the result against the original ask.');
|
|
1236
|
+
traceEvent({
|
|
1237
|
+
kind: 'gate',
|
|
1238
|
+
gate: 'self-review',
|
|
1239
|
+
summary: 'a substantial turn that changed files reached its end — one bounded nudge to check the result against the original ask',
|
|
1240
|
+
});
|
|
1241
|
+
thread.push({ role: 'assistant', content: response.content });
|
|
1242
|
+
thread.push({ role: 'user', content: review });
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1058
1245
|
return {
|
|
1059
1246
|
content: response.content.length >= lastContent.length ? response.content : lastContent,
|
|
1060
1247
|
followups,
|
|
@@ -1085,14 +1272,47 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1085
1272
|
const tool = getTool(call.name);
|
|
1086
1273
|
const priorSameTool = toolCallsRun.filter((t) => t === call.name).length;
|
|
1087
1274
|
toolCallsRun.push(call.name);
|
|
1088
|
-
// S5 — PLANNER LOOP GUARD
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1275
|
+
// S5 — PLANNER LOOP GUARD, now split by ACTION.
|
|
1276
|
+
//
|
|
1277
|
+
// The guard exists because of a real failure: the model called plan_todo
|
|
1278
|
+
// six times in one turn (trace-1788059239352-k7zl03 — 15.6K tokens,
|
|
1279
|
+
// 2m25s, FAILED) instead of doing the work. But `create` and `update` are
|
|
1280
|
+
// the SAME tool, and the original `priorSameTool >= 1` check refused BOTH
|
|
1281
|
+
// — so a plan could be declared once and then never advanced, and the
|
|
1282
|
+
// checklist the user watches went stale the moment the first step closed.
|
|
1283
|
+
// Only repeated CREATES are the loop; updates ARE the tracking. Anything
|
|
1284
|
+
// that is not an explicit update (absent/malformed action) is counted as
|
|
1285
|
+
// a create, so the original protection is unchanged for the loop case.
|
|
1286
|
+
if (call.name === 'plan_todo') {
|
|
1287
|
+
const action = (() => {
|
|
1288
|
+
try {
|
|
1289
|
+
const raw = call.arguments;
|
|
1290
|
+
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
1291
|
+
return obj && typeof obj === 'object'
|
|
1292
|
+
? String(obj.action ?? '')
|
|
1293
|
+
: undefined;
|
|
1294
|
+
}
|
|
1295
|
+
catch {
|
|
1296
|
+
return undefined;
|
|
1297
|
+
}
|
|
1298
|
+
})();
|
|
1299
|
+
const isUpdate = action === 'update';
|
|
1300
|
+
if (!isUpdate && planTodoCreates >= 1) {
|
|
1301
|
+
return {
|
|
1302
|
+
call,
|
|
1303
|
+
refuse: 'Error: a plan already exists for this turn — do NOT declare it again. Advance it instead: call plan_todo with action "update", the step id and its status, then keep doing the work.',
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
if (isUpdate && planTodoUpdates >= PLAN_TODO_UPDATE_CAP) {
|
|
1307
|
+
return {
|
|
1308
|
+
call,
|
|
1309
|
+
refuse: `Error: plan_todo has been updated ${PLAN_TODO_UPDATE_CAP} times this turn — stop updating the plan and finish the remaining work.`,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
if (isUpdate)
|
|
1313
|
+
planTodoUpdates += 1;
|
|
1314
|
+
else
|
|
1315
|
+
planTodoCreates += 1;
|
|
1096
1316
|
}
|
|
1097
1317
|
if (tool?.category === 'pipeline' && priorSameTool >= 1) {
|
|
1098
1318
|
// The guard this replaces tested the literal name `pipeline`, which is
|
|
@@ -1286,6 +1506,118 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1286
1506
|
toolSpan?.end({ ok: false, message: 'tool outcome was never reported' });
|
|
1287
1507
|
}
|
|
1288
1508
|
};
|
|
1509
|
+
// ── PROJECT PREREQUISITE PRE-FLIGHT (D2.2) ──────────────────────────
|
|
1510
|
+
// Before the FIRST build of the turn, check the project's markers so a
|
|
1511
|
+
// missing prerequisite (build.rs, a Cargo feature, an icon) is handed over
|
|
1512
|
+
// BEFORE a failed build — not after ten identical retries. Best-effort and
|
|
1513
|
+
// bounded: once per turn, only when a build is actually planned.
|
|
1514
|
+
if (!prerequisitesPreflighted) {
|
|
1515
|
+
const buildPlan = plans.find((p) => {
|
|
1516
|
+
if (p.refuse !== undefined)
|
|
1517
|
+
return false;
|
|
1518
|
+
if (p.call.name !== 'run_terminal' && p.call.name !== 'terminal')
|
|
1519
|
+
return false;
|
|
1520
|
+
try {
|
|
1521
|
+
const raw = p.call.arguments;
|
|
1522
|
+
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
1523
|
+
const cmd = String(obj?.command ?? '');
|
|
1524
|
+
return isBuildCommand(cmd);
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
if (buildPlan) {
|
|
1531
|
+
prerequisitesPreflighted = true;
|
|
1532
|
+
try {
|
|
1533
|
+
const findings = checkProjectPrerequisites(createNodePrereqFs(opts.context?.cwd || process.cwd()));
|
|
1534
|
+
if (findings.length > 0) {
|
|
1535
|
+
thread.push({ role: 'user', content: formatPreflightFindings(findings) });
|
|
1536
|
+
deps.onEvent?.(` 🧱 pre-flight: ${findings.length} missing project prerequisite(s) — handed the exact fix before building.`);
|
|
1537
|
+
traceEvent({
|
|
1538
|
+
kind: 'gate',
|
|
1539
|
+
gate: 'prerequisite',
|
|
1540
|
+
summary: `pre-flight found ${findings.length} missing project prerequisite(s) before the build: ` +
|
|
1541
|
+
findings.map((f) => f.id).join(', '),
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
catch {
|
|
1546
|
+
// A pre-flight must never break the turn.
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
// ── PLAN GATE (E2) ──────────────────────────────────────────────────
|
|
1551
|
+
// A workspace-directing turn about to MUTATE without having declared a plan
|
|
1552
|
+
// is stopped ONCE: its first FILE mutation is refused and the model is told
|
|
1553
|
+
// to plan first (`plan_todo`). Terminal commands still trigger the nudge but
|
|
1554
|
+
// are not blocked (see PLAN_GATED_TOOL_NAMES) — a build or a test is a step
|
|
1555
|
+
// a plan is meant to reach, not a change to gate. Bounded by design: the
|
|
1556
|
+
// SECOND attempt runs even without a plan (see `planBlocked`), so this guides
|
|
1557
|
+
// the model into the plan → track → verify contract instead of walling the
|
|
1558
|
+
// turn off. A batch that DECLARES a plan itself is never blocked (the model
|
|
1559
|
+
// is already doing the right thing), and `requirePlan:false` leaves the gate
|
|
1560
|
+
// byte-identical to not existing.
|
|
1561
|
+
const declaresPlan = !opts.context?.planStore?.snapshot?.() && plans.some((p) => {
|
|
1562
|
+
if (p.call.name !== 'plan_todo')
|
|
1563
|
+
return false;
|
|
1564
|
+
try {
|
|
1565
|
+
const raw = p.call.arguments;
|
|
1566
|
+
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
1567
|
+
const action = obj && typeof obj === 'object' ? String(obj.action ?? '') : '';
|
|
1568
|
+
return action !== 'update';
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
// Malformed args still count as a create attempt — the plan_todo guard
|
|
1572
|
+
// above already handles a repeated create, and a first one is honored.
|
|
1573
|
+
return true;
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
if (!planNudged &&
|
|
1577
|
+
!declaresPlan &&
|
|
1578
|
+
opts.requirePlan !== false &&
|
|
1579
|
+
schemas.length > 0 &&
|
|
1580
|
+
!opts.context?.planStore?.snapshot?.() &&
|
|
1581
|
+
plans.some((p) => p.refuse === undefined && MUTATING_TOOL_NAMES.has(p.call.name)) &&
|
|
1582
|
+
requestRequiresWorkspaceAction(requestText, authorization.authorized)) {
|
|
1583
|
+
planNudged = true;
|
|
1584
|
+
// Hard requirement, spent ONCE: refuse the first batch's FILE mutations
|
|
1585
|
+
// so nothing is written before a plan exists. It bumps the step budget by
|
|
1586
|
+
// one because it forces a genuine re-answer (the model must declare the
|
|
1587
|
+
// plan and retry), exactly like the zero-action nudge.
|
|
1588
|
+
const blocked = [];
|
|
1589
|
+
if (!planBlocked) {
|
|
1590
|
+
planBlocked = true;
|
|
1591
|
+
for (const p of plans) {
|
|
1592
|
+
if (p.refuse === undefined && PLAN_GATED_TOOL_NAMES.has(p.call.name)) {
|
|
1593
|
+
p.refuse =
|
|
1594
|
+
'Error: declare a plan first via plan_todo — call it with 2–6 ordered steps (action "create"), then retry this change. The plan is how the turn is tracked and verified.';
|
|
1595
|
+
blocked.push(p.call.name);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (blocked.length > 0)
|
|
1599
|
+
stepLimit += 1;
|
|
1600
|
+
for (const name of blocked) {
|
|
1601
|
+
traceEvent({
|
|
1602
|
+
kind: 'refusal',
|
|
1603
|
+
gate: 'plan',
|
|
1604
|
+
tool: name,
|
|
1605
|
+
summary: `refused ${name}: no plan declared — the first mutation is blocked once so the model plans before it changes files`,
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
deps.onEvent?.(blocked.length > 0
|
|
1610
|
+
? ' 🗺️ No plan declared — blocking the first change until the model plans.'
|
|
1611
|
+
: ' 🗺️ No plan declared — asking the model to plan before it changes files.');
|
|
1612
|
+
traceEvent({
|
|
1613
|
+
kind: 'gate',
|
|
1614
|
+
gate: 'plan',
|
|
1615
|
+
summary: blocked.length > 0
|
|
1616
|
+
? `a workspace-directing turn was about to mutate without a plan — the first mutation batch (${blocked.join(', ')}) was refused once and the model asked to declare a plan first`
|
|
1617
|
+
: 'a workspace-directing turn was about to mutate without a plan — one bounded nudge to declare one first',
|
|
1618
|
+
});
|
|
1619
|
+
thread.push({ role: 'user', content: planRequiredNudge(currentAsk(opts)) });
|
|
1620
|
+
}
|
|
1289
1621
|
const executed = new Array(plans.length).fill('');
|
|
1290
1622
|
for (let i = 0; i < plans.length;) {
|
|
1291
1623
|
// A run of consecutive read-only calls is ONE bounded fan-out; any
|
|
@@ -1323,6 +1655,7 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1323
1655
|
// What each call actually delivered (post hint/tip decoration), kept in
|
|
1324
1656
|
// call order for the endsAgentStep exit below.
|
|
1325
1657
|
const delivered = new Array(plans.length).fill('');
|
|
1658
|
+
let stepAnySuccess = false;
|
|
1326
1659
|
for (let i = 0; i < plans.length; i += 1) {
|
|
1327
1660
|
const call = plans[i].call;
|
|
1328
1661
|
const rawResult = executed[i];
|
|
@@ -1342,8 +1675,10 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1342
1675
|
const ranOk = refusal === null &&
|
|
1343
1676
|
!rawResult.startsWith('Error:') &&
|
|
1344
1677
|
(!DELIVERY_TOOL_NAMES.has(call.name) || deliveryResultSucceeded(rawResult));
|
|
1345
|
-
if (ranOk)
|
|
1678
|
+
if (ranOk) {
|
|
1346
1679
|
progress.successfulToolCalls.push(call.name);
|
|
1680
|
+
stepAnySuccess = true;
|
|
1681
|
+
}
|
|
1347
1682
|
if (call.name === 'gateway_send' && deliveryResultSucceeded(rawResult)) {
|
|
1348
1683
|
progress.deliveryConfirmed = true;
|
|
1349
1684
|
}
|
|
@@ -1367,6 +1702,17 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1367
1702
|
}
|
|
1368
1703
|
}
|
|
1369
1704
|
else if (refusal !== null || rawResult.startsWith('Error:')) {
|
|
1705
|
+
// SELF-DIAGNOSIS — record an action that RAN and FAILED (a non-zero
|
|
1706
|
+
// exit, a timeout, a tool error), keyed by the action itself (the
|
|
1707
|
+
// command / path). The gate below fires when the SAME action repeats:
|
|
1708
|
+
// a refusal was already remembered across runs (step-handoff), but a
|
|
1709
|
+
// command that RUNS and FAILS was not, so the live macOS-build turn
|
|
1710
|
+
// re-issued `npx tauri build` ~10 times against the same missing Cargo
|
|
1711
|
+
// and never once stopped to diagnose. Refusals are the other record
|
|
1712
|
+
// below; this one is for the calls that actually executed.
|
|
1713
|
+
if (refusal === null) {
|
|
1714
|
+
runTrace.recordFailure(call.name, failureActionOf(call), rawResult);
|
|
1715
|
+
}
|
|
1370
1716
|
// Stage 2 — the other half of noticing a loop: the SAME refusal, twice.
|
|
1371
1717
|
// The run records it with its reason, so a later step (or a self-report)
|
|
1372
1718
|
// can see "blocked twice, identically" instead of rediscovering it — and
|
|
@@ -1422,6 +1768,54 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1422
1768
|
const parallelTip = parallel.note(call.name, !resultText.startsWith('Error:'));
|
|
1423
1769
|
if (parallelTip)
|
|
1424
1770
|
resultText = `${resultText}\n\n${parallelTip}`;
|
|
1771
|
+
// ── MISSING-PREREQUISITE TAKEOVER ──────────────────────────────────
|
|
1772
|
+
// A `command not found` (exit 127) for a KNOWN, installable system tool
|
|
1773
|
+
// is a STEP, not a wall — but a model can read it as an environment
|
|
1774
|
+
// limit and stop: "I cannot install system-level software on your host
|
|
1775
|
+
// machine — I am physically unable" (live: trace-1791127992452-qzgodi,
|
|
1776
|
+
// where a user who had explicitly granted terminal permission was handed
|
|
1777
|
+
// a manual `curl … | sh` step instead of the install being done). Inject
|
|
1778
|
+
// the exact install command and forbid the refusal, deterministically, so
|
|
1779
|
+
// the NEXT model step acts. Advisory text only — the loop never runs the
|
|
1780
|
+
// install itself; that stays the model's governed `run_terminal` call.
|
|
1781
|
+
if (!ranOk && (call.name === 'run_terminal' || call.name === 'terminal')) {
|
|
1782
|
+
const failedCommand = String(call.arguments?.command ?? '');
|
|
1783
|
+
// EVERY installable tool the failing line needs, not just the first —
|
|
1784
|
+
// a `cd app && cargo build && cmake .` turn should hand the model all
|
|
1785
|
+
// the prerequisites in one takeover instead of one per round-trip.
|
|
1786
|
+
const missing = installableToolsFromFailure(failedCommand, rawResult);
|
|
1787
|
+
if (missing.length > 0) {
|
|
1788
|
+
const installCommands = missing.map((t) => resolveInstallCommand(t));
|
|
1789
|
+
resultText = `${resultText}\n\n${toolTakeoverInstruction(missing, installCommands)}`;
|
|
1790
|
+
deps.onEvent?.(` 🛠️ ${missing.map((t) => `'${t}'`).join(', ')} missing and installable — handing the model the install command(s) to run.`);
|
|
1791
|
+
traceEvent({
|
|
1792
|
+
kind: 'gate',
|
|
1793
|
+
gate: 'tool-takeover',
|
|
1794
|
+
summary: `${missing.join(', ')} missing (command not found) and installable — the model is ` +
|
|
1795
|
+
'told to install them itself instead of declaring itself unable',
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
// ── PROJECT PREREQUISITE BACKSTOP (D2.3) ──────────────────────────
|
|
1799
|
+
// No missing BINARY, but this failure may be a missing project FILE/
|
|
1800
|
+
// FEATURE (the other half of the same wall): `src-tauri/build.rs`, a
|
|
1801
|
+
// Cargo `custom-protocol` feature, an icon — none of which is a tool to
|
|
1802
|
+
// install, so the takeover above never fired and the live macOS turn
|
|
1803
|
+
// re-ran `npx tauri build` ~10 times. Match the OUTPUT SIGNATURE, hand
|
|
1804
|
+
// the model the exact fix on the FIRST failure, and record it.
|
|
1805
|
+
if (missing.length === 0) {
|
|
1806
|
+
const rules = matchPrerequisiteSignatures(rawResult);
|
|
1807
|
+
if (rules.length > 0) {
|
|
1808
|
+
resultText = `${resultText}\n\n${prerequisiteTakeoverInstruction(rules)}`;
|
|
1809
|
+
deps.onEvent?.(` 🧱 build prerequisite signature matched (${rules.map((r) => r.id).join(', ')}) — handing the model the exact fix.`);
|
|
1810
|
+
traceEvent({
|
|
1811
|
+
kind: 'gate',
|
|
1812
|
+
gate: 'prerequisite',
|
|
1813
|
+
summary: `build failed on a missing PROJECT prerequisite (${rules.map((r) => r.id).join(', ')}) — ` +
|
|
1814
|
+
'the model is given the exact fix instead of retrying the identical command',
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1425
1819
|
delivered[i] = resultText;
|
|
1426
1820
|
// G18 — record what this call DID, in the model's own call order, with the
|
|
1427
1821
|
// evidence a later reader needs: the args the gate saw, the first line of
|
|
@@ -1443,6 +1837,52 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1443
1837
|
});
|
|
1444
1838
|
thread.push({ role: 'tool', toolCallId: call.id, content: resultText });
|
|
1445
1839
|
}
|
|
1840
|
+
// Track the generalized stall: a step that RAN tools but succeeded at none
|
|
1841
|
+
// of them extends the streak; any success clears it. A text-only step (no
|
|
1842
|
+
// tools) leaves it as it was — the model is thinking, not failing.
|
|
1843
|
+
if (plans.length > 0)
|
|
1844
|
+
failedToolSteps = stepAnySuccess ? 0 : failedToolSteps + 1;
|
|
1845
|
+
// ── SELF-DIAGNOSIS nudge (bounded, once) ──────────────────────────────
|
|
1846
|
+
// The loop's missing introspection: when the SAME action has failed more
|
|
1847
|
+
// than once, re-issuing it is a loop, not progress. This is the exact
|
|
1848
|
+
// shape of the live macOS-build turn — `npx tauri build` was retried ~10
|
|
1849
|
+
// times against the same missing Rust/Cargo, each attempt rediscovering
|
|
1850
|
+
// the same wall, and the run never stopped to ask WHY. Unlike the
|
|
1851
|
+
// end-of-turn gates (permission/repeat/deliverable), which fire only when
|
|
1852
|
+
// the model stops calling tools, this one must fire MID-TURN while the
|
|
1853
|
+
// model is still looping on the failing call — so it is injected here,
|
|
1854
|
+
// right after the step's results are in the thread, and the next model
|
|
1855
|
+
// step sees it. Bounded once per turn and keyed on the action so the same
|
|
1856
|
+
// repeat cannot trigger it twice.
|
|
1857
|
+
if (diagnosisNudges < 1) {
|
|
1858
|
+
const repeated = runTrace.repeatedFailure();
|
|
1859
|
+
if (repeated && repeated.times >= 2) {
|
|
1860
|
+
diagnosisNudges += 1;
|
|
1861
|
+
stepLimit += 1;
|
|
1862
|
+
deps.onEvent?.(` 🩺 Same action failed ${repeated.times}× — asking the model to diagnose the cause instead of retrying.`);
|
|
1863
|
+
traceEvent({
|
|
1864
|
+
kind: 'gate',
|
|
1865
|
+
gate: 'diagnosis',
|
|
1866
|
+
summary: `the same action failed ${repeated.times} times (${repeated.tool}: ${repeated.action.slice(0, 120)}) — ` +
|
|
1867
|
+
'one bounded nudge to diagnose the root cause and change the approach',
|
|
1868
|
+
});
|
|
1869
|
+
thread.push({ role: 'user', content: repeatedFailureNudge(repeated) });
|
|
1870
|
+
}
|
|
1871
|
+
else if (failedToolSteps >= NO_PROGRESS_STALL_STEPS) {
|
|
1872
|
+
// Stage 2 — no SINGLE action repeated, but the last N steps each ran
|
|
1873
|
+
// tools and none succeeded. Same diagnosis demanded, different evidence.
|
|
1874
|
+
diagnosisNudges += 1;
|
|
1875
|
+
stepLimit += 1;
|
|
1876
|
+
deps.onEvent?.(` 🩺 ${failedToolSteps} steps with no successful action — asking the model to diagnose the stall.`);
|
|
1877
|
+
traceEvent({
|
|
1878
|
+
kind: 'gate',
|
|
1879
|
+
gate: 'diagnosis',
|
|
1880
|
+
summary: `${failedToolSteps} consecutive steps ran tools and none succeeded — ` +
|
|
1881
|
+
'one bounded nudge to diagnose the shared cause and change the approach',
|
|
1882
|
+
});
|
|
1883
|
+
thread.push({ role: 'user', content: noProgressNudge(runTrace.recentFailures(3), failedToolSteps) });
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1446
1886
|
// Tiered exposure: after EVERY executed tool call, union any newly
|
|
1447
1887
|
// loaded toolset schemas into the live set so the NEXT model step can
|
|
1448
1888
|
// call them natively (tool_search load → loadedExtraTools → here).
|
|
@@ -1571,6 +2011,40 @@ async function runToolLoopInner(opts, progress) {
|
|
|
1571
2011
|
thread.push({ role: 'user', content: deliverableNudge(authorization.requestedPath) });
|
|
1572
2012
|
continue;
|
|
1573
2013
|
}
|
|
2014
|
+
// ZERO-ACTION gate (concluding exit) — the same bounded pass as the
|
|
2015
|
+
// no-tools exit, for a turn that concluded (suggest_followups) without
|
|
2016
|
+
// ever touching the workspace a directed request asked it to change.
|
|
2017
|
+
if (actionNudges < 1 &&
|
|
2018
|
+
zeroActionGateApplies(opts, progress, schemas.length, requestText, authorization.authorized)) {
|
|
2019
|
+
actionNudges += 1;
|
|
2020
|
+
stepLimit += 1;
|
|
2021
|
+
deps.onEvent?.(' 🛠️ The request asked for work and nothing was done — telling the model to do it now.');
|
|
2022
|
+
traceEvent({
|
|
2023
|
+
kind: 'gate',
|
|
2024
|
+
gate: 'action',
|
|
2025
|
+
summary: 'the request directed work on the workspace and the turn performed none — one bounded nudge to carry it out',
|
|
2026
|
+
});
|
|
2027
|
+
lastContent = '';
|
|
2028
|
+
thread.push({ role: 'assistant', content: response.content });
|
|
2029
|
+
thread.push({ role: 'user', content: zeroActionNudge(currentAsk(opts)) });
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
// SELF-REVIEW (concluding exit) — same bounded pass, placed AFTER the
|
|
2033
|
+
// verification/deliverable gates so those settle their questions first.
|
|
2034
|
+
const review = selfReviewCorrection();
|
|
2035
|
+
if (review) {
|
|
2036
|
+
selfReviewNudges += 1;
|
|
2037
|
+
stepLimit += 1;
|
|
2038
|
+
deps.onEvent?.(' 🧭 Substantial turn ending — asking the model to review the result against the original ask.');
|
|
2039
|
+
traceEvent({
|
|
2040
|
+
kind: 'gate',
|
|
2041
|
+
gate: 'self-review',
|
|
2042
|
+
summary: 'a substantial turn that changed files reached its end — one bounded nudge to check the result against the original ask',
|
|
2043
|
+
});
|
|
2044
|
+
thread.push({ role: 'assistant', content: response.content });
|
|
2045
|
+
thread.push({ role: 'user', content: review });
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
1574
2048
|
const content = response.content.length >= lastContent.length ? response.content : lastContent;
|
|
1575
2049
|
return {
|
|
1576
2050
|
content,
|
|
@@ -1921,9 +2395,23 @@ export async function runToolLoop(opts) {
|
|
|
1921
2395
|
if (!result.cancelled &&
|
|
1922
2396
|
progress.mutatedPaths.length === 0 &&
|
|
1923
2397
|
!replyAsksTheReader(result.content) &&
|
|
2398
|
+
// A request that forbade writes cannot have "failed to deliver" a file.
|
|
2399
|
+
!requestForbidsWrites(lastUserText(opts.messages)) &&
|
|
1924
2400
|
wantsAuthoredArtifact(lastUserText(opts.messages))) {
|
|
1925
2401
|
result.undeliveredArtifact = true;
|
|
1926
2402
|
}
|
|
2403
|
+
// ZERO-ACTION honesty — a request that DIRECTED work on the workspace was
|
|
2404
|
+
// answered with nothing at all: no tool succeeded, nothing was written. The
|
|
2405
|
+
// flag is a function of what the turn DID, never of configuration, so a
|
|
2406
|
+
// caller can never read "completed" from a turn whose request it never
|
|
2407
|
+
// touched. Authored-artifact asks are left to `undeliveredArtifact` above so
|
|
2408
|
+
// one turn is never reported under two names.
|
|
2409
|
+
const askText = lastUserText(opts.messages);
|
|
2410
|
+
if (!hasProductiveAction(progress) &&
|
|
2411
|
+
progress.mutatedPaths.length === 0 &&
|
|
2412
|
+
requestRequiresWorkspaceAction(askText, requestAuthorizesWrites(askText).authorized)) {
|
|
2413
|
+
result.noActionTaken = true;
|
|
2414
|
+
}
|
|
1927
2415
|
}
|
|
1928
2416
|
return result;
|
|
1929
2417
|
}
|
|
@@ -1961,8 +2449,125 @@ function deliverableGateApplies(opts, content, progress, schemaCount, requestTex
|
|
|
1961
2449
|
return false;
|
|
1962
2450
|
if (replyAsksTheReader(content))
|
|
1963
2451
|
return false;
|
|
2452
|
+
// A negative instruction outranks every positive signal. "Do not write any
|
|
2453
|
+
// files — answer in chat" contains a creation verb and a file-shaped noun, so
|
|
2454
|
+
// without this the gate read it as an authored-artifact ask and wrote a file
|
|
2455
|
+
// against the user's explicit instruction.
|
|
2456
|
+
if (requestForbidsWrites(requestText))
|
|
2457
|
+
return false;
|
|
1964
2458
|
return wantsAuthoredArtifact(requestText);
|
|
1965
2459
|
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Tools that do not count as having DONE anything on their own. `suggest_followups`
|
|
2462
|
+
* concludes a turn; it produces no work, so a turn that only called it has still
|
|
2463
|
+
* performed nothing the request asked for. Every other successful tool counts as
|
|
2464
|
+
* an action (a read is an action), which keeps the zero-action gate conservative.
|
|
2465
|
+
*/
|
|
2466
|
+
/**
|
|
2467
|
+
* A token that NAMES a file (a real source/config extension). The strongest
|
|
2468
|
+
* signal that the request is about the WORKSPACE, not a chat answer.
|
|
2469
|
+
*/
|
|
2470
|
+
const REQUEST_FILE_TOKEN_RE = /\b[\w./~-]+\.(?:js|mjs|cjs|ts|tsx|jsx|py|rb|go|rs|java|kt|cs|cpp|cxx|cc|c|h|hpp|json|ya?ml|toml|ini|cfg|conf|md|markdown|txt|csv|tsv|html?|css|scss|sass|less|sql|sh|bash|zsh|fish|env|lock|xml|gradle|properties|vue|svelte|php|lua|pl|swift|dart|scala)\b/i;
|
|
2471
|
+
/** A verb that asks for a change to the workspace. */
|
|
2472
|
+
const WORK_EDIT_VERB_RE = /\b(?:fix|repair|refactor|edit|modify|updat|chang|add|implement|creat|writ|delet|remov|renam|rewrit|migrat|correct|debug|patch|tweak|scaffold)\w*/i;
|
|
2473
|
+
/** A code/workspace noun that pairs with the verb above. */
|
|
2474
|
+
const CODE_NOUN_RE = /\b(?:file|files|function|functions|method|methods|class|classes|module|modules|script|scripts|component|components|test|tests|suite|api|endpoint|endpoints|route|routes|schema|schemas|migration|migrations|package|dependency|dependencies|import|imports|config|configuration|type|types|interface|interfaces|bug|bugs|repo|repository|codebase|project|source)\b/i;
|
|
2475
|
+
/**
|
|
2476
|
+
* Does this request DIRECT work on the workspace — the precondition for the
|
|
2477
|
+
* ZERO-ACTION gate?
|
|
2478
|
+
*
|
|
2479
|
+
* Deliberately narrower than "the request authorizes writes": the gate spends a
|
|
2480
|
+
* model step, so it must only fire on an ask that genuinely needs the tools.
|
|
2481
|
+
* `requestAuthorizesWrites` is true for a prose deliverable ("draft an
|
|
2482
|
+
* itinerary") or a review ("assess this project"), which are correctly answered
|
|
2483
|
+
* in chat — nudging those would turn one turn into two for no reason (found by
|
|
2484
|
+
* the existing suite). The workspace signal is therefore explicit: the request
|
|
2485
|
+
* either names a FILE, or pairs an edit verb with a code noun.
|
|
2486
|
+
*/
|
|
2487
|
+
function requestRequiresWorkspaceAction(requestText, authorized) {
|
|
2488
|
+
const text = (requestText || '').trim();
|
|
2489
|
+
if (!text)
|
|
2490
|
+
return false;
|
|
2491
|
+
if (!authorized)
|
|
2492
|
+
return false;
|
|
2493
|
+
if (requestForbidsWrites(text))
|
|
2494
|
+
return false;
|
|
2495
|
+
if (wantsAuthoredArtifact(text))
|
|
2496
|
+
return false;
|
|
2497
|
+
if (REQUEST_FILE_TOKEN_RE.test(text))
|
|
2498
|
+
return true;
|
|
2499
|
+
return WORK_EDIT_VERB_RE.test(text) && CODE_NOUN_RE.test(text);
|
|
2500
|
+
}
|
|
2501
|
+
const NON_PRODUCTIVE_TOOLS = new Set(['suggest_followups']);
|
|
2502
|
+
/** True when the turn performed at least one action beyond merely concluding. */
|
|
2503
|
+
function hasProductiveAction(progress) {
|
|
2504
|
+
return progress.successfulToolCalls.some((name) => !NON_PRODUCTIVE_TOOLS.has(name));
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Does the ZERO-ACTION gate apply to this turn?
|
|
2508
|
+
*
|
|
2509
|
+
* Every term is an independent, checkable fact — the conjunction is what keeps
|
|
2510
|
+
* the gate from firing on turns that are legitimately answer-only:
|
|
2511
|
+
*
|
|
2512
|
+
* - the request AUTHORIZES writes (`authorized`) — a create/maintenance ask,
|
|
2513
|
+
* not a question (`requestAuthorizesWrites` already vetoes pure questions);
|
|
2514
|
+
* - NO tool call succeeded this turn (a turn that gathered context and then
|
|
2515
|
+
* answered is out of scope — it did something, even if it then stalled);
|
|
2516
|
+
* - NOTHING was written (`mutatedPaths` is the loop's own proof a write
|
|
2517
|
+
* landed, so a heredoc through `run_terminal` is not nudged);
|
|
2518
|
+
* - tools are actually available (a caller that exposed none cannot comply);
|
|
2519
|
+
* - the request did NOT forbid writes (a negative instruction outranks every
|
|
2520
|
+
* positive signal — see `requestForbidsWrites`);
|
|
2521
|
+
* - it is NOT an authored-artifact ask: `wantsAuthoredArtifact` requests are
|
|
2522
|
+
* the DELIVERABLE gate's job, with their own narrower message, and running
|
|
2523
|
+
* both would spend two nudges on one ask.
|
|
2524
|
+
*/
|
|
2525
|
+
function zeroActionGateApplies(opts, progress, schemaCount, requestText, authorized) {
|
|
2526
|
+
if (opts.requireAction === false)
|
|
2527
|
+
return false;
|
|
2528
|
+
if (schemaCount === 0)
|
|
2529
|
+
return false;
|
|
2530
|
+
if (hasProductiveAction(progress))
|
|
2531
|
+
return false;
|
|
2532
|
+
if (progress.mutatedPaths.length > 0)
|
|
2533
|
+
return false;
|
|
2534
|
+
return requestRequiresWorkspaceAction(requestText, authorized);
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* The bounded PLAN-REQUIRED nudge (E2) — one advisory step telling the model to
|
|
2538
|
+
* declare a short plan before it mutates.
|
|
2539
|
+
*
|
|
2540
|
+
* Names the ask, states the contract (plan first, then work), and closes the
|
|
2541
|
+
* failure mode the nudge exists for: planning that REPLACES the work; the last
|
|
2542
|
+
* line forbids exactly that dormancy.
|
|
2543
|
+
*/
|
|
2544
|
+
export function planRequiredNudge(ask) {
|
|
2545
|
+
const quoted = (ask || '').trim().replace(/\s+/g, ' ').slice(0, 300);
|
|
2546
|
+
return ('Before you change anything, declare a short PLAN for this turn — call `plan_todo` with 2–6 ordered steps' +
|
|
2547
|
+
' (create the plan once, then advance it with action "update" as you go).' +
|
|
2548
|
+
(quoted ? ` The request: "${quoted}".` : '') +
|
|
2549
|
+
'\nA plan is how the user can follow along and how the turn is verified — keep it accurate, do not pad it,' +
|
|
2550
|
+
' and do NOT let planning replace the work: declare it, then do the first step now.');
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* The bounded ZERO-ACTION correction — the loop telling the model, in one step,
|
|
2554
|
+
* that a directed request has not been touched yet.
|
|
2555
|
+
*
|
|
2556
|
+
* Names the ask (so the model cannot claim it did not know what was wanted),
|
|
2557
|
+
* states plainly that nothing ran and nothing changed, and closes the two escape
|
|
2558
|
+
* hatches that produced the observed non-actions: do not re-ask a request that is
|
|
2559
|
+
* already specified, and do not answer a work request with a plan, an apology or
|
|
2560
|
+
* a question. It still leaves a REAL blocker as a legitimate way out — the goal
|
|
2561
|
+
* is the work, not compliance theatre.
|
|
2562
|
+
*/
|
|
2563
|
+
export function zeroActionNudge(ask) {
|
|
2564
|
+
const quoted = (ask || '').trim().replace(/\s+/g, ' ').slice(0, 300);
|
|
2565
|
+
return ('Nothing has been done yet: you have not called a single tool this turn, so no file was changed and nothing was checked.' +
|
|
2566
|
+
(quoted ? ` The request already asked for this work: "${quoted}".` : '') +
|
|
2567
|
+
'\nYou have the tools to do it — read what you need, make the change, and run the check NOW.' +
|
|
2568
|
+
' Do NOT reply with a plan, a summary of what you would do, an apology, or a request for the user to restate or confirm' +
|
|
2569
|
+
' a request that is already complete. If something genuinely blocks you, say exactly what it is and why; otherwise do the work.');
|
|
2570
|
+
}
|
|
1966
2571
|
/**
|
|
1967
2572
|
* The text of the LAST user message in the thread — what the user is actually
|
|
1968
2573
|
* asking for right now, as opposed to the history above it. Used to derive
|
|
@@ -2148,4 +2753,92 @@ export function trimThreadBudget(thread, maxChars = DEFAULT_THREAD_BUDGET_CHARS)
|
|
|
2148
2753
|
}
|
|
2149
2754
|
return { thread: out, trimmed };
|
|
2150
2755
|
}
|
|
2756
|
+
// ─── Within-turn work digest (the memory a trimmed thread keeps) ────────────
|
|
2757
|
+
// `trimThreadBudget` keeps the first 500 chars of each old tool result, but past
|
|
2758
|
+
// that the model loses the VERDICT of what it ran and can re-run finished work.
|
|
2759
|
+
// `working-state` solves this ACROSS turns; this solves it WITHIN one. It is
|
|
2760
|
+
// deliberately deterministic and LLM-free (facts: what changed, what commands
|
|
2761
|
+
// ran and whether they passed, which tools were used) — no summarizer, no
|
|
2762
|
+
// latency, no drift, exactly like the ledger and the budget it complements.
|
|
2763
|
+
/** Marker prefix identifying the loop's within-turn work digest message. */
|
|
2764
|
+
export const WORK_DIGEST_MARKER = '[work digest — your actions so far this turn]';
|
|
2765
|
+
/**
|
|
2766
|
+
* Format the turn's actions so far as a bounded, model-readable digest. Returns
|
|
2767
|
+
* '' when there is nothing worth saying, so a pristine turn adds no noise.
|
|
2768
|
+
*/
|
|
2769
|
+
export function buildWorkDigest(input) {
|
|
2770
|
+
const lines = [];
|
|
2771
|
+
const changed = [...new Set(input.mutatedPaths.filter(Boolean))];
|
|
2772
|
+
if (changed.length > 0) {
|
|
2773
|
+
const shown = changed.slice(-12);
|
|
2774
|
+
lines.push(`• Files changed (${changed.length}): ${shown.join(', ')}${changed.length > shown.length ? ', …' : ''}`);
|
|
2775
|
+
}
|
|
2776
|
+
// Commands with their verdict, newest first, deduped by command+verdict — the
|
|
2777
|
+
// single most valuable thing to retain (did the build/test pass?).
|
|
2778
|
+
const cmds = input.executedActions.filter((a) => typeof a.command === 'string' && a.command.trim());
|
|
2779
|
+
if (cmds.length > 0) {
|
|
2780
|
+
const seen = new Set();
|
|
2781
|
+
const shown = [];
|
|
2782
|
+
for (let i = cmds.length - 1; i >= 0 && shown.length < 8; i -= 1) {
|
|
2783
|
+
const a = cmds[i];
|
|
2784
|
+
const key = `${a.ok ? 'ok' : 'fail'}:${a.command}`;
|
|
2785
|
+
if (seen.has(key))
|
|
2786
|
+
continue;
|
|
2787
|
+
seen.add(key);
|
|
2788
|
+
shown.unshift(`${a.ok ? '✅' : '❌'} ${a.command}`);
|
|
2789
|
+
}
|
|
2790
|
+
lines.push('• Commands run:');
|
|
2791
|
+
for (const s of shown)
|
|
2792
|
+
lines.push(` ${s}`);
|
|
2793
|
+
}
|
|
2794
|
+
const tools = [...new Set(input.successfulTools)];
|
|
2795
|
+
if (tools.length > 0)
|
|
2796
|
+
lines.push(`• Tools used: ${tools.join(', ')}`);
|
|
2797
|
+
if (lines.length === 0)
|
|
2798
|
+
return '';
|
|
2799
|
+
return `${WORK_DIGEST_MARKER}\n${lines.join('\n')}`;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Insert or refresh the single work-digest message. Idempotent: a digest already
|
|
2803
|
+
* in the thread is UPDATED in place, never stacked, so repeated compaction in a
|
|
2804
|
+
* long turn keeps one current digest rather than accumulating stale ones. Placed
|
|
2805
|
+
* just after the system prompt + first user message so it sits with the ask and
|
|
2806
|
+
* survives the NEXT compaction (the tail is never trimmed).
|
|
2807
|
+
*/
|
|
2808
|
+
export function upsertWorkDigest(thread, digest) {
|
|
2809
|
+
const existing = thread.findIndex((m) => m.content.startsWith(WORK_DIGEST_MARKER));
|
|
2810
|
+
if (existing !== -1) {
|
|
2811
|
+
thread[existing] = { ...thread[existing], content: digest };
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
let at = 0;
|
|
2815
|
+
while (at < thread.length && thread[at].role === 'system')
|
|
2816
|
+
at += 1;
|
|
2817
|
+
const firstUser = thread.findIndex((m) => m.role === 'user');
|
|
2818
|
+
const insertAt = Math.min(thread.length, firstUser === -1 ? at : Math.max(at, firstUser + 1));
|
|
2819
|
+
thread.splice(insertAt, 0, { role: 'user', content: digest });
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* The bounded SELF-REVIEW correction — the loop's stand-in for a reviewer who
|
|
2823
|
+
* asks "is this actually what was asked for?".
|
|
2824
|
+
*
|
|
2825
|
+
* Fired once, only for a substantial turn that changed files and has already
|
|
2826
|
+
* been verified (the verification gate owns "did you check"). Its job is
|
|
2827
|
+
* orthogonal: catch a result that is verified but does not satisfy the WHOLE
|
|
2828
|
+
* original ask. It demands evidence from THIS turn and forbids padding the
|
|
2829
|
+
* answer with more prose — the failure mode it targets is a confident, verified
|
|
2830
|
+
* answer to a slightly wrong question.
|
|
2831
|
+
*/
|
|
2832
|
+
export function selfReviewNudge(ask) {
|
|
2833
|
+
const quoted = (ask || '').trim().replace(/\s+/g, ' ').slice(0, 300);
|
|
2834
|
+
return ('Before you finish, REVIEW your result against the ORIGINAL request' +
|
|
2835
|
+
(quoted ? `: "${quoted}".` : '.') +
|
|
2836
|
+
'\nAnswer these to yourself in one short pass, and fix anything that is not true:' +
|
|
2837
|
+
'\n 1. Does what you produced satisfy EVERY part of that request — not just the part you found easiest?' +
|
|
2838
|
+
'\n 2. Is each claim in your answer backed by a tool result from THIS turn — or is it an assumption you did not check?' +
|
|
2839
|
+
'\n 3. Is anything the request asked for still missing, half-done, or done for the wrong target?' +
|
|
2840
|
+
'\nIf everything is satisfied and evidenced, reply with a brief confirmation and stop.' +
|
|
2841
|
+
' If something is missing, do it NOW — or state plainly and specifically what is not done and why.' +
|
|
2842
|
+
' Do NOT restate the work in more words — verify it.');
|
|
2843
|
+
}
|
|
2151
2844
|
//# sourceMappingURL=tool-loop.js.map
|