@oh-my-pi/pi-coding-agent 16.4.6 → 16.5.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 +88 -0
- package/dist/cli.js +3312 -3254
- package/dist/types/cli/args.d.ts +5 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +15 -0
- package/dist/types/config/model-resolver.d.ts +4 -3
- package/dist/types/config/model-roles.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +46 -14
- package/dist/types/extensibility/extensions/types.d.ts +1 -1
- package/dist/types/launch/broker.d.ts +2 -0
- package/dist/types/launch/client.d.ts +22 -0
- package/dist/types/launch/paths.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +8 -0
- package/dist/types/launch/protocol.d.ts +170 -0
- package/dist/types/launch/terminal-output.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -1
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-browser.d.ts +28 -3
- package/dist/types/modes/components/model-hub.d.ts +0 -10
- package/dist/types/modes/components/model-picker.d.ts +44 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +14 -8
- package/dist/types/modes/print-mode.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +42 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/thinking.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
- package/dist/types/tools/browser/tab-worker.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/launch.d.ts +121 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/terminal-output.d.ts +5 -0
- package/dist/types/vibe/runtime.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +0 -8
- package/package.json +20 -20
- package/src/cli/args.ts +11 -0
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gallery-fixtures/shell.ts +82 -1
- package/src/cli.ts +10 -0
- package/src/commands/launch.ts +17 -0
- package/src/config/model-resolver.ts +110 -31
- package/src/config/model-roles.ts +14 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +2 -2
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/completion-bridge.ts +4 -4
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/extensions/model-api.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/internal-urls/docs-index.ts +4 -4
- package/src/launch/broker.ts +1017 -0
- package/src/launch/client.ts +344 -0
- package/src/launch/paths.ts +17 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +386 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/main.ts +50 -1
- package/src/modes/acp/acp-agent.ts +8 -1
- package/src/modes/components/agent-hub.ts +101 -31
- package/src/modes/components/compaction-summary-message.ts +8 -2
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/model-browser.ts +152 -49
- package/src/modes/components/model-hub.ts +55 -142
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
- package/src/modes/components/status-line/component.test.ts +41 -2
- package/src/modes/components/status-line/component.ts +4 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +9 -1
- package/src/modes/controllers/event-controller.ts +31 -32
- package/src/modes/controllers/selector-controller.ts +96 -22
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +20 -3
- package/src/modes/print-mode.test.ts +71 -0
- package/src/modes/print-mode.ts +51 -2
- package/src/modes/theme/theme.ts +9 -0
- package/src/modes/utils/ui-helpers.ts +25 -4
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/scout.md +1 -1
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/system-prompt.md +9 -7
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/launch.md +25 -0
- package/src/sdk.ts +8 -2
- package/src/session/agent-session.ts +550 -87
- package/src/session/session-context.test.ts +10 -5
- package/src/session/session-context.ts +25 -8
- package/src/session/session-entries.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +25 -0
- package/src/task/agents.ts +2 -2
- package/src/thinking.ts +10 -3
- package/src/tiny/models.ts +7 -7
- package/src/tools/bash-interactive.ts +5 -8
- package/src/tools/bash.ts +38 -24
- package/src/tools/browser/cmux/cmux-tab.ts +17 -2
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +66 -6
- package/src/tools/browser/tab-protocol.ts +6 -0
- package/src/tools/browser/tab-supervisor.ts +17 -1
- package/src/tools/browser/tab-worker.ts +140 -21
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/index.ts +5 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/launch.ts +643 -0
- package/src/tools/render-utils.ts +3 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tts/speech-enhancer.ts +2 -2
- package/src/utils/image-vision-fallback.ts +3 -3
- package/src/vibe/runtime.ts +2 -2
- package/src/web/search/provider.ts +0 -10
- package/src/web/search/providers/perplexity.ts +18 -2
- package/src/web/search/providers/public.ts +2 -4
- package/src/web/search/types.ts +0 -10
- package/dist/types/web/search/providers/bing.d.ts +0 -14
- package/dist/types/web/search/providers/yahoo.d.ts +0 -14
- package/src/web/search/providers/bing.ts +0 -197
- package/src/web/search/providers/yahoo.ts +0 -179
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type AgentMessage,
|
|
31
31
|
type AgentState,
|
|
32
32
|
type AgentTool,
|
|
33
|
+
type AgentToolResult,
|
|
33
34
|
type AgentTurnEndContext,
|
|
34
35
|
AppendOnlyContextManager,
|
|
35
36
|
type AsideMessage,
|
|
@@ -250,6 +251,7 @@ import { parseTurnBudget } from "../modes/turn-budget";
|
|
|
250
251
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
251
252
|
import { computeNonMessageBreakdown, computeNonMessageTokens } from "../modes/utils/context-usage";
|
|
252
253
|
import { containsWorkflow, renderWorkflowNotice } from "../modes/workflow";
|
|
254
|
+
import { resolveApprovedPlan } from "../plan-mode/approved-plan";
|
|
253
255
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
254
256
|
import type { PlanModeState } from "../plan-mode/state";
|
|
255
257
|
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
@@ -270,6 +272,10 @@ import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" w
|
|
|
270
272
|
import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool-decision-reminder.md" with {
|
|
271
273
|
type: "text",
|
|
272
274
|
};
|
|
275
|
+
import planYoloHandoffPrompt from "../prompts/system/plan-yolo-handoff.md" with { type: "text" };
|
|
276
|
+
import prewalkChecklistPrompt from "../prompts/system/prewalk-checklist.md" with { type: "text" };
|
|
277
|
+
import prewalkContinuePrompt from "../prompts/system/prewalk-continue.md" with { type: "text" };
|
|
278
|
+
import prewalkPlanPrompt from "../prompts/system/prewalk-plan.md" with { type: "text" };
|
|
273
279
|
import rewindReportTemplate from "../prompts/system/rewind-report.md" with { type: "text" };
|
|
274
280
|
import sideChannelNoToolsReminder from "../prompts/system/side-channel-no-tools.md" with { type: "text" };
|
|
275
281
|
import thinkingLoopRedirectTemplate from "../prompts/system/thinking-loop-redirect.md" with { type: "text" };
|
|
@@ -318,7 +324,7 @@ import type { CheckpointState, CompletedRewindState } from "../tools/checkpoint"
|
|
|
318
324
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
319
325
|
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
320
326
|
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
|
321
|
-
import { buildResolveReminderMessage } from "../tools/resolve";
|
|
327
|
+
import { buildResolveReminderMessage, type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
322
328
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo";
|
|
323
329
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
324
330
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
@@ -413,7 +419,31 @@ const MID_RUN_TODO_NUDGE_MUTATING_TOOLS: Record<string, true> = {
|
|
|
413
419
|
/** `customType` for the hidden mid-run todo nudge; `display: false`, so it reaches
|
|
414
420
|
* the model but never renders in the TUI or transcript. */
|
|
415
421
|
const MID_RUN_TODO_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
|
|
416
|
-
|
|
422
|
+
/** Hidden plan nudge injected by prewalk; scrubbed from the LLM context
|
|
423
|
+
* when the switch happens. */
|
|
424
|
+
const PREWALK_PLAN_MESSAGE_TYPE = "prewalk-plan";
|
|
425
|
+
/** Hidden safety-net nudge forcing one more turn after a text-only reply to
|
|
426
|
+
* the plan nudge, which would otherwise end the run with no code written. */
|
|
427
|
+
const PREWALK_CONTINUE_MESSAGE_TYPE = "prewalk-continue";
|
|
428
|
+
/** Hidden "verify before finishing" checklist steered into the run at the
|
|
429
|
+
* switch, aimed at the fast model's specific failure patterns: partial
|
|
430
|
+
* multi-site fixes, unnecessarily broad rewrites, and reported-test-only
|
|
431
|
+
* verification. */
|
|
432
|
+
const PREWALK_CHECKLIST_MESSAGE_TYPE = "prewalk-checklist";
|
|
433
|
+
/** Tools whose first successful call triggers the switch — once the todo
|
|
434
|
+
* gate is open (see {@link AgentSession.#prewalkTodoSeen}). Bash is
|
|
435
|
+
* deliberately excluded: it doubles as exploration (ls/cat) and fired
|
|
436
|
+
* turn-1 switches in practice. `todo` is deliberately NOT a trigger: firing
|
|
437
|
+
* at the todo init handed the fast model 100% of the implementation with
|
|
438
|
+
* zero started work and measurably regressed pass rates. */
|
|
439
|
+
const PREWALK_ACTION_TOOLS: Record<string, true> = {
|
|
440
|
+
edit: true,
|
|
441
|
+
write: true,
|
|
442
|
+
};
|
|
443
|
+
/** `customType` for the hidden hand-off message steered to the target model
|
|
444
|
+
* once PlanYolo auto-approves the plan. Unlike prewalk's plan nudge this
|
|
445
|
+
* is never scrubbed — it IS the instruction the target model acts on. */
|
|
446
|
+
const PLAN_YOLO_HANDOFF_MESSAGE_TYPE = "plan-yolo-handoff";
|
|
417
447
|
/** Abort reason for the Gemini reasoning-header runaway interrupt. Surfaced on the
|
|
418
448
|
* discarded assistant turn only; never reaches the model. */
|
|
419
449
|
const GEMINI_HEADER_INTERRUPT_REASON = "Interrupted: emit a tool call instead of more planning";
|
|
@@ -599,8 +629,9 @@ const COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION: CompactionCheckResult = {
|
|
|
599
629
|
|
|
600
630
|
/**
|
|
601
631
|
* User-facing notice for a compaction dead end: maintenance freed too little
|
|
602
|
-
* to retry safely. `remedies` names the recovery actions
|
|
603
|
-
*
|
|
632
|
+
* to retry safely. `remedies` names the recovery actions left on the emitting
|
|
633
|
+
* path — by the time the post-pass dead end fires, the tiered rescue has
|
|
634
|
+
* already attempted both elide and image-drop automatically.
|
|
604
635
|
*/
|
|
605
636
|
function compactionDeadEndWarning(remedies: string): string {
|
|
606
637
|
return (
|
|
@@ -680,6 +711,34 @@ export interface AsyncJobSnapshot {
|
|
|
680
711
|
}
|
|
681
712
|
|
|
682
713
|
export type { ShakeMode, ShakeResult };
|
|
714
|
+
/**
|
|
715
|
+
* Prewalk: switches an active session one-way from its starting model to
|
|
716
|
+
* a fast/cheap `target` at the first completed turn that runs an edit/write
|
|
717
|
+
* tool once the todo list exists. A hidden plan nudge asks the starting
|
|
718
|
+
* model to write a plan, initialize its todo list from it, and start; the
|
|
719
|
+
* todo call opens the trigger gate (it never fires the switch itself), so
|
|
720
|
+
* the starting model always begins the implementation. A hidden
|
|
721
|
+
* checklist nudge asks the target model to verify its work before
|
|
722
|
+
* finishing. Both are always on — this is the one mechanism that won out
|
|
723
|
+
* over turn-count and ungated variants in testing.
|
|
724
|
+
*/
|
|
725
|
+
export interface Prewalk {
|
|
726
|
+
target: Model;
|
|
727
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* PlanYolo: forces the session into read-only plan mode at start, then
|
|
732
|
+
* auto-approves the plan the instant the model calls `resolve({ action:
|
|
733
|
+
* "apply" })` for it — no interactive review — and switches to a fast/cheap
|
|
734
|
+
* `target` model to implement it. The headless counterpart to interactive
|
|
735
|
+
* plan mode's "Approve and execute", for print/non-interactive runs where
|
|
736
|
+
* there is no one to click Approve.
|
|
737
|
+
*/
|
|
738
|
+
export interface PlanYolo {
|
|
739
|
+
target: Model;
|
|
740
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
741
|
+
}
|
|
683
742
|
|
|
684
743
|
// ============================================================================
|
|
685
744
|
// Types
|
|
@@ -695,6 +754,12 @@ export interface AgentSessionConfig {
|
|
|
695
754
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
696
755
|
/** Initial session thinking selector. */
|
|
697
756
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
757
|
+
/** Prewalk from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
|
|
758
|
+
prewalk?: Prewalk;
|
|
759
|
+
/** Force read-only plan mode at start, auto-approve on the model's first
|
|
760
|
+
* `resolve` call, then switch to the target to implement. */
|
|
761
|
+
planYolo?: PlanYolo;
|
|
762
|
+
|
|
698
763
|
/** Initial per-family service tiers (OpenAI / Anthropic / Google) for the live session. */
|
|
699
764
|
serviceTierByFamily?: ServiceTierByFamily;
|
|
700
765
|
/** Prompt templates for expansion */
|
|
@@ -1607,6 +1672,17 @@ export class AgentSession {
|
|
|
1607
1672
|
#autoThinking: boolean = false;
|
|
1608
1673
|
/** The level `auto` last resolved to (for UI); undefined until a turn is classified. */
|
|
1609
1674
|
#autoResolvedLevel: Effort | undefined;
|
|
1675
|
+
#prewalk: Prewalk | undefined;
|
|
1676
|
+
/** True once the plan nudge has been queued; scrubbed from context at the switch. */
|
|
1677
|
+
#prewalkPlanInjected = false;
|
|
1678
|
+
/** True once any successful `todo` call landed — opens the prewalk
|
|
1679
|
+
* trigger gate: the switch fires at the first edit/write AFTER the todo
|
|
1680
|
+
* list exists (sessions without a todo tool skip the gate). */
|
|
1681
|
+
#prewalkTodoSeen = false;
|
|
1682
|
+
#planYolo: PlanYolo | undefined;
|
|
1683
|
+
#planYoloPreviousTools: string[] | undefined;
|
|
1684
|
+
#planYoloArmed = false;
|
|
1685
|
+
|
|
1610
1686
|
#promptTemplates: PromptTemplate[];
|
|
1611
1687
|
#slashCommands: FileSlashCommand[];
|
|
1612
1688
|
|
|
@@ -2091,6 +2167,253 @@ export class AgentSession {
|
|
|
2091
2167
|
this.#emit(pending);
|
|
2092
2168
|
}
|
|
2093
2169
|
|
|
2170
|
+
/** Advance the one-way prewalk switch at a completed assistant-turn boundary. */
|
|
2171
|
+
async #advancePrewalk(liveMessages: AgentMessage[], context: AgentTurnEndContext | undefined): Promise<void> {
|
|
2172
|
+
const prewalk = this.#prewalk;
|
|
2173
|
+
if (!prewalk || context?.message.role !== "assistant") return;
|
|
2174
|
+
|
|
2175
|
+
// Structural safety net: every branch below assumes the agent loop will
|
|
2176
|
+
// run another turn. It won't if THIS turn had no tool calls — the loop
|
|
2177
|
+
// treats a text-only turn as "the agent is done" and ends the session
|
|
2178
|
+
// with no further prompting. The plan nudge explicitly asks for a prose
|
|
2179
|
+
// reply, which makes a text-only turn common right after it — observed
|
|
2180
|
+
// silently killing production SWE-bench runs before any code was ever
|
|
2181
|
+
// written. Force one more turn only in that specific, self-created
|
|
2182
|
+
// hazard window.
|
|
2183
|
+
if (this.#prewalkPlanInjected && context.toolResults.length === 0) {
|
|
2184
|
+
this.agent.steer({
|
|
2185
|
+
role: "custom",
|
|
2186
|
+
customType: PREWALK_CONTINUE_MESSAGE_TYPE,
|
|
2187
|
+
content: prewalkContinuePrompt,
|
|
2188
|
+
attribution: "agent",
|
|
2189
|
+
display: false,
|
|
2190
|
+
timestamp: Date.now(),
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Todo gate: the plan nudge instructs "finish the plan, then init the
|
|
2195
|
+
// todo list from it and start" — so the switch waits until a todo list
|
|
2196
|
+
// exists AND the model has actually started implementing (first
|
|
2197
|
+
// edit/write). The todo call itself never triggers: firing there handed
|
|
2198
|
+
// the fast model the whole implementation cold. Sessions without a todo
|
|
2199
|
+
// tool skip the gate.
|
|
2200
|
+
if (context.toolResults.some(result => result.toolName === "todo")) {
|
|
2201
|
+
this.#prewalkTodoSeen = true;
|
|
2202
|
+
}
|
|
2203
|
+
const todoGateOpen = this.#prewalkTodoSeen || !this.#toolRegistry.has("todo");
|
|
2204
|
+
const action = todoGateOpen
|
|
2205
|
+
? context.toolResults.find(result => PREWALK_ACTION_TOOLS[result.toolName])
|
|
2206
|
+
: undefined;
|
|
2207
|
+
if (!action) {
|
|
2208
|
+
if (!this.#prewalkPlanInjected) {
|
|
2209
|
+
this.#prewalkPlanInjected = true;
|
|
2210
|
+
this.agent.steer({
|
|
2211
|
+
role: "custom",
|
|
2212
|
+
customType: PREWALK_PLAN_MESSAGE_TYPE,
|
|
2213
|
+
content: prewalkPlanPrompt,
|
|
2214
|
+
display: false,
|
|
2215
|
+
attribution: "agent",
|
|
2216
|
+
timestamp: Date.now(),
|
|
2217
|
+
});
|
|
2218
|
+
this.emitNotice("info", "Prewalk: injected deep-plan nudge.", "prewalk");
|
|
2219
|
+
}
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
await this.#waitForSessionMessagePersistence(context.message);
|
|
2224
|
+
for (const toolResult of context.toolResults) {
|
|
2225
|
+
await this.#waitForSessionMessagePersistence(toolResult);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
this.#scrubPrewalkPlanNudge(liveMessages);
|
|
2229
|
+
const target = prewalk.target;
|
|
2230
|
+
if (this.model && modelsAreEqual(this.model, target)) {
|
|
2231
|
+
this.#prewalk = undefined;
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
await this.setModelTemporary(target, prewalk.thinkingLevel, { ephemeral: true });
|
|
2236
|
+
this.#prewalk = undefined;
|
|
2237
|
+
this.emitNotice(
|
|
2238
|
+
"info",
|
|
2239
|
+
`Prewalk: switched to ${target.provider}/${target.id} after first ${action.toolName} call.`,
|
|
2240
|
+
"prewalk",
|
|
2241
|
+
);
|
|
2242
|
+
this.agent.steer({
|
|
2243
|
+
role: "custom",
|
|
2244
|
+
customType: PREWALK_CHECKLIST_MESSAGE_TYPE,
|
|
2245
|
+
content: prewalkChecklistPrompt,
|
|
2246
|
+
attribution: "agent",
|
|
2247
|
+
display: false,
|
|
2248
|
+
timestamp: Date.now(),
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
/**
|
|
2253
|
+
* Arm prewalk outside the normal startup path (the `/prewalk` slash
|
|
2254
|
+
* command): sets the target and immediately steers the plan nudge rather
|
|
2255
|
+
* than waiting for the next turn boundary, since an explicit manual
|
|
2256
|
+
* invocation means "start this now." A no-op with a notice if a prewalk
|
|
2257
|
+
* is already armed and waiting.
|
|
2258
|
+
*/
|
|
2259
|
+
armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel): void {
|
|
2260
|
+
if (this.#prewalk) {
|
|
2261
|
+
this.emitNotice(
|
|
2262
|
+
"info",
|
|
2263
|
+
`Prewalk: already armed for ${this.#prewalk.target.provider}/${this.#prewalk.target.id}, waiting for the first edit/write.`,
|
|
2264
|
+
"prewalk",
|
|
2265
|
+
);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
this.#prewalk = { target, thinkingLevel };
|
|
2269
|
+
this.#prewalkPlanInjected = true;
|
|
2270
|
+
this.agent.steer({
|
|
2271
|
+
role: "custom",
|
|
2272
|
+
customType: PREWALK_PLAN_MESSAGE_TYPE,
|
|
2273
|
+
content: prewalkPlanPrompt,
|
|
2274
|
+
display: false,
|
|
2275
|
+
attribution: "agent",
|
|
2276
|
+
timestamp: Date.now(),
|
|
2277
|
+
});
|
|
2278
|
+
this.emitNotice(
|
|
2279
|
+
"info",
|
|
2280
|
+
`Prewalk: armed for ${target.provider}/${target.id} — will switch at the first edit/write once the todo list exists.`,
|
|
2281
|
+
"prewalk",
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* Remove the plan nudge from the LLM context before the model switch: the
|
|
2287
|
+
* fast model inherits the plan the nudge produced, not the nudge itself.
|
|
2288
|
+
* Splices the loop's live context array in place (the run streams from
|
|
2289
|
+
* it) and mirrors the removal into agent state. The persisted transcript
|
|
2290
|
+
* keeps the message for audit; a session reload re-materializes it,
|
|
2291
|
+
* which is acceptable for prewalk's single-run lifecycle.
|
|
2292
|
+
*/
|
|
2293
|
+
#scrubPrewalkPlanNudge(liveMessages: AgentMessage[]): void {
|
|
2294
|
+
if (!this.#prewalkPlanInjected) return;
|
|
2295
|
+
const isPlanNudge = (m: AgentMessage): boolean =>
|
|
2296
|
+
m.role === "custom" && m.customType === PREWALK_PLAN_MESSAGE_TYPE;
|
|
2297
|
+
for (let i = liveMessages.length - 1; i >= 0; i--) {
|
|
2298
|
+
if (isPlanNudge(liveMessages[i])) liveMessages.splice(i, 1);
|
|
2299
|
+
}
|
|
2300
|
+
const stateMessages = this.agent.state.messages;
|
|
2301
|
+
const filtered = stateMessages.filter(m => !isPlanNudge(m));
|
|
2302
|
+
if (filtered.length !== stateMessages.length) this.agent.replaceMessages(filtered);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
/**
|
|
2306
|
+
* Lazily arm PlanYolo before the first prompt is built: restricts tools to
|
|
2307
|
+
* the plan-mode read-only set (plus `resolve`/`write`, both normally
|
|
2308
|
+
* discovery-hidden), marks plan-mode state so `#buildPlanModeMessage`
|
|
2309
|
+
* injects the standard plan-mode-active instructions on this and every
|
|
2310
|
+
* following prompt, and registers the auto-approve resolve handler.
|
|
2311
|
+
* Idempotent — a no-op once armed or when PlanYolo is not configured.
|
|
2312
|
+
*/
|
|
2313
|
+
async #armPlanYoloIfNeeded(): Promise<void> {
|
|
2314
|
+
if (!this.#planYolo || this.#planYoloArmed) return;
|
|
2315
|
+
this.#planYoloArmed = true;
|
|
2316
|
+
const previousTools = this.getActiveToolNames();
|
|
2317
|
+
const augmentations = ["resolve"];
|
|
2318
|
+
if (this.hasBuiltInTool("write")) augmentations.push("write");
|
|
2319
|
+
await this.setActiveToolsByName([...new Set([...previousTools, ...augmentations])]);
|
|
2320
|
+
this.#planYoloPreviousTools = previousTools;
|
|
2321
|
+
this.setPlanModeState({
|
|
2322
|
+
enabled: true,
|
|
2323
|
+
planFilePath: this.getPlanReferencePath() || "local://PLAN.md",
|
|
2324
|
+
workflow: "parallel",
|
|
2325
|
+
});
|
|
2326
|
+
this.setStandingResolveHandler(input => this.#runPlanYoloApprovalResolve(input));
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Standing resolve handler while PlanYolo's plan phase is active. Auto-
|
|
2331
|
+
* approves the instant the model calls `resolve { action: "apply" }` for
|
|
2332
|
+
* the plan — no interactive review, the headless counterpart to plan
|
|
2333
|
+
* mode's "Approve and execute" — then restores tools, exits plan-mode
|
|
2334
|
+
* state, switches to the configured `target`, and hands off the approved
|
|
2335
|
+
* plan for it to implement.
|
|
2336
|
+
*/
|
|
2337
|
+
#runPlanYoloApprovalResolve(input: unknown): Promise<AgentToolResult<ResolveToolDetails>> {
|
|
2338
|
+
return runResolveInvocation(input as Parameters<typeof runResolveInvocation>[0], {
|
|
2339
|
+
sourceToolName: "plan_approval",
|
|
2340
|
+
label: "Plan ready for approval",
|
|
2341
|
+
apply: async (_reason, extra) => {
|
|
2342
|
+
const planYolo = this.#planYolo;
|
|
2343
|
+
const state = this.getPlanModeState();
|
|
2344
|
+
if (!planYolo || !state?.enabled) {
|
|
2345
|
+
throw new ToolError("Plan mode is not active.");
|
|
2346
|
+
}
|
|
2347
|
+
const { planFilePath, title } = await resolveApprovedPlan({
|
|
2348
|
+
suppliedTitle: extra?.title,
|
|
2349
|
+
statePlanFilePath: state.planFilePath,
|
|
2350
|
+
readPlan: url => this.#readPlanYoloFile(url),
|
|
2351
|
+
listPlanFiles: () => this.#listPlanYoloFiles(),
|
|
2352
|
+
});
|
|
2353
|
+
const previousTools = this.#planYoloPreviousTools;
|
|
2354
|
+
if (previousTools) {
|
|
2355
|
+
await this.setActiveToolsByName(previousTools);
|
|
2356
|
+
}
|
|
2357
|
+
this.setStandingResolveHandler(null);
|
|
2358
|
+
this.setPlanModeState(undefined);
|
|
2359
|
+
this.#planYolo = undefined;
|
|
2360
|
+
this.#planYoloPreviousTools = undefined;
|
|
2361
|
+
await this.setModelTemporary(planYolo.target, planYolo.thinkingLevel, { ephemeral: true });
|
|
2362
|
+
this.emitNotice(
|
|
2363
|
+
"info",
|
|
2364
|
+
`Plan-yolo: plan approved, switched to ${planYolo.target.provider}/${planYolo.target.id} to implement "${title}".`,
|
|
2365
|
+
"plan-yolo",
|
|
2366
|
+
);
|
|
2367
|
+
this.agent.steer({
|
|
2368
|
+
role: "custom",
|
|
2369
|
+
customType: PLAN_YOLO_HANDOFF_MESSAGE_TYPE,
|
|
2370
|
+
content: prompt.render(planYoloHandoffPrompt, { planFilePath, title }),
|
|
2371
|
+
attribution: "agent",
|
|
2372
|
+
display: false,
|
|
2373
|
+
timestamp: Date.now(),
|
|
2374
|
+
});
|
|
2375
|
+
return {
|
|
2376
|
+
content: [
|
|
2377
|
+
{ type: "text" as const, text: `Plan approved. Implementing now with ${planYolo.target.id}.` },
|
|
2378
|
+
],
|
|
2379
|
+
details: { planFilePath, title, planExists: true },
|
|
2380
|
+
};
|
|
2381
|
+
},
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
async #readPlanYoloFile(planFilePath: string): Promise<string | null> {
|
|
2386
|
+
const resolvedPath = planFilePath.startsWith("local:")
|
|
2387
|
+
? resolveLocalUrlToPath(normalizeLocalScheme(planFilePath), this.#localProtocolOptions())
|
|
2388
|
+
: resolveToCwd(planFilePath, this.sessionManager.getCwd());
|
|
2389
|
+
try {
|
|
2390
|
+
return await Bun.file(resolvedPath).text();
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
if (isEnoent(error)) return null;
|
|
2393
|
+
throw error;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/** `local://` URLs of plan files in the session-local root, newest first —
|
|
2398
|
+
* a fallback for `resolveApprovedPlan` when the agent dropped `extra.title`. */
|
|
2399
|
+
async #listPlanYoloFiles(): Promise<string[]> {
|
|
2400
|
+
const localRoot = resolveLocalUrlToPath("local://", this.#localProtocolOptions());
|
|
2401
|
+
try {
|
|
2402
|
+
const entries = await fs.promises.readdir(localRoot, { withFileTypes: true });
|
|
2403
|
+
const plans = await Promise.all(
|
|
2404
|
+
entries
|
|
2405
|
+
.filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
|
|
2406
|
+
.map(async entry => {
|
|
2407
|
+
const stat = await fs.promises.stat(path.join(localRoot, entry.name)).catch(() => null);
|
|
2408
|
+
return { url: `local://${entry.name}`, mtime: stat?.mtimeMs ?? 0 };
|
|
2409
|
+
}),
|
|
2410
|
+
);
|
|
2411
|
+
return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
|
|
2412
|
+
} catch {
|
|
2413
|
+
return [];
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2094
2417
|
constructor(config: AgentSessionConfig) {
|
|
2095
2418
|
this.agent = config.agent;
|
|
2096
2419
|
this.sessionManager = config.sessionManager;
|
|
@@ -2111,7 +2434,14 @@ export class AgentSession {
|
|
|
2111
2434
|
} else {
|
|
2112
2435
|
this.#thinkingLevel = config.thinkingLevel;
|
|
2113
2436
|
}
|
|
2437
|
+
if (config.prewalk) {
|
|
2438
|
+
this.#prewalk = config.prewalk;
|
|
2439
|
+
}
|
|
2440
|
+
if (config.planYolo) {
|
|
2441
|
+
this.#planYolo = config.planYolo;
|
|
2442
|
+
}
|
|
2114
2443
|
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
2444
|
+
|
|
2115
2445
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
2116
2446
|
this.#slashCommands = config.slashCommands ?? [];
|
|
2117
2447
|
this.#extensionRunner = config.extensionRunner;
|
|
@@ -2184,6 +2514,7 @@ export class AgentSession {
|
|
|
2184
2514
|
});
|
|
2185
2515
|
if (detection) this.#maybeInjectToolCallLoopRedirect(messages, detection);
|
|
2186
2516
|
}
|
|
2517
|
+
await this.#advancePrewalk(messages, context);
|
|
2187
2518
|
this.#advisorPrimaryTurnsCompleted++;
|
|
2188
2519
|
if (this.#advisors.length > 0) {
|
|
2189
2520
|
for (const a of this.#advisors) {
|
|
@@ -4136,6 +4467,17 @@ export class AgentSession {
|
|
|
4136
4467
|
await emitAgentEndNotification();
|
|
4137
4468
|
return;
|
|
4138
4469
|
}
|
|
4470
|
+
} else if (this.#isHardErrorFallbackEligible(msg)) {
|
|
4471
|
+
// A non-retryable hard error on a model covered by a configured
|
|
4472
|
+
// fallback chain: retrying the SAME model is pointless, but a
|
|
4473
|
+
// DIFFERENT model is a fresh chance — consult the chain before
|
|
4474
|
+
// surfacing the failure. #handleRetryableError bails out (no
|
|
4475
|
+
// backoff-retry of the failing model) when no switch happens.
|
|
4476
|
+
const didRetry = await this.#handleRetryableError(msg, { hardErrorFallback: true });
|
|
4477
|
+
if (didRetry) {
|
|
4478
|
+
await emitAgentEndNotification();
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4139
4481
|
}
|
|
4140
4482
|
// Classifier refusals are persisted-skipped above; also prune the trailing
|
|
4141
4483
|
// stub from active context so the next turn's prompt does not replay it.
|
|
@@ -7131,6 +7473,11 @@ export class AgentSession {
|
|
|
7131
7473
|
return this.#planModeState;
|
|
7132
7474
|
}
|
|
7133
7475
|
|
|
7476
|
+
/** Prewalk state, if armed and active */
|
|
7477
|
+
getPrewalkState(): Prewalk | undefined {
|
|
7478
|
+
return this.#prewalk;
|
|
7479
|
+
}
|
|
7480
|
+
|
|
7134
7481
|
setPlanModeState(state: PlanModeState | undefined): void {
|
|
7135
7482
|
this.#planModeState = state;
|
|
7136
7483
|
if (state?.enabled) {
|
|
@@ -7871,6 +8218,8 @@ export class AgentSession {
|
|
|
7871
8218
|
await this.#checkCompaction(lastAssistant, false, false, false);
|
|
7872
8219
|
}
|
|
7873
8220
|
|
|
8221
|
+
await this.#armPlanYoloIfNeeded();
|
|
8222
|
+
|
|
7874
8223
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
7875
8224
|
const messages: AgentMessage[] = [];
|
|
7876
8225
|
const planReferenceMessage = await this.#buildPlanReferenceMessage?.();
|
|
@@ -9305,7 +9654,7 @@ export class AgentSession {
|
|
|
9305
9654
|
const all = this.#modelRegistry.getAvailable();
|
|
9306
9655
|
const patterns = this.settings.get("enabledModels");
|
|
9307
9656
|
if (!patterns || patterns.length === 0) return all;
|
|
9308
|
-
return filterAvailableModelsByEnabledPatterns(all, patterns);
|
|
9657
|
+
return filterAvailableModelsByEnabledPatterns(all, patterns, this.settings);
|
|
9309
9658
|
}
|
|
9310
9659
|
|
|
9311
9660
|
// =========================================================================
|
|
@@ -9641,6 +9990,11 @@ export class AgentSession {
|
|
|
9641
9990
|
* candidate is small or the session has been idle long enough that the
|
|
9642
9991
|
* provider prompt cache is cold), so it is cheap to run every turn. Gated
|
|
9643
9992
|
* on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
|
|
9993
|
+
*
|
|
9994
|
+
* Persists via `rewriteEntries` like every other history rewrite — the
|
|
9995
|
+
* session file must match the live (pruned) context or file-based forks
|
|
9996
|
+
* (`/fork`, `/tan`) and resume rebuild a divergent prefix and cold-miss the
|
|
9997
|
+
* provider prompt cache.
|
|
9644
9998
|
*/
|
|
9645
9999
|
async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
9646
10000
|
const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
|
|
@@ -9663,6 +10017,7 @@ export class AgentSession {
|
|
|
9663
10017
|
return undefined;
|
|
9664
10018
|
}
|
|
9665
10019
|
|
|
10020
|
+
await this.sessionManager.rewriteEntries();
|
|
9666
10021
|
const sessionContext = this.buildDisplaySessionContext();
|
|
9667
10022
|
this.agent.replaceMessages(sessionContext.messages);
|
|
9668
10023
|
this.#resetAllAdvisorRuntimes();
|
|
@@ -10104,6 +10459,7 @@ export class AgentSession {
|
|
|
10104
10459
|
const newEntries = this.sessionManager.getEntries();
|
|
10105
10460
|
const sessionContext = this.buildDisplaySessionContext();
|
|
10106
10461
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10462
|
+
this.#rebasePendingContextSnapshotAfterCompaction();
|
|
10107
10463
|
// Compaction discarded the conversation history that carried the approved
|
|
10108
10464
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
10109
10465
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
@@ -12498,49 +12854,85 @@ export class AgentSession {
|
|
|
12498
12854
|
}
|
|
12499
12855
|
|
|
12500
12856
|
/**
|
|
12501
|
-
* Last-resort reducer when {@link #runAutoCompaction} would otherwise
|
|
12502
|
-
* The summarizer cut at the only available turn boundary, but the
|
|
12503
|
-
* still over the recovery band because a single recent turn (a
|
|
12504
|
-
* tool-result, a heavy fenced/XML block) is itself
|
|
12505
|
-
* `findCutPoint` cannot cut inside one message.
|
|
12506
|
-
* that tail — it offloads heavy tool-result / block content to one
|
|
12507
|
-
* `artifact://` blob and leaves a recoverable placeholder — so residual context
|
|
12508
|
-
* genuinely drops instead of the guard pausing maintenance and looping the
|
|
12509
|
-
* warning. Without it the guard would pause/warn here; with it the caller
|
|
12510
|
-
* re-tests its progress predicate after the elide pass and only falls through
|
|
12511
|
-
* to the warning when residual stays over.
|
|
12857
|
+
* Last-resort tiered reducer when {@link #runAutoCompaction} would otherwise
|
|
12858
|
+
* dead-end. The summarizer cut at the only available turn boundary, but the
|
|
12859
|
+
* kept tail is still over the recovery band because a single recent turn (a
|
|
12860
|
+
* large tool-result, a heavy fenced/XML block, attached images) is itself
|
|
12861
|
+
* bigger than the band and `findCutPoint` cannot cut inside one message.
|
|
12512
12862
|
*
|
|
12513
|
-
*
|
|
12514
|
-
*
|
|
12515
|
-
*
|
|
12516
|
-
* `/shake images`
|
|
12863
|
+
* Tier 1 — `shake("elide")` reaches INSIDE that tail: heavy tool-result /
|
|
12864
|
+
* block content is offloaded to one `artifact://` blob behind a recoverable
|
|
12865
|
+
* placeholder. Skipped when this pass already ran a shake (`skipElide`).
|
|
12866
|
+
* Tier 2 — `dropImages()`: the manual `/shake images` remedy, automated.
|
|
12867
|
+
* Image blocks are stripped from the branch; unlike elided text they are NOT
|
|
12868
|
+
* artifact-recoverable, so this tier only runs once elide has failed the
|
|
12869
|
+
* progress re-test.
|
|
12517
12870
|
*
|
|
12518
|
-
*
|
|
12519
|
-
*
|
|
12520
|
-
*
|
|
12871
|
+
* Each tier that rewrote history re-anchors the in-flight context snapshot,
|
|
12872
|
+
* then the caller's progress predicate is re-tested; the first tier that
|
|
12873
|
+
* restores progress emits one info notice describing everything freed and
|
|
12874
|
+
* stops. Returns whether progress was restored — `false` falls through to
|
|
12875
|
+
* the dead-end warning.
|
|
12521
12876
|
*/
|
|
12522
|
-
async #
|
|
12523
|
-
|
|
12877
|
+
async #rescueCompactionDeadEnd(
|
|
12878
|
+
signal: AbortSignal,
|
|
12879
|
+
options: { skipElide: boolean; hasProgress: () => boolean },
|
|
12880
|
+
): Promise<boolean> {
|
|
12881
|
+
if (signal.aborted) return false;
|
|
12882
|
+
let elided = 0;
|
|
12883
|
+
let elidedTokens = 0;
|
|
12884
|
+
let elideSink = "placeholders";
|
|
12885
|
+
if (!options.skipElide) {
|
|
12886
|
+
try {
|
|
12887
|
+
const result = await this.shake("elide", { signal });
|
|
12888
|
+
elided = result.toolResultsDropped + result.blocksDropped;
|
|
12889
|
+
elidedTokens = result.tokensFreed;
|
|
12890
|
+
if (result.artifactId) elideSink = "an artifact";
|
|
12891
|
+
if (elided > 0) {
|
|
12892
|
+
// The elide pass rewrote history; re-anchor the in-flight snapshot
|
|
12893
|
+
// so the caller's headroom/retry-fit re-test measures the shaken
|
|
12894
|
+
// context.
|
|
12895
|
+
this.#rebasePendingContextSnapshotAfterCompaction();
|
|
12896
|
+
}
|
|
12897
|
+
} catch (error) {
|
|
12898
|
+
logger.warn("Dead-end shake rescue failed", {
|
|
12899
|
+
error: error instanceof Error ? error.message : String(error),
|
|
12900
|
+
});
|
|
12901
|
+
}
|
|
12902
|
+
if (elided > 0 && options.hasProgress()) {
|
|
12903
|
+
this.emitNotice(
|
|
12904
|
+
"info",
|
|
12905
|
+
`Compaction dead-end recovery: ${this.#describeElideRescue(elided, elidedTokens, elideSink)} so maintenance could make progress.`,
|
|
12906
|
+
"compaction",
|
|
12907
|
+
);
|
|
12908
|
+
return true;
|
|
12909
|
+
}
|
|
12910
|
+
}
|
|
12911
|
+
if (signal.aborted) return false;
|
|
12912
|
+
let imagesDropped = 0;
|
|
12524
12913
|
try {
|
|
12525
|
-
|
|
12526
|
-
|
|
12914
|
+
imagesDropped = (await this.dropImages()).removed;
|
|
12915
|
+
if (imagesDropped > 0) this.#rebasePendingContextSnapshotAfterCompaction();
|
|
12527
12916
|
} catch (error) {
|
|
12528
|
-
logger.warn("Dead-end
|
|
12917
|
+
logger.warn("Dead-end image-drop rescue failed", {
|
|
12529
12918
|
error: error instanceof Error ? error.message : String(error),
|
|
12530
12919
|
});
|
|
12531
|
-
return undefined;
|
|
12532
12920
|
}
|
|
12921
|
+
if (imagesDropped > 0 && options.hasProgress()) {
|
|
12922
|
+
const elidedPart = elided > 0 ? `${this.#describeElideRescue(elided, elidedTokens, elideSink)} and ` : "";
|
|
12923
|
+
this.emitNotice(
|
|
12924
|
+
"info",
|
|
12925
|
+
`Compaction dead-end recovery: ${elidedPart}dropped ${imagesDropped} attached image${imagesDropped === 1 ? "" : "s"} so maintenance could make progress.`,
|
|
12926
|
+
"compaction",
|
|
12927
|
+
);
|
|
12928
|
+
return true;
|
|
12929
|
+
}
|
|
12930
|
+
return false;
|
|
12533
12931
|
}
|
|
12534
12932
|
|
|
12535
|
-
/** Notice
|
|
12536
|
-
#
|
|
12537
|
-
|
|
12538
|
-
const sink = result.artifactId ? "an artifact" : "placeholders";
|
|
12539
|
-
this.emitNotice(
|
|
12540
|
-
"info",
|
|
12541
|
-
`Compaction dead-end recovery: elided ${elided} heavy block${elided === 1 ? "" : "s"} (~${result.tokensFreed.toLocaleString()} tokens) to ${sink} so maintenance could make progress.`,
|
|
12542
|
-
"compaction",
|
|
12543
|
-
);
|
|
12933
|
+
/** Notice fragment for a dead-end elide tier: what was freed and where it went. */
|
|
12934
|
+
#describeElideRescue(elided: number, tokensFreed: number, sink: string): string {
|
|
12935
|
+
return `elided ${elided} heavy block${elided === 1 ? "" : "s"} (~${tokensFreed.toLocaleString()} tokens) to ${sink}`;
|
|
12544
12936
|
}
|
|
12545
12937
|
|
|
12546
12938
|
/**
|
|
@@ -13048,6 +13440,7 @@ export class AgentSession {
|
|
|
13048
13440
|
const newEntries = this.sessionManager.getEntries();
|
|
13049
13441
|
const sessionContext = this.buildDisplaySessionContext();
|
|
13050
13442
|
this.agent.replaceMessages(sessionContext.messages);
|
|
13443
|
+
this.#rebasePendingContextSnapshotAfterCompaction();
|
|
13051
13444
|
// Compaction discarded the conversation history that carried the approved
|
|
13052
13445
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
13053
13446
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
@@ -13081,23 +13474,28 @@ export class AgentSession {
|
|
|
13081
13474
|
details,
|
|
13082
13475
|
preserveData,
|
|
13083
13476
|
};
|
|
13084
|
-
|
|
13085
|
-
|
|
13086
|
-
//
|
|
13087
|
-
//
|
|
13088
|
-
//
|
|
13089
|
-
//
|
|
13090
|
-
//
|
|
13091
|
-
//
|
|
13092
|
-
//
|
|
13093
|
-
//
|
|
13094
|
-
//
|
|
13095
|
-
//
|
|
13477
|
+
// Post-maintenance progress guard — evaluated BEFORE emitting
|
|
13478
|
+
// auto_compaction_end so the TUI rebuild triggered by that event
|
|
13479
|
+
// already reflects any rescue rewrite (elide / image-drop) and the
|
|
13480
|
+
// dead-end warning stamped on the compaction entry. Snapcompact can
|
|
13481
|
+
// project over budget and fall back to a context-full summary; the
|
|
13482
|
+
// summarizer keeps `keepRecentTokens` of recent history verbatim and
|
|
13483
|
+
// findCutPoint can only cut at turn boundaries (never tool results),
|
|
13484
|
+
// so a single oversized recent turn (e.g. a huge tool result) leaves
|
|
13485
|
+
// the rewritten context still above threshold. Scheduling the
|
|
13486
|
+
// continuation regardless means the next agent_end re-enters
|
|
13487
|
+
// #checkCompaction over the same oversized tail and re-fires forever.
|
|
13488
|
+
// The retry and the threshold auto-continue use different progress
|
|
13489
|
+
// tests (a recoverable overflow only has to fit; the auto-continue
|
|
13490
|
+
// thrash needs the stricter recovery band), so each branch evaluates
|
|
13491
|
+
// its own below.
|
|
13096
13492
|
let continuationScheduled = false;
|
|
13097
13493
|
// A non-idle pass that wanted to continue (retry or auto-continue) but freed
|
|
13098
13494
|
// too little for that path to proceed is a dead-end: warn once so the user
|
|
13099
13495
|
// understands why maintenance paused instead of silently looping.
|
|
13100
13496
|
let noProgressDeadEnd = false;
|
|
13497
|
+
let retryFits = false;
|
|
13498
|
+
let hasHeadroom = false;
|
|
13101
13499
|
|
|
13102
13500
|
if (willRetry) {
|
|
13103
13501
|
const messages = this.agent.state.messages;
|
|
@@ -13113,6 +13511,7 @@ export class AgentSession {
|
|
|
13113
13511
|
(reason === "incomplete" && lastAssistant.stopReason === "length");
|
|
13114
13512
|
if (shouldDrop) {
|
|
13115
13513
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
13514
|
+
this.#rebasePendingContextSnapshotAfterCompaction();
|
|
13116
13515
|
}
|
|
13117
13516
|
}
|
|
13118
13517
|
|
|
@@ -13121,18 +13520,14 @@ export class AgentSession {
|
|
|
13121
13520
|
// won't include) is excluded. Reusing the auto-continue recovery band
|
|
13122
13521
|
// here turned recoverable overflows into manual dead-ends (#3412 review),
|
|
13123
13522
|
// so use the looser fit budget.
|
|
13124
|
-
|
|
13125
|
-
if (!retryFits
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13130
|
-
}
|
|
13523
|
+
retryFits = this.#compactionCreatedRetryFit();
|
|
13524
|
+
if (!retryFits) {
|
|
13525
|
+
retryFits = await this.#rescueCompactionDeadEnd(autoCompactionSignal, {
|
|
13526
|
+
skipElide: fallbackFromShake,
|
|
13527
|
+
hasProgress: () => this.#compactionCreatedRetryFit(),
|
|
13528
|
+
});
|
|
13131
13529
|
}
|
|
13132
|
-
if (retryFits) {
|
|
13133
|
-
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13134
|
-
continuationScheduled = true;
|
|
13135
|
-
} else {
|
|
13530
|
+
if (!retryFits) {
|
|
13136
13531
|
noProgressDeadEnd = true;
|
|
13137
13532
|
}
|
|
13138
13533
|
} else if (reason !== "idle") {
|
|
@@ -13143,23 +13538,36 @@ export class AgentSession {
|
|
|
13143
13538
|
// when auto-continue is disabled, a no-headroom threshold pass must still
|
|
13144
13539
|
// block later automatic continuations (todo reminders/session_stop hooks)
|
|
13145
13540
|
// from re-entering the same oversized context.
|
|
13146
|
-
|
|
13147
|
-
if (!hasHeadroom
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
|
|
13152
|
-
}
|
|
13541
|
+
hasHeadroom = this.#compactionCreatedHeadroom();
|
|
13542
|
+
if (!hasHeadroom) {
|
|
13543
|
+
hasHeadroom = await this.#rescueCompactionDeadEnd(autoCompactionSignal, {
|
|
13544
|
+
skipElide: fallbackFromShake,
|
|
13545
|
+
hasProgress: () => this.#compactionCreatedHeadroom(),
|
|
13546
|
+
});
|
|
13153
13547
|
}
|
|
13154
|
-
if (hasHeadroom) {
|
|
13155
|
-
if (shouldAutoContinue) {
|
|
13156
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13157
|
-
continuationScheduled = true;
|
|
13158
|
-
}
|
|
13159
|
-
} else {
|
|
13548
|
+
if (!hasHeadroom) {
|
|
13160
13549
|
noProgressDeadEnd = true;
|
|
13161
13550
|
}
|
|
13162
13551
|
}
|
|
13552
|
+
|
|
13553
|
+
const deadEndWarning = noProgressDeadEnd ? compactionDeadEndWarning("clear large tool output") : undefined;
|
|
13554
|
+
if (deadEndWarning && savedCompactionEntry) {
|
|
13555
|
+
// Stamp the divider: the compaction bar badges the dead-end and
|
|
13556
|
+
// carries the full warning in its ctrl+o detail, so the pause
|
|
13557
|
+
// stays explained even after the notice row scrolls away.
|
|
13558
|
+
savedCompactionEntry.warning = deadEndWarning;
|
|
13559
|
+
await this.sessionManager.rewriteEntries();
|
|
13560
|
+
}
|
|
13561
|
+
|
|
13562
|
+
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
13563
|
+
|
|
13564
|
+
if (retryFits) {
|
|
13565
|
+
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13566
|
+
continuationScheduled = true;
|
|
13567
|
+
} else if (hasHeadroom && shouldAutoContinue) {
|
|
13568
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
13569
|
+
continuationScheduled = true;
|
|
13570
|
+
}
|
|
13163
13571
|
if (!continuationScheduled && !suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
13164
13572
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
13165
13573
|
// Kick the loop so queued messages are actually delivered. This remains separate
|
|
@@ -13172,12 +13580,8 @@ export class AgentSession {
|
|
|
13172
13580
|
continuationScheduled = true;
|
|
13173
13581
|
}
|
|
13174
13582
|
|
|
13175
|
-
if (
|
|
13176
|
-
this.emitNotice(
|
|
13177
|
-
"warning",
|
|
13178
|
-
compactionDeadEndWarning("clear large tool output, run `/shake images` to drop attached images,"),
|
|
13179
|
-
"compaction",
|
|
13180
|
-
);
|
|
13583
|
+
if (deadEndWarning) {
|
|
13584
|
+
this.emitNotice("warning", deadEndWarning, "compaction");
|
|
13181
13585
|
}
|
|
13182
13586
|
if (continuationScheduled) return COMPACTION_CHECK_CONTINUATION;
|
|
13183
13587
|
return noProgressDeadEnd ? COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION : COMPACTION_CHECK_NONE;
|
|
@@ -13845,6 +14249,34 @@ export class AgentSession {
|
|
|
13845
14249
|
return this.#modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
|
|
13846
14250
|
}
|
|
13847
14251
|
|
|
14252
|
+
/**
|
|
14253
|
+
* True when a turn failed with a hard (non-retryable) provider error but a
|
|
14254
|
+
* configured `retry.fallbackChains` entry covers the active model: the same
|
|
14255
|
+
* model is not worth retrying, yet a DIFFERENT model is a fresh chance, so
|
|
14256
|
+
* the chain is consulted before the error becomes final. Skips failures a
|
|
14257
|
+
* model switch cannot fix or must not replay: cancellations (abort-flavored
|
|
14258
|
+
* errors are not model faults), context overflow (compaction's job),
|
|
14259
|
+
* classifier refusals (chain consult is handled on the retryable path with
|
|
14260
|
+
* `pinFallback`), and turns that already emitted a tool call (replaying
|
|
14261
|
+
* could duplicate work).
|
|
14262
|
+
*/
|
|
14263
|
+
#isHardErrorFallbackEligible(message: AssistantMessage): boolean {
|
|
14264
|
+
if (message.stopReason !== "error") return false;
|
|
14265
|
+
const model = this.model;
|
|
14266
|
+
if (!model) return false;
|
|
14267
|
+
const retrySettings = this.settings.getGroup("retry");
|
|
14268
|
+
if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
|
|
14269
|
+
if (this.#isClassifierRefusal(message)) return false;
|
|
14270
|
+
const id = this.#classifyRetryMessage(message);
|
|
14271
|
+
if (AIError.is(id, AIError.Flag.Abort) || AIError.is(id, AIError.Flag.UserInterrupt)) return false;
|
|
14272
|
+
if (AIError.isContextOverflow(message, model.contextWindow ?? 0)) return false;
|
|
14273
|
+
if (this.#hasReplayUnsafeToolOutput(message)) return false;
|
|
14274
|
+
const currentSelector = formatRetryFallbackSelector(model, this.thinkingLevel);
|
|
14275
|
+
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
14276
|
+
if (!role) return false;
|
|
14277
|
+
return this.#findRetryFallbackCandidates(role, currentSelector).length > 0;
|
|
14278
|
+
}
|
|
14279
|
+
|
|
13848
14280
|
/**
|
|
13849
14281
|
* Switch the active model from a Fireworks Fast (`-fast`) variant to its base
|
|
13850
14282
|
* (Standard) id and stick there for the rest of the session — the auto
|
|
@@ -13968,12 +14400,16 @@ export class AgentSession {
|
|
|
13968
14400
|
}
|
|
13969
14401
|
|
|
13970
14402
|
/**
|
|
13971
|
-
* Handle retryable errors with exponential backoff
|
|
14403
|
+
* Handle retryable errors with exponential backoff, credential rotation, and
|
|
14404
|
+
* model-fallback chains. Also entered for NON-retryable errors when a switch
|
|
14405
|
+
* is the recovery (`fireworksFastFallback`, `hardErrorFallback`): then a
|
|
14406
|
+
* successful model switch retries immediately, and a failed switch surfaces
|
|
14407
|
+
* the error without a same-model backoff retry.
|
|
13972
14408
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
13973
14409
|
*/
|
|
13974
14410
|
async #handleRetryableError(
|
|
13975
14411
|
message: AssistantMessage,
|
|
13976
|
-
options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean },
|
|
14412
|
+
options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean; hardErrorFallback?: boolean },
|
|
13977
14413
|
): Promise<boolean> {
|
|
13978
14414
|
const retrySettings = this.settings.getGroup("retry");
|
|
13979
14415
|
// The Fireworks Fast→base degrade is an intrinsic model-selection safety net,
|
|
@@ -14115,11 +14551,16 @@ export class AgentSession {
|
|
|
14115
14551
|
this.#resolveRetry();
|
|
14116
14552
|
return false;
|
|
14117
14553
|
}
|
|
14118
|
-
//
|
|
14119
|
-
//
|
|
14120
|
-
//
|
|
14121
|
-
// classifier wouldn't retry —
|
|
14122
|
-
|
|
14554
|
+
// A fallback switch was the whole reason we entered (Fast→base degrade or
|
|
14555
|
+
// a hard-error chain consult) but it could not happen (e.g. no candidate
|
|
14556
|
+
// has a credential). Don't fall through to backing-off and retrying the
|
|
14557
|
+
// failing model for an error the generic classifier wouldn't retry —
|
|
14558
|
+
// surface it instead.
|
|
14559
|
+
if (
|
|
14560
|
+
(options?.fireworksFastFallback || options?.hardErrorFallback) &&
|
|
14561
|
+
!switchedModel &&
|
|
14562
|
+
!this.#isRetryableError(message)
|
|
14563
|
+
) {
|
|
14123
14564
|
this.#retryAttempt = 0;
|
|
14124
14565
|
this.#resolveRetry();
|
|
14125
14566
|
return false;
|
|
@@ -15893,6 +16334,28 @@ export class AgentSession {
|
|
|
15893
16334
|
this.#contextUsageRevision++;
|
|
15894
16335
|
}
|
|
15895
16336
|
|
|
16337
|
+
/**
|
|
16338
|
+
* Rebase the in-flight pending context snapshot onto the current message
|
|
16339
|
+
* set after a compaction (or its dead-end rescue) rewrote history mid-run.
|
|
16340
|
+
* The snapshot captures the prompt as submitted at run start and lives for
|
|
16341
|
+
* the whole run; once a compaction entry lands, every earlier usage anchor
|
|
16342
|
+
* is hidden from {@link getContextBreakdown}, so the stale run-start figure
|
|
16343
|
+
* would be reported as live context until the next provider response. That
|
|
16344
|
+
* inflated residual is what the post-compaction headroom/retry-fit checks
|
|
16345
|
+
* measure — a run that started above the recovery band then trips the
|
|
16346
|
+
* "freed too little context" dead-end even when compaction genuinely
|
|
16347
|
+
* shrank the context. No-op while no prompt is in flight.
|
|
16348
|
+
*/
|
|
16349
|
+
#rebasePendingContextSnapshotAfterCompaction(): void {
|
|
16350
|
+
if (!this.#pendingContextSnapshot) return;
|
|
16351
|
+
const nonMessageTokens = computeNonMessageTokens(this);
|
|
16352
|
+
this.#setPendingContextSnapshot({
|
|
16353
|
+
promptTokens: nonMessageTokens + this.messages.reduce((sum, msg) => sum + estimateTokens(msg), 0),
|
|
16354
|
+
nonMessageTokens,
|
|
16355
|
+
cutoffCount: this.messages.length,
|
|
16356
|
+
});
|
|
16357
|
+
}
|
|
16358
|
+
|
|
15896
16359
|
#ingestProviderUsageHeaders(response: ProviderResponseMetadata, model?: Model): void {
|
|
15897
16360
|
if (model?.provider !== "anthropic") return;
|
|
15898
16361
|
this.#modelRegistry.authStorage.ingestUsageHeaders("anthropic", response.headers, {
|