@oh-my-pi/pi-coding-agent 16.2.6 → 16.2.8
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 +46 -0
- package/dist/cli.js +2781 -2660
- package/dist/types/cli/bench-cli.d.ts +3 -3
- package/dist/types/commands/bench.d.ts +1 -1
- package/dist/types/config/service-tier.d.ts +39 -24
- package/dist/types/config/settings-schema.d.ts +92 -36
- package/dist/types/edit/hashline/filesystem.d.ts +1 -0
- package/dist/types/mcp/config-writer.d.ts +48 -0
- package/dist/types/mcp/types.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +33 -13
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +2 -7
- package/dist/types/session/session-entries.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +2 -2
- package/dist/types/session/settings-stream-fn.d.ts +2 -2
- package/dist/types/system-prompt.test.d.ts +1 -0
- package/dist/types/task/executor.d.ts +6 -6
- package/dist/types/task/types.d.ts +6 -0
- package/dist/types/task/worktree.d.ts +32 -6
- package/dist/types/tiny/title-client.d.ts +5 -1
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tools/output-schema-validator.d.ts +10 -0
- package/dist/types/utils/git.d.ts +17 -0
- package/package.json +12 -12
- package/src/cli/bench-cli.ts +19 -12
- package/src/cli/tiny-models-cli.ts +18 -4
- package/src/commands/bench.ts +3 -3
- package/src/config/mcp-schema.json +10 -1
- package/src/config/service-tier.ts +85 -56
- package/src/config/settings-schema.ts +81 -37
- package/src/config/settings.ts +47 -0
- package/src/discovery/builtin-rules/go-add-cleanup.md +32 -0
- package/src/discovery/builtin-rules/go-bench-loop.md +36 -0
- package/src/discovery/builtin-rules/go-exp-promoted.md +39 -0
- package/src/discovery/builtin-rules/go-ioutil.md +36 -0
- package/src/discovery/builtin-rules/go-join-hostport.md +29 -0
- package/src/discovery/builtin-rules/go-new-expr.md +44 -0
- package/src/discovery/builtin-rules/go-rand-v2.md +40 -0
- package/src/discovery/builtin-rules/go-range-int.md +45 -0
- package/src/discovery/builtin-rules/index.ts +16 -0
- package/src/edit/hashline/filesystem.ts +12 -0
- package/src/eval/agent-bridge.ts +4 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +1 -1
- package/src/mcp/config-writer.ts +121 -0
- package/src/mcp/config.ts +10 -6
- package/src/mcp/types.ts +3 -0
- package/src/modes/components/extensions/extension-dashboard.ts +46 -0
- package/src/modes/components/extensions/state-manager.ts +24 -3
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/prompts/tools/bash.md +1 -4
- package/src/sdk.ts +12 -11
- package/src/session/agent-session.ts +294 -82
- package/src/session/messages.ts +1 -1
- package/src/session/session-context.ts +17 -6
- package/src/session/session-entries.ts +2 -2
- package/src/session/session-manager.ts +9 -2
- package/src/session/settings-stream-fn.ts +12 -2
- package/src/slash-commands/builtin-registry.ts +2 -10
- package/src/system-prompt.test.ts +158 -0
- package/src/system-prompt.ts +69 -26
- package/src/task/executor.ts +23 -16
- package/src/task/index.ts +7 -5
- package/src/task/isolation-runner.ts +15 -1
- package/src/task/types.ts +6 -0
- package/src/task/worktree.ts +219 -38
- package/src/tiny/title-client.ts +19 -13
- package/src/tools/grep.ts +19 -1
- package/src/tools/index.ts +3 -3
- package/src/tools/irc.ts +9 -3
- package/src/tools/output-schema-validator.ts +38 -0
- package/src/tools/read.ts +28 -28
- package/src/tools/yield.ts +52 -15
- package/src/utils/file-mentions.ts +10 -1
- package/src/utils/git.ts +38 -0
- package/src/web/search/providers/duckduckgo.ts +17 -3
|
@@ -92,6 +92,8 @@ import type {
|
|
|
92
92
|
ResetCreditRedeemOutcome,
|
|
93
93
|
ResetCreditTarget,
|
|
94
94
|
ServiceTier,
|
|
95
|
+
ServiceTierByFamily,
|
|
96
|
+
ServiceTierFamily,
|
|
95
97
|
SimpleStreamOptions,
|
|
96
98
|
TextContent,
|
|
97
99
|
ToolCall,
|
|
@@ -105,7 +107,9 @@ import {
|
|
|
105
107
|
deriveClaudeDeviceId,
|
|
106
108
|
Effort,
|
|
107
109
|
parseRateLimitReason,
|
|
108
|
-
|
|
110
|
+
realizesPriorityServiceTier,
|
|
111
|
+
resolveModelServiceTier,
|
|
112
|
+
serviceTierFamily,
|
|
109
113
|
streamSimple,
|
|
110
114
|
} from "@oh-my-pi/pi-ai";
|
|
111
115
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
@@ -168,7 +172,7 @@ import {
|
|
|
168
172
|
} from "../config/model-resolver";
|
|
169
173
|
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
170
174
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
171
|
-
import {
|
|
175
|
+
import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
172
176
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
173
177
|
import { getDefault, onAppendOnlyModeChanged, validateProviderMaxInFlightRequests } from "../config/settings";
|
|
174
178
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
@@ -506,6 +510,8 @@ export interface AgentSessionConfig {
|
|
|
506
510
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
507
511
|
/** Initial session thinking selector. */
|
|
508
512
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
513
|
+
/** Initial per-family service tiers (OpenAI / Anthropic / Google) for the live session. */
|
|
514
|
+
serviceTierByFamily?: ServiceTierByFamily;
|
|
509
515
|
/** Prompt templates for expansion */
|
|
510
516
|
promptTemplates?: PromptTemplate[];
|
|
511
517
|
/** File-based slash commands for expansion */
|
|
@@ -1787,6 +1793,7 @@ export class AgentSession {
|
|
|
1787
1793
|
// toggle scopes priority to Fireworks alone, without mutating the shared
|
|
1788
1794
|
// session `serviceTier` that drives `/fast` and OpenAI/Anthropic priority.
|
|
1789
1795
|
this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
|
|
1796
|
+
this.#serviceTierByFamily = config.serviceTierByFamily ?? {};
|
|
1790
1797
|
this.#advisorTools = config.advisorTools;
|
|
1791
1798
|
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1792
1799
|
this.#advisorSharedInstructions = config.advisorSharedInstructions;
|
|
@@ -2045,15 +2052,20 @@ export class AgentSession {
|
|
|
2045
2052
|
const legacy = !this.#advisorConfigs?.length;
|
|
2046
2053
|
const roster: AdvisorConfig[] = legacy ? [{ name: "default" }] : this.#advisorConfigs!;
|
|
2047
2054
|
|
|
2048
|
-
// Advisor service tier (`
|
|
2049
|
-
//
|
|
2050
|
-
// per request (like the main agent, including /fast toggles)
|
|
2051
|
-
//
|
|
2052
|
-
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2055
|
+
// Advisor service tier (`tier.advisor`): "none" (default) runs the advisor
|
|
2056
|
+
// on standard processing; "inherit" tracks the session's live per-family
|
|
2057
|
+
// tiers per request (like the main agent, including /fast toggles); a
|
|
2058
|
+
// concrete value is broadcast across families and applied to the advisor
|
|
2059
|
+
// model's family. One value for all advisors.
|
|
2060
|
+
const advisorTierSetting = this.settings.get("tier.advisor");
|
|
2061
|
+
const advisorTierMap =
|
|
2062
|
+
advisorTierSetting === "inherit"
|
|
2063
|
+
? undefined
|
|
2064
|
+
: serviceTierForAllFamilies(serviceTierSettingToTier(advisorTierSetting));
|
|
2065
|
+
const advisorServiceTierResolver = (model: Model): ServiceTier | undefined =>
|
|
2066
|
+
advisorTierSetting === "inherit"
|
|
2067
|
+
? this.#effectiveServiceTier(model)
|
|
2068
|
+
: resolveModelServiceTier(advisorTierMap, model);
|
|
2057
2069
|
|
|
2058
2070
|
const usedSlugs = new Set<string>();
|
|
2059
2071
|
for (const config of roster) {
|
|
@@ -2152,7 +2164,7 @@ export class AgentSession {
|
|
|
2152
2164
|
transformProviderContext: this.#transformProviderContext,
|
|
2153
2165
|
intentTracing: false,
|
|
2154
2166
|
telemetry: advisorTelemetry,
|
|
2155
|
-
serviceTier:
|
|
2167
|
+
serviceTier: undefined,
|
|
2156
2168
|
serviceTierResolver: advisorServiceTierResolver,
|
|
2157
2169
|
});
|
|
2158
2170
|
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
@@ -3215,10 +3227,11 @@ export class AgentSession {
|
|
|
3215
3227
|
if (event.message.role === "assistant") {
|
|
3216
3228
|
this.#lastAssistantMessage = event.message;
|
|
3217
3229
|
const assistantMsg = event.message as AssistantMessage;
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3230
|
+
if (
|
|
3231
|
+
assistantMsg.disabledFeatures?.includes("priority") &&
|
|
3232
|
+
this.#serviceTierByFamily.anthropic === "priority"
|
|
3233
|
+
) {
|
|
3234
|
+
this.setServiceTierFamily("anthropic", undefined);
|
|
3222
3235
|
this.emitNotice(
|
|
3223
3236
|
"warning",
|
|
3224
3237
|
"Priority/fast mode rejected for this model; retried without it. Fast mode is now off.",
|
|
@@ -5203,8 +5216,11 @@ export class AgentSession {
|
|
|
5203
5216
|
return this.#autoResolvedLevel;
|
|
5204
5217
|
}
|
|
5205
5218
|
|
|
5206
|
-
|
|
5207
|
-
|
|
5219
|
+
#serviceTierByFamily: ServiceTierByFamily = {};
|
|
5220
|
+
|
|
5221
|
+
/** Live per-family service tiers (OpenAI / Anthropic / Google). */
|
|
5222
|
+
get serviceTierByFamily(): ServiceTierByFamily {
|
|
5223
|
+
return this.#serviceTierByFamily;
|
|
5208
5224
|
}
|
|
5209
5225
|
|
|
5210
5226
|
/** Whether agent is currently streaming a response */
|
|
@@ -7892,7 +7908,7 @@ export class AgentSession {
|
|
|
7892
7908
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7893
7909
|
|
|
7894
7910
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
|
|
7895
|
-
this.sessionManager.appendServiceTierChange(this
|
|
7911
|
+
this.sessionManager.appendServiceTierChange(this.#serviceTierEntry());
|
|
7896
7912
|
if (nextDiscoverySessionToolNames) {
|
|
7897
7913
|
await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
|
|
7898
7914
|
if (this.getSelectedMCPToolNames().length > 0) {
|
|
@@ -8434,38 +8450,36 @@ export class AgentSession {
|
|
|
8434
8450
|
}
|
|
8435
8451
|
|
|
8436
8452
|
/**
|
|
8437
|
-
* True when
|
|
8438
|
-
*
|
|
8439
|
-
*
|
|
8440
|
-
*
|
|
8453
|
+
* True when the currently selected model's family is set to `priority` — the
|
|
8454
|
+
* `/fast` on/off state for the active model. Returns false when no model is
|
|
8455
|
+
* selected or the model exposes no service-tier family (e.g. Fireworks, which
|
|
8456
|
+
* has its own Providers › Fireworks Tier toggle).
|
|
8441
8457
|
*
|
|
8442
|
-
* For "is
|
|
8443
|
-
* {@link isFastModeActive} instead
|
|
8458
|
+
* For "is priority actually applied to the next request?" use
|
|
8459
|
+
* {@link isFastModeActive} instead.
|
|
8444
8460
|
*/
|
|
8445
8461
|
isFastModeEnabled(): boolean {
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
);
|
|
8462
|
+
const family = this.model ? serviceTierFamily(this.model) : undefined;
|
|
8463
|
+
return family ? this.#serviceTierByFamily[family] === "priority" : false;
|
|
8449
8464
|
}
|
|
8450
8465
|
|
|
8451
8466
|
/**
|
|
8452
|
-
* True when
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8455
|
-
* no model is selected.
|
|
8467
|
+
* True when `priority` is actually realized on the wire for the currently
|
|
8468
|
+
* selected model (OpenAI/Google `service_tier`, direct Anthropic fast mode,
|
|
8469
|
+
* or Fireworks priority). Returns false for tiers the active model can't
|
|
8470
|
+
* realize and when no model is selected.
|
|
8456
8471
|
*/
|
|
8457
8472
|
isFastModeActive(): boolean {
|
|
8458
|
-
|
|
8473
|
+
const model = this.model;
|
|
8474
|
+
return !!model && realizesPriorityServiceTier(this.#effectiveServiceTier(model), model);
|
|
8459
8475
|
}
|
|
8460
8476
|
|
|
8461
8477
|
/**
|
|
8462
|
-
* Effective wire service-tier for a request to `model`. Fireworks models
|
|
8463
|
-
*
|
|
8464
|
-
*
|
|
8465
|
-
*
|
|
8466
|
-
*
|
|
8467
|
-
* serving path is mutually exclusive with Priority. Every other provider
|
|
8468
|
-
* uses the session `serviceTier` unchanged.
|
|
8478
|
+
* Effective wire service-tier for a request to `model`. Fireworks models take
|
|
8479
|
+
* the Priority serving path only when the Providers › Fireworks Tier setting
|
|
8480
|
+
* is `"priority"` (and never for `-fast` variants, whose Fast serving path is
|
|
8481
|
+
* mutually exclusive with Priority). Every other model resolves the live
|
|
8482
|
+
* per-family tier map down to the entry for its family.
|
|
8469
8483
|
*/
|
|
8470
8484
|
#effectiveServiceTier(model: Model | undefined = this.model): ServiceTier | undefined {
|
|
8471
8485
|
if (model?.provider === "fireworks") {
|
|
@@ -8473,40 +8487,56 @@ export class AgentSession {
|
|
|
8473
8487
|
? "priority"
|
|
8474
8488
|
: undefined;
|
|
8475
8489
|
}
|
|
8476
|
-
return
|
|
8490
|
+
if (!model) return undefined;
|
|
8491
|
+
return resolveModelServiceTier(this.#serviceTierByFamily, model);
|
|
8492
|
+
}
|
|
8493
|
+
|
|
8494
|
+
/** The live per-family tier map, or `null` when empty (for session persistence). */
|
|
8495
|
+
#serviceTierEntry(): ServiceTierByFamily | null {
|
|
8496
|
+
return Object.keys(this.#serviceTierByFamily).length > 0 ? this.#serviceTierByFamily : null;
|
|
8477
8497
|
}
|
|
8478
8498
|
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8499
|
+
/** Set one family's tier (or clear it with `undefined`); persists the change. */
|
|
8500
|
+
setServiceTierFamily(family: ServiceTierFamily, tier: ServiceTier | undefined): void {
|
|
8501
|
+
if (this.#serviceTierByFamily[family] === tier) return;
|
|
8502
|
+
const next: ServiceTierByFamily = { ...this.#serviceTierByFamily };
|
|
8503
|
+
if (tier) next[family] = tier;
|
|
8504
|
+
else delete next[family];
|
|
8505
|
+
this.#applyServiceTierByFamily(next);
|
|
8506
|
+
}
|
|
8507
|
+
|
|
8508
|
+
/** Replace the whole per-family tier map; persists + re-arms Anthropic fast mode. */
|
|
8509
|
+
#applyServiceTierByFamily(next: ServiceTierByFamily): void {
|
|
8510
|
+
// Re-arming Anthropic priority clears the per-session fast-mode auto-disable
|
|
8511
|
+
// so the next request actually carries `speed: "fast"` again.
|
|
8512
|
+
if (next.anthropic === "priority" && this.#serviceTierByFamily.anthropic !== "priority") {
|
|
8487
8513
|
clearAnthropicFastModeFallback(this.#providerSessionState);
|
|
8488
8514
|
}
|
|
8489
|
-
this
|
|
8490
|
-
this.sessionManager.appendServiceTierChange(
|
|
8515
|
+
this.#serviceTierByFamily = next;
|
|
8516
|
+
this.sessionManager.appendServiceTierChange(this.#serviceTierEntry());
|
|
8491
8517
|
}
|
|
8492
8518
|
|
|
8519
|
+
/**
|
|
8520
|
+
* `/fast on|off` targets the family of the currently selected model: it sets
|
|
8521
|
+
* (or clears) that family's `priority` tier. Models without a service-tier
|
|
8522
|
+
* family (Fireworks, or providers with no tier knob) have nothing to toggle.
|
|
8523
|
+
*/
|
|
8493
8524
|
setFastMode(enabled: boolean): void {
|
|
8494
|
-
|
|
8495
|
-
|
|
8525
|
+
const family = this.model ? serviceTierFamily(this.model) : undefined;
|
|
8526
|
+
if (!family) {
|
|
8527
|
+
this.emitNotice("info", "The current model has no service-tier control for /fast to toggle.", "priority");
|
|
8496
8528
|
return;
|
|
8497
8529
|
}
|
|
8498
8530
|
if (!enabled) {
|
|
8499
|
-
this.
|
|
8531
|
+
if (this.#serviceTierByFamily[family] === "priority") this.setServiceTierFamily(family, undefined);
|
|
8500
8532
|
return;
|
|
8501
8533
|
}
|
|
8502
|
-
|
|
8503
|
-
this.setServiceTier(scope === "openai" ? "openai-only" : scope === "claude" ? "claude-only" : "priority");
|
|
8534
|
+
this.setServiceTierFamily(family, "priority");
|
|
8504
8535
|
}
|
|
8505
8536
|
|
|
8506
8537
|
toggleFastMode(): boolean {
|
|
8507
|
-
|
|
8508
|
-
this.
|
|
8509
|
-
return enabled;
|
|
8538
|
+
this.setFastMode(!this.isFastModeEnabled());
|
|
8539
|
+
return this.isFastModeEnabled();
|
|
8510
8540
|
}
|
|
8511
8541
|
|
|
8512
8542
|
/**
|
|
@@ -8871,6 +8901,8 @@ export class AgentSession {
|
|
|
8871
8901
|
const wantsSnapcompact =
|
|
8872
8902
|
compactionPrep.kind !== "fromHook" && effectiveSettings.strategy === "snapcompact" && !customInstructions;
|
|
8873
8903
|
const snapcompactReady = wantsSnapcompact;
|
|
8904
|
+
const snapcompactShapeSetting = this.settings.get("snapcompact.shape");
|
|
8905
|
+
let snapcompactShape: snapcompact.Shape | undefined;
|
|
8874
8906
|
if (wantsSnapcompact && !this.model.input.includes("image")) {
|
|
8875
8907
|
this.emitNotice(
|
|
8876
8908
|
"warning",
|
|
@@ -8879,8 +8911,16 @@ export class AgentSession {
|
|
|
8879
8911
|
);
|
|
8880
8912
|
throw new Error(`snapcompact cannot run locally: ${this.model.id} is text-only.`);
|
|
8881
8913
|
} else if (snapcompactReady) {
|
|
8882
|
-
const text = snapcompact.serializeConversation(
|
|
8883
|
-
|
|
8914
|
+
const text = snapcompact.serializeConversation(
|
|
8915
|
+
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
|
8916
|
+
);
|
|
8917
|
+
const probeText = snapcompact.renderabilityProbeText(
|
|
8918
|
+
text,
|
|
8919
|
+
preparation.previousPreserveData,
|
|
8920
|
+
preparation.previousSummary,
|
|
8921
|
+
);
|
|
8922
|
+
snapcompactShape = snapcompact.resolveShapeForText(probeText, this.model, snapcompactShapeSetting);
|
|
8923
|
+
const renderScan = snapcompact.scanRenderability(probeText, { shape: snapcompactShape });
|
|
8884
8924
|
if (!renderScan.isSafe) {
|
|
8885
8925
|
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
|
8886
8926
|
this.emitNotice(
|
|
@@ -8918,12 +8958,32 @@ export class AgentSession {
|
|
|
8918
8958
|
);
|
|
8919
8959
|
throw new Error("snapcompact cannot run locally: kept history alone exceeds the context budget.");
|
|
8920
8960
|
} else {
|
|
8961
|
+
const shape = snapcompactShape;
|
|
8962
|
+
if (!shape) {
|
|
8963
|
+
throw new Error("snapcompact shape was not resolved before rendering.");
|
|
8964
|
+
}
|
|
8921
8965
|
snapcompactResult = await snapcompact.compact(preparation, {
|
|
8922
8966
|
convertToLlm,
|
|
8923
8967
|
model: this.model,
|
|
8924
|
-
|
|
8968
|
+
...(snapcompactShapeSetting === "auto" ? {} : { shape }),
|
|
8925
8969
|
maxFrames,
|
|
8926
8970
|
});
|
|
8971
|
+
const framePayloadBytes = this.#snapcompactFramePayloadBytes(snapcompactResult);
|
|
8972
|
+
if (framePayloadBytes > snapcompact.FRAME_DATA_BYTES_BUDGET) {
|
|
8973
|
+
logger.warn("Snapcompact exceeded the per-request frame payload budget", {
|
|
8974
|
+
model: this.model?.id,
|
|
8975
|
+
framePayloadBytes,
|
|
8976
|
+
budget: snapcompact.FRAME_DATA_BYTES_BUDGET,
|
|
8977
|
+
});
|
|
8978
|
+
this.emitNotice(
|
|
8979
|
+
"warning",
|
|
8980
|
+
"snapcompact produced too much standing image payload. No LLM fallback was attempted.",
|
|
8981
|
+
"compaction",
|
|
8982
|
+
);
|
|
8983
|
+
throw new Error(
|
|
8984
|
+
"snapcompact cannot run locally: standing image payload exceeds the per-request budget.",
|
|
8985
|
+
);
|
|
8986
|
+
}
|
|
8927
8987
|
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
8928
8988
|
const budget =
|
|
8929
8989
|
ctxWindow > 0
|
|
@@ -10886,7 +10946,7 @@ export class AgentSession {
|
|
|
10886
10946
|
*/
|
|
10887
10947
|
#computeSnapcompactMaxFrames(preparation: CompactionPreparation, settings: CompactionSettings): number {
|
|
10888
10948
|
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
10889
|
-
if (ctxWindow <= 0) return snapcompact.MAX_FRAMES_DEFAULT;
|
|
10949
|
+
if (ctxWindow <= 0) return Math.min(snapcompact.MAX_FRAMES_DEFAULT, snapcompact.maxFramesForDataBudget());
|
|
10890
10950
|
const reserve = effectiveReserveTokens(ctxWindow, settings);
|
|
10891
10951
|
let baseTokens = computeNonMessageTokens(this);
|
|
10892
10952
|
for (const message of preparation.recentMessages) {
|
|
@@ -10925,7 +10985,16 @@ export class AgentSession {
|
|
|
10925
10985
|
const capReserve = textEdgeTokens + SUMMARY_TEMPLATE_TOKENS;
|
|
10926
10986
|
const frameBudget = totalBudget - baseTokens - capReserve;
|
|
10927
10987
|
if (frameBudget < snapcompact.FRAME_TOKEN_ESTIMATE) return 1;
|
|
10928
|
-
return Math.min(
|
|
10988
|
+
return Math.min(
|
|
10989
|
+
Math.floor(frameBudget / snapcompact.FRAME_TOKEN_ESTIMATE),
|
|
10990
|
+
snapcompact.MAX_FRAMES_DEFAULT,
|
|
10991
|
+
snapcompact.maxFramesForDataBudget(),
|
|
10992
|
+
);
|
|
10993
|
+
}
|
|
10994
|
+
|
|
10995
|
+
#snapcompactFramePayloadBytes(result: snapcompact.CompactionResult): number {
|
|
10996
|
+
const archive = snapcompact.getPreservedArchive(result.preserveData);
|
|
10997
|
+
return archive ? snapcompact.frameDataBytes(archive.frames) : 0;
|
|
10929
10998
|
}
|
|
10930
10999
|
|
|
10931
11000
|
/**
|
|
@@ -10938,7 +11007,9 @@ export class AgentSession {
|
|
|
10938
11007
|
*/
|
|
10939
11008
|
#projectSnapcompactContextTokens(preparation: CompactionPreparation, result: snapcompact.CompactionResult): number {
|
|
10940
11009
|
const archive = snapcompact.getPreservedArchive(result.preserveData);
|
|
10941
|
-
const blocks = archive
|
|
11010
|
+
const blocks = archive
|
|
11011
|
+
? snapcompact.historyBlocks(archive, { maxFrameDataBytes: snapcompact.FRAME_DATA_BYTES_BUDGET })
|
|
11012
|
+
: undefined;
|
|
10942
11013
|
const summaryMessage = createCompactionSummaryMessage(
|
|
10943
11014
|
result.summary,
|
|
10944
11015
|
result.tokensBefore,
|
|
@@ -11029,6 +11100,52 @@ export class AgentSession {
|
|
|
11029
11100
|
return residualTokens <= fitBudget;
|
|
11030
11101
|
}
|
|
11031
11102
|
|
|
11103
|
+
/**
|
|
11104
|
+
* Last-resort reducer when {@link #runAutoCompaction} would otherwise dead-end.
|
|
11105
|
+
* The summarizer cut at the only available turn boundary, but the kept tail is
|
|
11106
|
+
* still over the recovery band because a single recent turn (a large
|
|
11107
|
+
* tool-result, a heavy fenced/XML block) is itself bigger than the band and
|
|
11108
|
+
* `findCutPoint` cannot cut inside one message. `shake("elide")` reaches INSIDE
|
|
11109
|
+
* that tail — it offloads heavy tool-result / block content to one
|
|
11110
|
+
* `artifact://` blob and leaves a recoverable placeholder — so residual context
|
|
11111
|
+
* genuinely drops instead of the guard pausing maintenance and looping the
|
|
11112
|
+
* warning. Without it the guard would pause/warn here; with it the caller
|
|
11113
|
+
* re-tests its progress predicate after the elide pass and only falls through
|
|
11114
|
+
* to the warning when residual stays over.
|
|
11115
|
+
*
|
|
11116
|
+
* Image-only tails are out of scope: `collectShakeRegions` skips image-only
|
|
11117
|
+
* tool results and user-message images aren't counted by the local estimate
|
|
11118
|
+
* that gates the dead-end, so those still surface the warning (remedy:
|
|
11119
|
+
* `/shake images`).
|
|
11120
|
+
*
|
|
11121
|
+
* Returns the elide {@link ShakeResult} when something was offloaded (so the
|
|
11122
|
+
* caller can re-test and report), or `undefined` when nothing was eligible or
|
|
11123
|
+
* the pass aborted/failed.
|
|
11124
|
+
*/
|
|
11125
|
+
async #tryShakeRescueForDeadEnd(signal: AbortSignal): Promise<ShakeResult | undefined> {
|
|
11126
|
+
if (signal.aborted) return undefined;
|
|
11127
|
+
try {
|
|
11128
|
+
const result = await this.shake("elide", { signal });
|
|
11129
|
+
return result.toolResultsDropped + result.blocksDropped > 0 ? result : undefined;
|
|
11130
|
+
} catch (error) {
|
|
11131
|
+
logger.warn("Dead-end shake rescue failed", {
|
|
11132
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11133
|
+
});
|
|
11134
|
+
return undefined;
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
|
|
11138
|
+
/** Notice describing a successful dead-end elide rescue. */
|
|
11139
|
+
#emitShakeRescueNotice(result: ShakeResult): void {
|
|
11140
|
+
const elided = result.toolResultsDropped + result.blocksDropped;
|
|
11141
|
+
const sink = result.artifactId ? "an artifact" : "placeholders";
|
|
11142
|
+
this.emitNotice(
|
|
11143
|
+
"info",
|
|
11144
|
+
`Compaction dead-end recovery: elided ${elided} heavy block${elided === 1 ? "" : "s"} (~${result.tokensFreed.toLocaleString()} tokens) to ${sink} so maintenance could make progress.`,
|
|
11145
|
+
"compaction",
|
|
11146
|
+
);
|
|
11147
|
+
}
|
|
11148
|
+
|
|
11032
11149
|
/**
|
|
11033
11150
|
* Internal: Run auto-compaction with events.
|
|
11034
11151
|
*
|
|
@@ -11062,6 +11179,7 @@ export class AgentSession {
|
|
|
11062
11179
|
const shouldAutoContinue =
|
|
11063
11180
|
!suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
11064
11181
|
const suppressHandoff = options.suppressHandoff === true;
|
|
11182
|
+
let fallbackFromShake = false;
|
|
11065
11183
|
// Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
|
|
11066
11184
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
11067
11185
|
// the oversized input still gets resolved.
|
|
@@ -11075,6 +11193,7 @@ export class AgentSession {
|
|
|
11075
11193
|
suppressContinuation,
|
|
11076
11194
|
);
|
|
11077
11195
|
if (outcome !== "fallback") return outcome;
|
|
11196
|
+
fallbackFromShake = true;
|
|
11078
11197
|
}
|
|
11079
11198
|
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
11080
11199
|
// paths the caller wants resolved before scheduling the next turn. "idle" is
|
|
@@ -11273,7 +11392,14 @@ export class AgentSession {
|
|
|
11273
11392
|
const text = snapcompact.serializeConversation(
|
|
11274
11393
|
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
|
11275
11394
|
);
|
|
11276
|
-
const
|
|
11395
|
+
const probeText = snapcompact.renderabilityProbeText(
|
|
11396
|
+
text,
|
|
11397
|
+
preparation.previousPreserveData,
|
|
11398
|
+
preparation.previousSummary,
|
|
11399
|
+
);
|
|
11400
|
+
const shapeSetting = this.settings.get("snapcompact.shape");
|
|
11401
|
+
const shape = snapcompact.resolveShapeForText(probeText, this.model, shapeSetting);
|
|
11402
|
+
const renderScan = snapcompact.scanRenderability(probeText, { shape });
|
|
11277
11403
|
if (!renderScan.isSafe) {
|
|
11278
11404
|
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
|
11279
11405
|
logger.warn("Snapcompact disabled: high non-ASCII rate detected", {
|
|
@@ -11293,9 +11419,20 @@ export class AgentSession {
|
|
|
11293
11419
|
snapcompactResult = await snapcompact.compact(preparation, {
|
|
11294
11420
|
convertToLlm,
|
|
11295
11421
|
model: this.model,
|
|
11296
|
-
|
|
11422
|
+
...(shapeSetting === "auto" ? {} : { shape }),
|
|
11297
11423
|
maxFrames,
|
|
11298
11424
|
});
|
|
11425
|
+
const framePayloadBytes = this.#snapcompactFramePayloadBytes(snapcompactResult);
|
|
11426
|
+
if (framePayloadBytes > snapcompact.FRAME_DATA_BYTES_BUDGET) {
|
|
11427
|
+
logger.warn("Snapcompact exceeded the per-request frame payload budget", {
|
|
11428
|
+
model: this.model?.id,
|
|
11429
|
+
framePayloadBytes,
|
|
11430
|
+
budget: snapcompact.FRAME_DATA_BYTES_BUDGET,
|
|
11431
|
+
});
|
|
11432
|
+
snapcompactBlocker =
|
|
11433
|
+
"snapcompact produced too much standing image payload; using context-full auto-compaction instead.";
|
|
11434
|
+
snapcompactResult = undefined;
|
|
11435
|
+
}
|
|
11299
11436
|
if (snapcompactResult) {
|
|
11300
11437
|
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
11301
11438
|
const budget =
|
|
@@ -11555,7 +11692,15 @@ export class AgentSession {
|
|
|
11555
11692
|
// won't include) is excluded. Reusing the auto-continue recovery band
|
|
11556
11693
|
// here turned recoverable overflows into manual dead-ends (#3412 review),
|
|
11557
11694
|
// so use the looser fit budget.
|
|
11558
|
-
|
|
11695
|
+
let retryFits = this.#compactionCreatedRetryFit();
|
|
11696
|
+
if (!retryFits && !fallbackFromShake) {
|
|
11697
|
+
const rescue = await this.#tryShakeRescueForDeadEnd(autoCompactionSignal);
|
|
11698
|
+
if (rescue && this.#compactionCreatedRetryFit()) {
|
|
11699
|
+
retryFits = true;
|
|
11700
|
+
this.#emitShakeRescueNotice(rescue);
|
|
11701
|
+
}
|
|
11702
|
+
}
|
|
11703
|
+
if (retryFits) {
|
|
11559
11704
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
11560
11705
|
continuationScheduled = true;
|
|
11561
11706
|
} else {
|
|
@@ -11569,7 +11714,15 @@ export class AgentSession {
|
|
|
11569
11714
|
// when auto-continue is disabled, a no-headroom threshold pass must still
|
|
11570
11715
|
// block later automatic continuations (todo reminders/session_stop hooks)
|
|
11571
11716
|
// from re-entering the same oversized context.
|
|
11572
|
-
|
|
11717
|
+
let hasHeadroom = this.#compactionCreatedHeadroom();
|
|
11718
|
+
if (!hasHeadroom && !fallbackFromShake) {
|
|
11719
|
+
const rescue = await this.#tryShakeRescueForDeadEnd(autoCompactionSignal);
|
|
11720
|
+
if (rescue && this.#compactionCreatedHeadroom()) {
|
|
11721
|
+
hasHeadroom = true;
|
|
11722
|
+
this.#emitShakeRescueNotice(rescue);
|
|
11723
|
+
}
|
|
11724
|
+
}
|
|
11725
|
+
if (hasHeadroom) {
|
|
11573
11726
|
if (shouldAutoContinue) {
|
|
11574
11727
|
this.#scheduleAutoContinuePrompt(generation);
|
|
11575
11728
|
continuationScheduled = true;
|
|
@@ -11593,7 +11746,7 @@ export class AgentSession {
|
|
|
11593
11746
|
if (noProgressDeadEnd) {
|
|
11594
11747
|
this.emitNotice(
|
|
11595
11748
|
"warning",
|
|
11596
|
-
"Compaction freed too little context to make progress — pausing automatic maintenance to avoid a compaction loop. The most recent turn alone is too large to reduce further;
|
|
11749
|
+
"Compaction freed too little context to make progress — pausing automatic maintenance to avoid a compaction loop. The most recent turn alone is too large to reduce further; clear large tool output, run `/shake images` to drop attached images, or switch to a larger-context model.",
|
|
11597
11750
|
"compaction",
|
|
11598
11751
|
);
|
|
11599
11752
|
}
|
|
@@ -12839,6 +12992,50 @@ export class AgentSession {
|
|
|
12839
12992
|
// IRC Delivery
|
|
12840
12993
|
// =========================================================================
|
|
12841
12994
|
|
|
12995
|
+
/**
|
|
12996
|
+
* Surfaces (and consumes) IRC incoming asides that have reached this running
|
|
12997
|
+
* session but have not yet been folded into the next model step.
|
|
12998
|
+
*
|
|
12999
|
+
* The inbox tool injects the formatted body into the tool result, so the
|
|
13000
|
+
* model sees it once via the result. Leaving the record in
|
|
13001
|
+
* {@link #pendingIrcAsides} would let the aside provider deliver it a second
|
|
13002
|
+
* time at the next step boundary — including on `peek`, which is why peek
|
|
13003
|
+
* also drains here.
|
|
13004
|
+
*/
|
|
13005
|
+
drainPendingIrcInboxMessages(agentId: string): IrcMessage[] {
|
|
13006
|
+
const messages: IrcMessage[] = [];
|
|
13007
|
+
const remaining: CustomMessage[] = [];
|
|
13008
|
+
for (const record of this.#pendingIrcAsides) {
|
|
13009
|
+
if (record.customType !== "irc:incoming") {
|
|
13010
|
+
remaining.push(record);
|
|
13011
|
+
continue;
|
|
13012
|
+
}
|
|
13013
|
+
const details = record.details;
|
|
13014
|
+
if (!details || typeof details !== "object") {
|
|
13015
|
+
remaining.push(record);
|
|
13016
|
+
continue;
|
|
13017
|
+
}
|
|
13018
|
+
const id = Reflect.get(details, "id");
|
|
13019
|
+
const from = Reflect.get(details, "from");
|
|
13020
|
+
const body = Reflect.get(details, "message");
|
|
13021
|
+
const replyTo = Reflect.get(details, "replyTo");
|
|
13022
|
+
if (typeof id !== "string" || typeof from !== "string" || typeof body !== "string") {
|
|
13023
|
+
remaining.push(record);
|
|
13024
|
+
continue;
|
|
13025
|
+
}
|
|
13026
|
+
messages.push({
|
|
13027
|
+
id,
|
|
13028
|
+
from,
|
|
13029
|
+
to: agentId,
|
|
13030
|
+
body,
|
|
13031
|
+
ts: record.timestamp,
|
|
13032
|
+
...(typeof replyTo === "string" ? { replyTo } : {}),
|
|
13033
|
+
});
|
|
13034
|
+
}
|
|
13035
|
+
this.#pendingIrcAsides = remaining;
|
|
13036
|
+
return messages;
|
|
13037
|
+
}
|
|
13038
|
+
|
|
12842
13039
|
/**
|
|
12843
13040
|
* Deliver an IRC message into this session (recipient side; called by the
|
|
12844
13041
|
* IrcBus). Emits the `irc_message` session event for UI cards and injects
|
|
@@ -13150,7 +13347,15 @@ export class AgentSession {
|
|
|
13150
13347
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
13151
13348
|
await this.sessionManager.flush();
|
|
13152
13349
|
const previousSessionState = this.sessionManager.captureState();
|
|
13153
|
-
|
|
13350
|
+
// Only same-session reloads compare against the prior context to detect
|
|
13351
|
+
// rollback edits (`#didSessionMessagesChange` below). Building it for a
|
|
13352
|
+
// different-session switch is a pure waste — and on huge pre-fix sessions
|
|
13353
|
+
// it materializes every persisted snapcompact frame plus the
|
|
13354
|
+
// `openaiRemoteCompaction.replacementHistory` payload into messages,
|
|
13355
|
+
// blowing the heap before the new session even loads (issue #3846). The
|
|
13356
|
+
// error-recovery path rebuilds the context on demand from the restored
|
|
13357
|
+
// state instead.
|
|
13358
|
+
const previousSessionContext = switchingToDifferentSession ? undefined : this.buildDisplaySessionContext();
|
|
13154
13359
|
// switchSession replaces these arrays wholesale during load/rollback, so retaining
|
|
13155
13360
|
// the existing message objects is sufficient and avoids structured-clone failures for
|
|
13156
13361
|
// extension/custom metadata that is valid to persist but not cloneable.
|
|
@@ -13163,7 +13368,7 @@ export class AgentSession {
|
|
|
13163
13368
|
const previousThinkingLevel = this.#thinkingLevel;
|
|
13164
13369
|
const previousAutoThinking = this.#autoThinking;
|
|
13165
13370
|
const previousAutoResolvedLevel = this.#autoResolvedLevel;
|
|
13166
|
-
const
|
|
13371
|
+
const previousServiceTierByFamily = this.#serviceTierByFamily;
|
|
13167
13372
|
const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
|
|
13168
13373
|
const previousTools = [...this.agent.state.tools];
|
|
13169
13374
|
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
@@ -13189,7 +13394,7 @@ export class AgentSession {
|
|
|
13189
13394
|
|
|
13190
13395
|
const sessionContext = this.buildDisplaySessionContext();
|
|
13191
13396
|
const didReloadConversationChange =
|
|
13192
|
-
|
|
13397
|
+
previousSessionContext !== undefined &&
|
|
13193
13398
|
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
13194
13399
|
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
13195
13400
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
@@ -13249,7 +13454,11 @@ export class AgentSession {
|
|
|
13249
13454
|
.getBranch()
|
|
13250
13455
|
.some(entry => entry.type === "service_tier_change");
|
|
13251
13456
|
const defaultThinkingLevel = parseConfiguredThinkingLevel(this.settings.get("defaultThinkingLevel"));
|
|
13252
|
-
const
|
|
13457
|
+
const configuredServiceTierByFamily = buildServiceTierByFamily(
|
|
13458
|
+
this.settings.get("tier.openai"),
|
|
13459
|
+
this.settings.get("tier.anthropic"),
|
|
13460
|
+
this.settings.get("tier.google"),
|
|
13461
|
+
);
|
|
13253
13462
|
// Restore the thinking selector. Each change persists the configured
|
|
13254
13463
|
// selector (`auto` or a concrete level), so prefer it: an `auto` session
|
|
13255
13464
|
// resumes in auto mode (reclassifying the next turn) instead of freezing at
|
|
@@ -13278,11 +13487,9 @@ export class AgentSession {
|
|
|
13278
13487
|
this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel);
|
|
13279
13488
|
}
|
|
13280
13489
|
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
13281
|
-
this
|
|
13282
|
-
? sessionContext.serviceTier
|
|
13283
|
-
:
|
|
13284
|
-
? undefined
|
|
13285
|
-
: configuredServiceTier;
|
|
13490
|
+
this.#serviceTierByFamily = hasServiceTierEntry
|
|
13491
|
+
? (sessionContext.serviceTier ?? {})
|
|
13492
|
+
: configuredServiceTierByFamily;
|
|
13286
13493
|
|
|
13287
13494
|
if (switchingToDifferentSession) {
|
|
13288
13495
|
await this.#resetMemoryContextForNewTranscript();
|
|
@@ -13305,7 +13512,12 @@ export class AgentSession {
|
|
|
13305
13512
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
13306
13513
|
let restoreMcpError: unknown;
|
|
13307
13514
|
try {
|
|
13308
|
-
|
|
13515
|
+
// `previousSessionContext` was skipped on different-session switches to
|
|
13516
|
+
// avoid materializing the previous session's heavy compaction payload
|
|
13517
|
+
// in the success path; rebuild it here on demand from the restored
|
|
13518
|
+
// state so MCP selection restoration still has its inputs.
|
|
13519
|
+
const mcpRestoreContext = previousSessionContext ?? this.buildDisplaySessionContext();
|
|
13520
|
+
await this.#restoreMCPSelectionsForSessionContext(mcpRestoreContext, {
|
|
13309
13521
|
fallbackSelectedMCPToolNames: previousFallbackSelectedMCPToolNames,
|
|
13310
13522
|
});
|
|
13311
13523
|
} catch (mcpError) {
|
|
@@ -13334,7 +13546,7 @@ export class AgentSession {
|
|
|
13334
13546
|
this.#autoThinking = previousAutoThinking;
|
|
13335
13547
|
this.#autoResolvedLevel = previousAutoResolvedLevel;
|
|
13336
13548
|
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
13337
|
-
this
|
|
13549
|
+
this.#serviceTierByFamily = previousServiceTierByFamily;
|
|
13338
13550
|
this.#syncTodoPhasesFromBranch();
|
|
13339
13551
|
this.#resetAllAdvisorRuntimes();
|
|
13340
13552
|
this.#reconnectToAgent();
|
|
@@ -14288,7 +14500,7 @@ export class AgentSession {
|
|
|
14288
14500
|
const payload = {
|
|
14289
14501
|
model: this.agent.state.model ?? null,
|
|
14290
14502
|
thinkingLevel: this.#thinkingLevel ?? null,
|
|
14291
|
-
serviceTier: this
|
|
14503
|
+
serviceTier: this.#serviceTierEntry(),
|
|
14292
14504
|
systemPrompt: this.agent.state.systemPrompt,
|
|
14293
14505
|
tools: this.agent.state.tools.map(tool => ({
|
|
14294
14506
|
name: tool.name,
|
package/src/session/messages.ts
CHANGED
|
@@ -488,7 +488,7 @@ export interface FileMentionMessage {
|
|
|
488
488
|
/** File size in bytes, if known. */
|
|
489
489
|
byteSize?: number;
|
|
490
490
|
/** Why the file contents were omitted from auto-read. */
|
|
491
|
-
skippedReason?: "tooLarge";
|
|
491
|
+
skippedReason?: "tooLarge" | "binary";
|
|
492
492
|
image?: ImageContent;
|
|
493
493
|
}>;
|
|
494
494
|
timestamp: number;
|