atom-agent 1.1.0 → 1.3.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/CHANGELOG.md +106 -0
- package/README.md +18 -8
- package/atom.example.json +11 -0
- package/dist/App.js +1637 -255
- package/dist/adapters.js +112 -21
- package/dist/agent/gates.js +14 -1
- package/dist/agent/goal-evaluator.js +69 -0
- package/dist/agent/loop-guard.js +11 -13
- package/dist/agent/loop.js +716 -132
- package/dist/agent/normalize.js +9 -2
- package/dist/cli.js +25 -3
- package/dist/compact.js +169 -17
- package/dist/config.js +43 -7
- package/dist/context-manager.js +16 -198
- package/dist/context-windows.js +4 -2
- package/dist/env-block.js +46 -8
- package/dist/extension-commands.js +196 -0
- package/dist/extension-ui.js +153 -0
- package/dist/extensions.js +1571 -0
- package/dist/goal.js +583 -0
- package/dist/project-trust.js +96 -0
- package/dist/providers.js +6 -6
- package/dist/scheduler.js +159 -41
- package/dist/session.js +23 -5
- package/dist/sessions.js +543 -0
- package/dist/system.js +89 -13
- package/dist/telemetry-dashboard.js +28 -0
- package/dist/telemetry.js +39 -0
- package/dist/tools/compaction-hooks.js +165 -0
- package/dist/tools/custom.js +189 -0
- package/dist/tools/dir-cache.js +7 -0
- package/dist/tools/filesystem.js +3 -2
- package/dist/tools/intercept.js +145 -0
- package/dist/tools/overrides.js +105 -0
- package/dist/tools/provider-hooks.js +224 -0
- package/dist/tools/registry.js +247 -17
- package/dist/tools/ripgrep.js +256 -0
- package/dist/tools/search.js +119 -58
- package/dist/tools/shared.js +39 -0
- package/dist/tools/shell.js +7 -5
- package/dist/tools/web.js +6 -6
- package/dist/tools.js +45 -0
- package/dist/ui/diff-view.js +7 -2
- package/dist/ui/live-host.js +18 -0
- package/dist/ui/live-tail.js +9 -3
- package/dist/ui/markdown.js +26 -2
- package/dist/ui/palette.js +3 -1
- package/dist/ui/side-by-side.js +2 -2
- package/dist/ui/status-bar.js +80 -5
- package/dist/ui/status-host.js +22 -0
- package/dist/ui/stream-store.js +48 -0
- package/dist/ui/tool-inspector.js +7 -1
- package/dist/ui/transcript.js +92 -38
- package/dist/zen.js +370 -87
- package/documentation/architecture.md +114 -0
- package/documentation/cli.md +82 -0
- package/documentation/compaction.md +50 -0
- package/documentation/configuration.md +111 -0
- package/documentation/development.md +62 -0
- package/documentation/extensions.md +160 -0
- package/documentation/getting-started.md +63 -0
- package/documentation/goals.md +41 -0
- package/documentation/index.md +41 -0
- package/documentation/observability.md +70 -0
- package/documentation/permissions.md +66 -0
- package/documentation/providers.md +78 -0
- package/documentation/sessions.md +92 -0
- package/documentation/skills.md +57 -0
- package/documentation/tools.md +94 -0
- package/documentation/troubleshooting.md +54 -0
- package/examples/extensions/01-audit-gate.js +24 -0
- package/examples/extensions/02-notes-tool.js +32 -0
- package/examples/extensions/03-custom-command.js +32 -0
- package/package.json +6 -2
package/dist/adapters.js
CHANGED
|
@@ -6,12 +6,68 @@
|
|
|
6
6
|
// Normalized output matches zen ChatResult:
|
|
7
7
|
// {content, tool_calls:[{id,function:{name,arguments}}], usage?}
|
|
8
8
|
// so runAgenticLoop/retry/rollback/status code is untouched.
|
|
9
|
-
import {
|
|
9
|
+
import { allToolDefinitions } from "./tools.js";
|
|
10
10
|
import { getProvider, modelsUrlForProvider, } from "./providers.js";
|
|
11
11
|
export const ANTHROPIC_VERSION = "2023-06-01";
|
|
12
12
|
export const ANTHROPIC_MAX_TOKENS = 4096;
|
|
13
|
+
// ---- Reasoning-effort mappings (one /effort knob, three wire shapes) ----
|
|
14
|
+
//
|
|
15
|
+
// OpenAI-chat kind sends `reasoning_effort` verbatim (no mapping needed).
|
|
16
|
+
// Anthropic takes a thinking budget in tokens: minimum 1024, and it must
|
|
17
|
+
// stay under max_tokens for the turn (budgets count toward max_tokens).
|
|
18
|
+
// Gemini takes a thinkingLevel enum (minimal/low/medium/high): our Max maps
|
|
19
|
+
// to high, the deepest level the API offers.
|
|
20
|
+
// Minimum thinking budget Anthropic accepts (see extended-thinking docs).
|
|
21
|
+
export const ANTHROPIC_MIN_THINKING_BUDGET = 1024;
|
|
22
|
+
export const ANTHROPIC_EFFORT_BUDGETS = {
|
|
23
|
+
low: 1024,
|
|
24
|
+
medium: 2048,
|
|
25
|
+
high: 3072,
|
|
26
|
+
max: 3500,
|
|
27
|
+
};
|
|
28
|
+
// Budget for an effort level under a max_tokens cap, or undefined when the
|
|
29
|
+
// knob must be omitted (Auto/unknown effort, or a cap too small to fit the
|
|
30
|
+
// 1024 minimum — e.g. a tiny compaction cap). Oversized wants shrink to
|
|
31
|
+
// cap - 1 instead of 400ing.
|
|
32
|
+
export function anthropicThinkingFor(effort, maxTokens) {
|
|
33
|
+
if (!effort)
|
|
34
|
+
return undefined;
|
|
35
|
+
const want = ANTHROPIC_EFFORT_BUDGETS[effort];
|
|
36
|
+
if (want === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens))
|
|
39
|
+
return want;
|
|
40
|
+
const max = Math.floor(maxTokens);
|
|
41
|
+
if (want < max)
|
|
42
|
+
return want;
|
|
43
|
+
const shrunk = max - 1;
|
|
44
|
+
return shrunk >= ANTHROPIC_MIN_THINKING_BUDGET ? shrunk : undefined;
|
|
45
|
+
}
|
|
46
|
+
const GEMINI_EFFORT_LEVELS = {
|
|
47
|
+
low: "low",
|
|
48
|
+
medium: "medium",
|
|
49
|
+
high: "high",
|
|
50
|
+
// The API's deepest level is high — Max rides it.
|
|
51
|
+
max: "high",
|
|
52
|
+
};
|
|
53
|
+
// thinkingLevel for an effort level, or undefined when the knob must be
|
|
54
|
+
// omitted (Auto/unknown effort).
|
|
55
|
+
export function geminiThinkingLevelFor(effort) {
|
|
56
|
+
if (!effort)
|
|
57
|
+
return undefined;
|
|
58
|
+
return GEMINI_EFFORT_LEVELS[effort];
|
|
59
|
+
}
|
|
60
|
+
// Server-authoritative unsupported detection: a 400 that names the effort
|
|
61
|
+
// knob means this model/deployment has no such control, and the caller
|
|
62
|
+
// retries once without it. Deliberately narrow (knob names only) so
|
|
63
|
+
// unrelated 400s keep failing loudly instead of silently dropping effort.
|
|
64
|
+
export function isEffortRejection(errorText) {
|
|
65
|
+
return /reasoning_effort|reasoning effort|thinking_level|budget_tokens|\bthinking\b/i.test(errorText);
|
|
66
|
+
}
|
|
13
67
|
function toolDefs() {
|
|
14
|
-
|
|
68
|
+
// Builtins plus extension-registered custom tools, so non-OpenAI kinds
|
|
69
|
+
// see the same model-visible surface as the OpenAI-chat path.
|
|
70
|
+
return allToolDefinitions();
|
|
15
71
|
}
|
|
16
72
|
function parseArgsObject(raw) {
|
|
17
73
|
try {
|
|
@@ -318,6 +374,24 @@ async function collectSSEText(res) {
|
|
|
318
374
|
const body = res.body;
|
|
319
375
|
const decoder = new TextDecoder();
|
|
320
376
|
let rawText = "";
|
|
377
|
+
// Data-silence tracking (mirrors zen.readSSEMessage): queue comments and
|
|
378
|
+
// keep-alives carry bytes but no model output, so only chunks containing a
|
|
379
|
+
// `data:` line start refresh the clock. The tail window catches a marker
|
|
380
|
+
// split across chunk boundaries.
|
|
381
|
+
let lastDataAt = Date.now();
|
|
382
|
+
let tail = "";
|
|
383
|
+
const noteChunk = (chunkText) => {
|
|
384
|
+
const joined = tail + chunkText;
|
|
385
|
+
if (/(?:^|\n)data:/.test(joined))
|
|
386
|
+
lastDataAt = Date.now();
|
|
387
|
+
tail = joined.slice(-8);
|
|
388
|
+
};
|
|
389
|
+
const throwIfDataStalled = () => {
|
|
390
|
+
const budget = sseStallTimeoutMs();
|
|
391
|
+
if (Date.now() - lastDataAt > budget) {
|
|
392
|
+
throw new Error(`Truncated stream from model (stall: no output for ${budget}ms — queued or stalled upstream; resend to retry).`);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
321
395
|
if (body == null)
|
|
322
396
|
return { rawText, events: [] };
|
|
323
397
|
try {
|
|
@@ -346,10 +420,12 @@ async function collectSSEText(res) {
|
|
|
346
420
|
if (chunk.done)
|
|
347
421
|
break;
|
|
348
422
|
const v = chunk.value;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
423
|
+
const textPart = typeof v === "string"
|
|
424
|
+
? v
|
|
425
|
+
: decoder.decode(v, { stream: true });
|
|
426
|
+
rawText += textPart;
|
|
427
|
+
noteChunk(textPart);
|
|
428
|
+
throwIfDataStalled();
|
|
353
429
|
}
|
|
354
430
|
}
|
|
355
431
|
finally {
|
|
@@ -369,10 +445,12 @@ async function collectSSEText(res) {
|
|
|
369
445
|
if (step.done)
|
|
370
446
|
break;
|
|
371
447
|
const v = step.value;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
448
|
+
const textPart = typeof v === "string"
|
|
449
|
+
? v
|
|
450
|
+
: decoder.decode(v, { stream: true });
|
|
451
|
+
rawText += textPart;
|
|
452
|
+
noteChunk(textPart);
|
|
453
|
+
throwIfDataStalled();
|
|
376
454
|
}
|
|
377
455
|
}
|
|
378
456
|
finally {
|
|
@@ -428,16 +506,9 @@ function finiteCount(value) {
|
|
|
428
506
|
? Math.floor(value)
|
|
429
507
|
: undefined;
|
|
430
508
|
}
|
|
431
|
-
function openAIUsage(prompt, completion, cache) {
|
|
509
|
+
function openAIUsage(prompt, completion, cache, opts) {
|
|
432
510
|
const out = {};
|
|
433
511
|
const p = finiteCount(prompt);
|
|
434
|
-
if (p !== undefined)
|
|
435
|
-
out.prompt_tokens = p;
|
|
436
|
-
const c = finiteCount(completion);
|
|
437
|
-
if (c !== undefined)
|
|
438
|
-
out.completion_tokens = c;
|
|
439
|
-
if (p !== undefined && c !== undefined)
|
|
440
|
-
out.total_tokens = p + c;
|
|
441
512
|
// Provider-reported cache counters ride alongside (Anthropic
|
|
442
513
|
// cache_read/_creation, Gemini cachedContentTokenCount) — present-only.
|
|
443
514
|
const read = finiteCount(cache?.read);
|
|
@@ -446,6 +517,22 @@ function openAIUsage(prompt, completion, cache) {
|
|
|
446
517
|
const write = finiteCount(cache?.write);
|
|
447
518
|
if (write !== undefined)
|
|
448
519
|
out.cacheWriteTokens = write;
|
|
520
|
+
// Anthropic's input_tokens EXCLUDES cache_read/_creation (they are separate
|
|
521
|
+
// counters for the same context), while OpenAI/Gemini prompt counts already
|
|
522
|
+
// include cached tokens. With foldCacheIntoPrompt, prompt_tokens is
|
|
523
|
+
// normalized to total input-side tokens so load (P%), spend (NK), and the
|
|
524
|
+
// auto-compact trigger all see the real context. Detail fields above stay
|
|
525
|
+
// provider-faithful regardless.
|
|
526
|
+
const folded = opts?.foldCacheIntoPrompt === true
|
|
527
|
+
? [p, read, write].reduce((acc, v) => (v === undefined ? acc : (acc ?? 0) + v), undefined)
|
|
528
|
+
: p;
|
|
529
|
+
if (folded !== undefined)
|
|
530
|
+
out.prompt_tokens = folded;
|
|
531
|
+
const c = finiteCount(completion);
|
|
532
|
+
if (c !== undefined)
|
|
533
|
+
out.completion_tokens = c;
|
|
534
|
+
if (folded !== undefined && c !== undefined)
|
|
535
|
+
out.total_tokens = folded + c;
|
|
449
536
|
return out.prompt_tokens !== undefined ||
|
|
450
537
|
out.completion_tokens !== undefined ||
|
|
451
538
|
out.total_tokens !== undefined
|
|
@@ -524,10 +611,12 @@ export async function readAnthropicSSEMessage(res, opts) {
|
|
|
524
611
|
const msg = o["message"];
|
|
525
612
|
const u = msg?.["usage"];
|
|
526
613
|
if (u) {
|
|
614
|
+
// Anthropic input_tokens excludes cache_read/_creation — fold them
|
|
615
|
+
// into prompt_tokens so the value is total input-side tokens.
|
|
527
616
|
const hit = openAIUsage(u["input_tokens"], u["output_tokens"], {
|
|
528
617
|
read: u["cache_read_input_tokens"],
|
|
529
618
|
write: u["cache_creation_input_tokens"],
|
|
530
|
-
});
|
|
619
|
+
}, { foldCacheIntoPrompt: true });
|
|
531
620
|
const merged = mergeUsage(usage, hit, { recomputeTotal: true });
|
|
532
621
|
if (merged !== undefined)
|
|
533
622
|
usage = merged;
|
|
@@ -609,10 +698,11 @@ export async function readAnthropicSSEMessage(res, opts) {
|
|
|
609
698
|
const inputSrc = u?.["input_tokens"] !== undefined ? u["input_tokens"] : o["input_tokens"];
|
|
610
699
|
const outputSrc = u?.["output_tokens"] !== undefined ? u["output_tokens"] : o["output_tokens"];
|
|
611
700
|
if (u !== undefined || o["input_tokens"] !== undefined || o["output_tokens"] !== undefined) {
|
|
701
|
+
// Same Anthropic-exclusive-cache fold as message_start above.
|
|
612
702
|
const hit = openAIUsage(inputSrc, outputSrc, {
|
|
613
703
|
read: u?.["cache_read_input_tokens"] ?? o["cache_read_input_tokens"],
|
|
614
704
|
write: u?.["cache_creation_input_tokens"] ?? o["cache_creation_input_tokens"],
|
|
615
|
-
});
|
|
705
|
+
}, { foldCacheIntoPrompt: true });
|
|
616
706
|
const merged = mergeUsage(usage, hit, { recomputeTotal: true });
|
|
617
707
|
if (merged !== undefined)
|
|
618
708
|
usage = merged;
|
|
@@ -731,10 +821,11 @@ export function parseAnthropicJson(data) {
|
|
|
731
821
|
};
|
|
732
822
|
const u = o["usage"];
|
|
733
823
|
if (u) {
|
|
824
|
+
// Same Anthropic-exclusive-cache fold as the SSE path above.
|
|
734
825
|
const hit = openAIUsage(u["input_tokens"], u["output_tokens"], {
|
|
735
826
|
read: u["cache_read_input_tokens"],
|
|
736
827
|
write: u["cache_creation_input_tokens"],
|
|
737
|
-
});
|
|
828
|
+
}, { foldCacheIntoPrompt: true });
|
|
738
829
|
if (hit)
|
|
739
830
|
result.usage = hit;
|
|
740
831
|
}
|
package/dist/agent/gates.js
CHANGED
|
@@ -19,6 +19,15 @@ export function todoCompletionGate(finalText, ctx) {
|
|
|
19
19
|
finalText: `${finalText}${finalText ? "\n" : ""}(blocked: ${open.length} open todo(s) — resolve with todo_update/todowrite before ending the turn:\n${items})`,
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
// Guard cycles are bounded (like the verification gate): a model that
|
|
23
|
+
// keeps answering without resolving todos ends with a blocked statement
|
|
24
|
+
// instead of looping forever. Normal flows resolve within a round or two.
|
|
25
|
+
if ((ctx.todoRounds ?? 0) >= MAX_TODO_ROUNDS) {
|
|
26
|
+
return {
|
|
27
|
+
action: "end",
|
|
28
|
+
finalText: `${finalText}${finalText ? "\n" : ""}(blocked: ${open.length} open todo(s) remain after ${MAX_TODO_ROUNDS} guard rounds — resolve with todo_update/todowrite before ending the turn:\n${items})`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
22
31
|
return {
|
|
23
32
|
action: "continue",
|
|
24
33
|
assistantText: finalText,
|
|
@@ -30,11 +39,15 @@ export function todoCompletionGate(finalText, ctx) {
|
|
|
30
39
|
// report — the system prompt forbids unverified finishes, so the runtime
|
|
31
40
|
// must not terminate while just labeling): the attempt is recorded and a
|
|
32
41
|
// verification follow-up re-enters the loop, exactly like the todo guard.
|
|
33
|
-
// Two bounded exits:
|
|
42
|
+
// Two bounded exits: an explicit step budget spent, or MAX_VERIFY_ROUNDS nag
|
|
34
43
|
// without a passing run — both end with an explicit labeled statement naming
|
|
35
44
|
// what is unverified and why the loop stopped. Turns with no code writes
|
|
36
45
|
// (questions, docs, explanations, read-only work) are unaffected.
|
|
37
46
|
export const MAX_VERIFY_ROUNDS = 3;
|
|
47
|
+
// Todo-guard continues before the turn ends blocked: a model that keeps
|
|
48
|
+
// answering final text without resolving open todos is sent back at most
|
|
49
|
+
// this many times. Mirrors MAX_VERIFY_ROUNDS so no gate can spin forever.
|
|
50
|
+
export const MAX_TODO_ROUNDS = 3;
|
|
38
51
|
// Source-code extensions whose writes require a passing verification run.
|
|
39
52
|
// Curated heuristic boundary (not a parser): docs, configs, data, and
|
|
40
53
|
// extensionless files never arm the gate, so a README edit finishes clean.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Goal evaluator fallback (ticket 04): one bounded, read-only judge call
|
|
2
|
+
// for a goal turn that ends with no disposition report. Follows the
|
|
3
|
+
// compaction summary-POST precedent (requestCompactSummary in src/compact.ts):
|
|
4
|
+
// same provider/model, tools disabled, capped output, single attempt, throws
|
|
5
|
+
// on failure (the loop turns that into a pause, never a crash).
|
|
6
|
+
//
|
|
7
|
+
// Split of responsibilities: the loop owns WHEN (report-less turn end) and
|
|
8
|
+
// WHAT NEXT (consumed verdicts flow through the model-report path); this
|
|
9
|
+
// module owns the request shape, the judge prompt, and the transport. App
|
|
10
|
+
// builds the full request from its live provider/key/model refs; unit tests
|
|
11
|
+
// inject a fake GoalJudgeRunner through AgenticOpts without fetch mocks.
|
|
12
|
+
import { chatCompletionForProvider } from "../zen.js";
|
|
13
|
+
import { parseGoalJudgeVerdict } from "../goal.js";
|
|
14
|
+
// Output cap for the judge POST: a verdict is a few dozen tokens of JSON —
|
|
15
|
+
// 256 leaves headroom for a short reason without letting the judge write prose.
|
|
16
|
+
export const GOAL_JUDGE_MAX_TOKENS = 256;
|
|
17
|
+
// The judge instruction: goal text plus the demand for one strict
|
|
18
|
+
// machine-readable verdict. No tools are offered, so there is nothing to
|
|
19
|
+
// call — the transcript above is the only evidence.
|
|
20
|
+
export function buildGoalJudgeInstruction(goal) {
|
|
21
|
+
return (`You are judging whether a coding-goal turn advanced. Goal: "${goal}".\n` +
|
|
22
|
+
`The recent transcript turns are above. Decide the turn outcome and answer ` +
|
|
23
|
+
`with a single JSON object only (no prose, no code fences):\n` +
|
|
24
|
+
`{"status": "continue", "next": "<concrete next action>"} — work remains; name the next action.\n` +
|
|
25
|
+
`{"status": "complete", "reason": "<why the goal is done>"} — the goal is fully achieved.\n` +
|
|
26
|
+
`{"status": "blocked", "reason": "<what blocks progress>"} — a genuine blocker, not mere remaining work.\n` +
|
|
27
|
+
`Rules: no tools are available for this request — judge from the transcript only, ` +
|
|
28
|
+
`answer with the JSON object only.`);
|
|
29
|
+
}
|
|
30
|
+
export function buildGoalJudgeMessages(systemContent, goal, turns) {
|
|
31
|
+
const system = typeof systemContent === "string" && systemContent.length > 0
|
|
32
|
+
? systemContent
|
|
33
|
+
: "You are a concise engineering judge.";
|
|
34
|
+
return [
|
|
35
|
+
{ role: "system", content: system },
|
|
36
|
+
...turns.map((m) => ({ ...m })),
|
|
37
|
+
{ role: "user", content: buildGoalJudgeInstruction(goal) },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
// Judge POST: SAME provider/model via the existing chat path but TOOLS
|
|
41
|
+
// DISABLED (no `tools` key in the POST body) and output capped
|
|
42
|
+
// (max_tokens/maxOutputTokens per kind — see zen.ts/adapters.ts). Single
|
|
43
|
+
// attempt, no retry — bounded by construction. Returns the parsed verdict,
|
|
44
|
+
// or null when the judge output is anything but a clear verdict (tolerant
|
|
45
|
+
// parsing lives in parseGoalJudgeVerdict). Transport failures and empty
|
|
46
|
+
// replies throw with history untouched (the loop pauses on them).
|
|
47
|
+
export async function requestGoalVerdict(req) {
|
|
48
|
+
const messages = buildGoalJudgeMessages(req.systemContent, req.goal, req.turns);
|
|
49
|
+
const res = await chatCompletionForProvider(req.provider, req.apiKey, req.model, messages, {
|
|
50
|
+
baseURL: req.baseURL,
|
|
51
|
+
endpointOverride: req.endpointOverride,
|
|
52
|
+
disableTools: true,
|
|
53
|
+
maxOutputTokens: req.maxOutputTokens ?? GOAL_JUDGE_MAX_TOKENS,
|
|
54
|
+
...(req.signal ? { signal: req.signal } : {}),
|
|
55
|
+
});
|
|
56
|
+
// Totals keep accumulating: forward real judge usage when present.
|
|
57
|
+
if (res.usage !== undefined) {
|
|
58
|
+
try {
|
|
59
|
+
req.onUsage?.(res.usage);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// observer errors never break the judge
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const text = (res.content ?? "").trim();
|
|
66
|
+
if (!text)
|
|
67
|
+
throw new Error("Empty reply from model (unexpected payload).");
|
|
68
|
+
return parseGoalJudgeVerdict(text);
|
|
69
|
+
}
|
package/dist/agent/loop-guard.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
// Loop-guard: repetition/runaway detection + error-streak recovery for the
|
|
2
2
|
// agentic loop. Pure state machines, no I/O, never throw.
|
|
3
3
|
//
|
|
4
|
-
// Why this exists:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// Why this exists: turns are uncapped by default, so a model stuck calling
|
|
5
|
+
// `read <same path>` forever burns POSTs without end. The guard spots the
|
|
6
|
+
// pattern early (consecutive identical signatures) and the loop nudges the
|
|
7
|
+
// model toward a different approach with a bounded follow-up — then stops
|
|
8
|
+
// hard if the pattern survives the nudges. Error streaks get the same
|
|
9
|
+
// treatment: ending on 3+ unaddressed `Error:` results is almost always
|
|
10
|
+
// premature, so the loop asks for a fix-forward attempt before accepting
|
|
11
|
+
// final text.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// notice". Error-streak recovery defaults to 3 (single errors still end
|
|
17
|
-
// normally — the model may be reporting a blocker).
|
|
13
|
+
// Repetition intervention is OPT-IN (maxRepeatedCalls set by the caller;
|
|
14
|
+
// unset = track-only for stats). Error-streak recovery defaults to 3
|
|
15
|
+
// (single errors still end normally — the model may be reporting a blocker).
|
|
18
16
|
//
|
|
19
17
|
// All thresholds clamp to sane minima; every method is safe to call with any
|
|
20
18
|
// input.
|