@oh-my-pi/pi-coding-agent 16.2.5 → 16.2.7
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 +41 -1
- package/dist/cli.js +3089 -3104
- package/dist/types/cli/bench-cli.d.ts +6 -1
- package/dist/types/commands/bench.d.ts +8 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/service-tier.d.ts +39 -24
- package/dist/types/config/settings-schema.d.ts +37 -37
- package/dist/types/edit/index.d.ts +1 -0
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/edit/snapshot-details.d.ts +33 -0
- package/dist/types/mcp/config-writer.d.ts +48 -0
- package/dist/types/mcp/oauth-flow.d.ts +4 -6
- package/dist/types/mcp/types.d.ts +3 -0
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
- package/dist/types/session/agent-session.d.ts +37 -13
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +2 -2
- package/dist/types/session/session-entries.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +5 -6
- 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/utils/git.d.ts +17 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/cli/args.ts +32 -1
- package/src/cli/bench-cli.ts +97 -22
- package/src/cli/tiny-models-cli.ts +18 -4
- package/src/cli/web-search-cli.ts +6 -1
- package/src/commands/bench.ts +10 -1
- package/src/config/mcp-schema.json +11 -2
- package/src/config/model-discovery.ts +66 -8
- package/src/config/model-registry.ts +13 -6
- package/src/config/service-tier.ts +85 -56
- package/src/config/settings-schema.ts +42 -36
- package/src/config/settings.ts +47 -0
- package/src/edit/hashline/execute.ts +15 -9
- package/src/edit/index.ts +19 -6
- package/src/edit/modes/patch.ts +3 -2
- package/src/edit/modes/replace.ts +3 -2
- package/src/edit/renderer.ts +4 -0
- package/src/edit/snapshot-details.ts +77 -0
- package/src/eval/agent-bridge.ts +4 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -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/oauth-flow.ts +10 -8
- package/src/mcp/transports/stdio.ts +9 -17
- 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 +24 -8
- package/src/modes/controllers/tool-args-reveal.ts +100 -22
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/prompts/bench.md +4 -10
- package/src/prompts/tools/irc.md +19 -29
- package/src/prompts/tools/job.md +8 -14
- package/src/prompts/tools/lsp.md +19 -30
- package/src/prompts/tools/task.md +42 -62
- package/src/sdk.ts +13 -11
- package/src/session/agent-session.ts +196 -76
- package/src/session/messages.ts +1 -1
- package/src/session/session-context.ts +4 -4
- package/src/session/session-entries.ts +2 -2
- package/src/session/session-listing.ts +9 -8
- package/src/session/session-loader.ts +98 -3
- package/src/session/session-manager.ts +43 -6
- package/src/slash-commands/builtin-registry.ts +2 -10
- package/src/subprocess/worker-client.ts +12 -4
- 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/index.ts +3 -3
- package/src/tools/irc.ts +9 -3
- package/src/tools/path-utils.ts +4 -2
- package/src/tools/read.ts +28 -28
- package/src/utils/file-mentions.ts +10 -1
- package/src/utils/git.ts +38 -0
- package/src/web/search/index.ts +14 -8
- package/src/web/search/providers/duckduckgo.ts +150 -78
- package/src/web/search/providers/gemini.ts +268 -185
- package/src/web/search/types.ts +1 -1
|
@@ -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 */
|
|
@@ -549,6 +555,8 @@ export interface AgentSessionConfig {
|
|
|
549
555
|
* main agent. Defaults to plain `streamSimple` when omitted.
|
|
550
556
|
*/
|
|
551
557
|
advisorStreamFn?: StreamFn;
|
|
558
|
+
/** Hint that OpenAI Codex requests should prefer websocket transport when supported. */
|
|
559
|
+
preferWebsockets?: boolean;
|
|
552
560
|
/** Provider payload hook used by the active session request path */
|
|
553
561
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
554
562
|
/** Provider response hook used by the active session request path */
|
|
@@ -1468,6 +1476,7 @@ export class AgentSession {
|
|
|
1468
1476
|
#transformProviderContext: ((context: Context, model: Model) => Context | Promise<Context>) | undefined;
|
|
1469
1477
|
#sideStreamFn: StreamFn;
|
|
1470
1478
|
#advisorStreamFn: StreamFn | undefined;
|
|
1479
|
+
#preferWebsockets: boolean | undefined;
|
|
1471
1480
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
1472
1481
|
#rebuildSystemPrompt:
|
|
1473
1482
|
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
@@ -1784,6 +1793,7 @@ export class AgentSession {
|
|
|
1784
1793
|
// toggle scopes priority to Fireworks alone, without mutating the shared
|
|
1785
1794
|
// session `serviceTier` that drives `/fast` and OpenAI/Anthropic priority.
|
|
1786
1795
|
this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
|
|
1796
|
+
this.#serviceTierByFamily = config.serviceTierByFamily ?? {};
|
|
1787
1797
|
this.#advisorTools = config.advisorTools;
|
|
1788
1798
|
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1789
1799
|
this.#advisorSharedInstructions = config.advisorSharedInstructions;
|
|
@@ -1798,6 +1808,7 @@ export class AgentSession {
|
|
|
1798
1808
|
this.#transformProviderContext = config.transformProviderContext;
|
|
1799
1809
|
this.#sideStreamFn = config.sideStreamFn ?? streamSimple;
|
|
1800
1810
|
this.#advisorStreamFn = config.advisorStreamFn;
|
|
1811
|
+
this.#preferWebsockets = config.preferWebsockets;
|
|
1801
1812
|
this.#onPayload = config.onPayload;
|
|
1802
1813
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
1803
1814
|
// Avoid wrapping in an `async` closure when no user callback is configured: the
|
|
@@ -2041,15 +2052,20 @@ export class AgentSession {
|
|
|
2041
2052
|
const legacy = !this.#advisorConfigs?.length;
|
|
2042
2053
|
const roster: AdvisorConfig[] = legacy ? [{ name: "default" }] : this.#advisorConfigs!;
|
|
2043
2054
|
|
|
2044
|
-
// Advisor service tier (`
|
|
2045
|
-
//
|
|
2046
|
-
// per request (like the main agent, including /fast toggles)
|
|
2047
|
-
//
|
|
2048
|
-
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
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);
|
|
2053
2069
|
|
|
2054
2070
|
const usedSlugs = new Set<string>();
|
|
2055
2071
|
for (const config of roster) {
|
|
@@ -2139,6 +2155,7 @@ export class AgentSession {
|
|
|
2139
2155
|
sessionId: advisorSessionId,
|
|
2140
2156
|
promptCacheKey: advisorSessionId,
|
|
2141
2157
|
providerSessionState: this.#providerSessionState,
|
|
2158
|
+
preferWebsockets: this.#preferWebsockets,
|
|
2142
2159
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
2143
2160
|
streamFn: this.#advisorStreamFn,
|
|
2144
2161
|
onPayload: this.#onPayload,
|
|
@@ -2147,7 +2164,7 @@ export class AgentSession {
|
|
|
2147
2164
|
transformProviderContext: this.#transformProviderContext,
|
|
2148
2165
|
intentTracing: false,
|
|
2149
2166
|
telemetry: advisorTelemetry,
|
|
2150
|
-
serviceTier:
|
|
2167
|
+
serviceTier: undefined,
|
|
2151
2168
|
serviceTierResolver: advisorServiceTierResolver,
|
|
2152
2169
|
});
|
|
2153
2170
|
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
@@ -2624,6 +2641,11 @@ export class AgentSession {
|
|
|
2624
2641
|
return this.#providerSessionState;
|
|
2625
2642
|
}
|
|
2626
2643
|
|
|
2644
|
+
/** Hint forwarded to provider calls that support websocket transport. */
|
|
2645
|
+
get preferWebsockets(): boolean | undefined {
|
|
2646
|
+
return this.#preferWebsockets;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2627
2649
|
getHindsightSessionState(): HindsightSessionState | undefined {
|
|
2628
2650
|
return this.#hindsightSessionState;
|
|
2629
2651
|
}
|
|
@@ -3205,10 +3227,11 @@ export class AgentSession {
|
|
|
3205
3227
|
if (event.message.role === "assistant") {
|
|
3206
3228
|
this.#lastAssistantMessage = event.message;
|
|
3207
3229
|
const assistantMsg = event.message as AssistantMessage;
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3230
|
+
if (
|
|
3231
|
+
assistantMsg.disabledFeatures?.includes("priority") &&
|
|
3232
|
+
this.#serviceTierByFamily.anthropic === "priority"
|
|
3233
|
+
) {
|
|
3234
|
+
this.setServiceTierFamily("anthropic", undefined);
|
|
3212
3235
|
this.emitNotice(
|
|
3213
3236
|
"warning",
|
|
3214
3237
|
"Priority/fast mode rejected for this model; retried without it. Fast mode is now off.",
|
|
@@ -5193,8 +5216,11 @@ export class AgentSession {
|
|
|
5193
5216
|
return this.#autoResolvedLevel;
|
|
5194
5217
|
}
|
|
5195
5218
|
|
|
5196
|
-
|
|
5197
|
-
|
|
5219
|
+
#serviceTierByFamily: ServiceTierByFamily = {};
|
|
5220
|
+
|
|
5221
|
+
/** Live per-family service tiers (OpenAI / Anthropic / Google). */
|
|
5222
|
+
get serviceTierByFamily(): ServiceTierByFamily {
|
|
5223
|
+
return this.#serviceTierByFamily;
|
|
5198
5224
|
}
|
|
5199
5225
|
|
|
5200
5226
|
/** Whether agent is currently streaming a response */
|
|
@@ -7882,7 +7908,7 @@ export class AgentSession {
|
|
|
7882
7908
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7883
7909
|
|
|
7884
7910
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
|
|
7885
|
-
this.sessionManager.appendServiceTierChange(this
|
|
7911
|
+
this.sessionManager.appendServiceTierChange(this.#serviceTierEntry());
|
|
7886
7912
|
if (nextDiscoverySessionToolNames) {
|
|
7887
7913
|
await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
|
|
7888
7914
|
if (this.getSelectedMCPToolNames().length > 0) {
|
|
@@ -8424,38 +8450,36 @@ export class AgentSession {
|
|
|
8424
8450
|
}
|
|
8425
8451
|
|
|
8426
8452
|
/**
|
|
8427
|
-
* True when
|
|
8428
|
-
*
|
|
8429
|
-
*
|
|
8430
|
-
*
|
|
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).
|
|
8431
8457
|
*
|
|
8432
|
-
* For "is
|
|
8433
|
-
* {@link isFastModeActive} instead
|
|
8458
|
+
* For "is priority actually applied to the next request?" use
|
|
8459
|
+
* {@link isFastModeActive} instead.
|
|
8434
8460
|
*/
|
|
8435
8461
|
isFastModeEnabled(): boolean {
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
);
|
|
8462
|
+
const family = this.model ? serviceTierFamily(this.model) : undefined;
|
|
8463
|
+
return family ? this.#serviceTierByFamily[family] === "priority" : false;
|
|
8439
8464
|
}
|
|
8440
8465
|
|
|
8441
8466
|
/**
|
|
8442
|
-
* True when
|
|
8443
|
-
*
|
|
8444
|
-
*
|
|
8445
|
-
* 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.
|
|
8446
8471
|
*/
|
|
8447
8472
|
isFastModeActive(): boolean {
|
|
8448
|
-
|
|
8473
|
+
const model = this.model;
|
|
8474
|
+
return !!model && realizesPriorityServiceTier(this.#effectiveServiceTier(model), model);
|
|
8449
8475
|
}
|
|
8450
8476
|
|
|
8451
8477
|
/**
|
|
8452
|
-
* Effective wire service-tier for a request to `model`. Fireworks models
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8455
|
-
*
|
|
8456
|
-
*
|
|
8457
|
-
* serving path is mutually exclusive with Priority. Every other provider
|
|
8458
|
-
* 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.
|
|
8459
8483
|
*/
|
|
8460
8484
|
#effectiveServiceTier(model: Model | undefined = this.model): ServiceTier | undefined {
|
|
8461
8485
|
if (model?.provider === "fireworks") {
|
|
@@ -8463,40 +8487,56 @@ export class AgentSession {
|
|
|
8463
8487
|
? "priority"
|
|
8464
8488
|
: undefined;
|
|
8465
8489
|
}
|
|
8466
|
-
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;
|
|
8497
|
+
}
|
|
8498
|
+
|
|
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);
|
|
8467
8506
|
}
|
|
8468
8507
|
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
// Re-arming priority
|
|
8472
|
-
//
|
|
8473
|
-
|
|
8474
|
-
// grants anthropic priority) after an auto-disable is a silent no-op
|
|
8475
|
-
// and the warning notice fires every turn.
|
|
8476
|
-
if (serviceTier === "priority" || serviceTier === "claude-only") {
|
|
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") {
|
|
8477
8513
|
clearAnthropicFastModeFallback(this.#providerSessionState);
|
|
8478
8514
|
}
|
|
8479
|
-
this
|
|
8480
|
-
this.sessionManager.appendServiceTierChange(
|
|
8515
|
+
this.#serviceTierByFamily = next;
|
|
8516
|
+
this.sessionManager.appendServiceTierChange(this.#serviceTierEntry());
|
|
8481
8517
|
}
|
|
8482
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
|
+
*/
|
|
8483
8524
|
setFastMode(enabled: boolean): void {
|
|
8484
|
-
|
|
8485
|
-
|
|
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");
|
|
8486
8528
|
return;
|
|
8487
8529
|
}
|
|
8488
8530
|
if (!enabled) {
|
|
8489
|
-
this.
|
|
8531
|
+
if (this.#serviceTierByFamily[family] === "priority") this.setServiceTierFamily(family, undefined);
|
|
8490
8532
|
return;
|
|
8491
8533
|
}
|
|
8492
|
-
|
|
8493
|
-
this.setServiceTier(scope === "openai" ? "openai-only" : scope === "claude" ? "claude-only" : "priority");
|
|
8534
|
+
this.setServiceTierFamily(family, "priority");
|
|
8494
8535
|
}
|
|
8495
8536
|
|
|
8496
8537
|
toggleFastMode(): boolean {
|
|
8497
|
-
|
|
8498
|
-
this.
|
|
8499
|
-
return enabled;
|
|
8538
|
+
this.setFastMode(!this.isFastModeEnabled());
|
|
8539
|
+
return this.isFastModeEnabled();
|
|
8500
8540
|
}
|
|
8501
8541
|
|
|
8502
8542
|
/**
|
|
@@ -8861,6 +8901,8 @@ export class AgentSession {
|
|
|
8861
8901
|
const wantsSnapcompact =
|
|
8862
8902
|
compactionPrep.kind !== "fromHook" && effectiveSettings.strategy === "snapcompact" && !customInstructions;
|
|
8863
8903
|
const snapcompactReady = wantsSnapcompact;
|
|
8904
|
+
const snapcompactShapeSetting = this.settings.get("snapcompact.shape");
|
|
8905
|
+
let snapcompactShape: snapcompact.Shape | undefined;
|
|
8864
8906
|
if (wantsSnapcompact && !this.model.input.includes("image")) {
|
|
8865
8907
|
this.emitNotice(
|
|
8866
8908
|
"warning",
|
|
@@ -8869,8 +8911,16 @@ export class AgentSession {
|
|
|
8869
8911
|
);
|
|
8870
8912
|
throw new Error(`snapcompact cannot run locally: ${this.model.id} is text-only.`);
|
|
8871
8913
|
} else if (snapcompactReady) {
|
|
8872
|
-
const text = snapcompact.serializeConversation(
|
|
8873
|
-
|
|
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 });
|
|
8874
8924
|
if (!renderScan.isSafe) {
|
|
8875
8925
|
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
|
8876
8926
|
this.emitNotice(
|
|
@@ -8908,10 +8958,14 @@ export class AgentSession {
|
|
|
8908
8958
|
);
|
|
8909
8959
|
throw new Error("snapcompact cannot run locally: kept history alone exceeds the context budget.");
|
|
8910
8960
|
} else {
|
|
8961
|
+
const shape = snapcompactShape;
|
|
8962
|
+
if (!shape) {
|
|
8963
|
+
throw new Error("snapcompact shape was not resolved before rendering.");
|
|
8964
|
+
}
|
|
8911
8965
|
snapcompactResult = await snapcompact.compact(preparation, {
|
|
8912
8966
|
convertToLlm,
|
|
8913
8967
|
model: this.model,
|
|
8914
|
-
|
|
8968
|
+
...(snapcompactShapeSetting === "auto" ? {} : { shape }),
|
|
8915
8969
|
maxFrames,
|
|
8916
8970
|
});
|
|
8917
8971
|
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
@@ -11263,7 +11317,14 @@ export class AgentSession {
|
|
|
11263
11317
|
const text = snapcompact.serializeConversation(
|
|
11264
11318
|
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
|
11265
11319
|
);
|
|
11266
|
-
const
|
|
11320
|
+
const probeText = snapcompact.renderabilityProbeText(
|
|
11321
|
+
text,
|
|
11322
|
+
preparation.previousPreserveData,
|
|
11323
|
+
preparation.previousSummary,
|
|
11324
|
+
);
|
|
11325
|
+
const shapeSetting = this.settings.get("snapcompact.shape");
|
|
11326
|
+
const shape = snapcompact.resolveShapeForText(probeText, this.model, shapeSetting);
|
|
11327
|
+
const renderScan = snapcompact.scanRenderability(probeText, { shape });
|
|
11267
11328
|
if (!renderScan.isSafe) {
|
|
11268
11329
|
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
|
11269
11330
|
logger.warn("Snapcompact disabled: high non-ASCII rate detected", {
|
|
@@ -11283,7 +11344,7 @@ export class AgentSession {
|
|
|
11283
11344
|
snapcompactResult = await snapcompact.compact(preparation, {
|
|
11284
11345
|
convertToLlm,
|
|
11285
11346
|
model: this.model,
|
|
11286
|
-
|
|
11347
|
+
...(shapeSetting === "auto" ? {} : { shape }),
|
|
11287
11348
|
maxFrames,
|
|
11288
11349
|
});
|
|
11289
11350
|
if (snapcompactResult) {
|
|
@@ -12829,6 +12890,50 @@ export class AgentSession {
|
|
|
12829
12890
|
// IRC Delivery
|
|
12830
12891
|
// =========================================================================
|
|
12831
12892
|
|
|
12893
|
+
/**
|
|
12894
|
+
* Surfaces (and consumes) IRC incoming asides that have reached this running
|
|
12895
|
+
* session but have not yet been folded into the next model step.
|
|
12896
|
+
*
|
|
12897
|
+
* The inbox tool injects the formatted body into the tool result, so the
|
|
12898
|
+
* model sees it once via the result. Leaving the record in
|
|
12899
|
+
* {@link #pendingIrcAsides} would let the aside provider deliver it a second
|
|
12900
|
+
* time at the next step boundary — including on `peek`, which is why peek
|
|
12901
|
+
* also drains here.
|
|
12902
|
+
*/
|
|
12903
|
+
drainPendingIrcInboxMessages(agentId: string): IrcMessage[] {
|
|
12904
|
+
const messages: IrcMessage[] = [];
|
|
12905
|
+
const remaining: CustomMessage[] = [];
|
|
12906
|
+
for (const record of this.#pendingIrcAsides) {
|
|
12907
|
+
if (record.customType !== "irc:incoming") {
|
|
12908
|
+
remaining.push(record);
|
|
12909
|
+
continue;
|
|
12910
|
+
}
|
|
12911
|
+
const details = record.details;
|
|
12912
|
+
if (!details || typeof details !== "object") {
|
|
12913
|
+
remaining.push(record);
|
|
12914
|
+
continue;
|
|
12915
|
+
}
|
|
12916
|
+
const id = Reflect.get(details, "id");
|
|
12917
|
+
const from = Reflect.get(details, "from");
|
|
12918
|
+
const body = Reflect.get(details, "message");
|
|
12919
|
+
const replyTo = Reflect.get(details, "replyTo");
|
|
12920
|
+
if (typeof id !== "string" || typeof from !== "string" || typeof body !== "string") {
|
|
12921
|
+
remaining.push(record);
|
|
12922
|
+
continue;
|
|
12923
|
+
}
|
|
12924
|
+
messages.push({
|
|
12925
|
+
id,
|
|
12926
|
+
from,
|
|
12927
|
+
to: agentId,
|
|
12928
|
+
body,
|
|
12929
|
+
ts: record.timestamp,
|
|
12930
|
+
...(typeof replyTo === "string" ? { replyTo } : {}),
|
|
12931
|
+
});
|
|
12932
|
+
}
|
|
12933
|
+
this.#pendingIrcAsides = remaining;
|
|
12934
|
+
return messages;
|
|
12935
|
+
}
|
|
12936
|
+
|
|
12832
12937
|
/**
|
|
12833
12938
|
* Deliver an IRC message into this session (recipient side; called by the
|
|
12834
12939
|
* IrcBus). Emits the `irc_message` session event for UI cards and injects
|
|
@@ -13140,7 +13245,15 @@ export class AgentSession {
|
|
|
13140
13245
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
13141
13246
|
await this.sessionManager.flush();
|
|
13142
13247
|
const previousSessionState = this.sessionManager.captureState();
|
|
13143
|
-
|
|
13248
|
+
// Only same-session reloads compare against the prior context to detect
|
|
13249
|
+
// rollback edits (`#didSessionMessagesChange` below). Building it for a
|
|
13250
|
+
// different-session switch is a pure waste — and on huge pre-fix sessions
|
|
13251
|
+
// it materializes every persisted snapcompact frame plus the
|
|
13252
|
+
// `openaiRemoteCompaction.replacementHistory` payload into messages,
|
|
13253
|
+
// blowing the heap before the new session even loads (issue #3846). The
|
|
13254
|
+
// error-recovery path rebuilds the context on demand from the restored
|
|
13255
|
+
// state instead.
|
|
13256
|
+
const previousSessionContext = switchingToDifferentSession ? undefined : this.buildDisplaySessionContext();
|
|
13144
13257
|
// switchSession replaces these arrays wholesale during load/rollback, so retaining
|
|
13145
13258
|
// the existing message objects is sufficient and avoids structured-clone failures for
|
|
13146
13259
|
// extension/custom metadata that is valid to persist but not cloneable.
|
|
@@ -13153,7 +13266,7 @@ export class AgentSession {
|
|
|
13153
13266
|
const previousThinkingLevel = this.#thinkingLevel;
|
|
13154
13267
|
const previousAutoThinking = this.#autoThinking;
|
|
13155
13268
|
const previousAutoResolvedLevel = this.#autoResolvedLevel;
|
|
13156
|
-
const
|
|
13269
|
+
const previousServiceTierByFamily = this.#serviceTierByFamily;
|
|
13157
13270
|
const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
|
|
13158
13271
|
const previousTools = [...this.agent.state.tools];
|
|
13159
13272
|
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
@@ -13179,7 +13292,7 @@ export class AgentSession {
|
|
|
13179
13292
|
|
|
13180
13293
|
const sessionContext = this.buildDisplaySessionContext();
|
|
13181
13294
|
const didReloadConversationChange =
|
|
13182
|
-
|
|
13295
|
+
previousSessionContext !== undefined &&
|
|
13183
13296
|
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
13184
13297
|
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
13185
13298
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
@@ -13239,7 +13352,11 @@ export class AgentSession {
|
|
|
13239
13352
|
.getBranch()
|
|
13240
13353
|
.some(entry => entry.type === "service_tier_change");
|
|
13241
13354
|
const defaultThinkingLevel = parseConfiguredThinkingLevel(this.settings.get("defaultThinkingLevel"));
|
|
13242
|
-
const
|
|
13355
|
+
const configuredServiceTierByFamily = buildServiceTierByFamily(
|
|
13356
|
+
this.settings.get("tier.openai"),
|
|
13357
|
+
this.settings.get("tier.anthropic"),
|
|
13358
|
+
this.settings.get("tier.google"),
|
|
13359
|
+
);
|
|
13243
13360
|
// Restore the thinking selector. Each change persists the configured
|
|
13244
13361
|
// selector (`auto` or a concrete level), so prefer it: an `auto` session
|
|
13245
13362
|
// resumes in auto mode (reclassifying the next turn) instead of freezing at
|
|
@@ -13268,11 +13385,9 @@ export class AgentSession {
|
|
|
13268
13385
|
this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel);
|
|
13269
13386
|
}
|
|
13270
13387
|
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
13271
|
-
this
|
|
13272
|
-
? sessionContext.serviceTier
|
|
13273
|
-
:
|
|
13274
|
-
? undefined
|
|
13275
|
-
: configuredServiceTier;
|
|
13388
|
+
this.#serviceTierByFamily = hasServiceTierEntry
|
|
13389
|
+
? (sessionContext.serviceTier ?? {})
|
|
13390
|
+
: configuredServiceTierByFamily;
|
|
13276
13391
|
|
|
13277
13392
|
if (switchingToDifferentSession) {
|
|
13278
13393
|
await this.#resetMemoryContextForNewTranscript();
|
|
@@ -13295,7 +13410,12 @@ export class AgentSession {
|
|
|
13295
13410
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
13296
13411
|
let restoreMcpError: unknown;
|
|
13297
13412
|
try {
|
|
13298
|
-
|
|
13413
|
+
// `previousSessionContext` was skipped on different-session switches to
|
|
13414
|
+
// avoid materializing the previous session's heavy compaction payload
|
|
13415
|
+
// in the success path; rebuild it here on demand from the restored
|
|
13416
|
+
// state so MCP selection restoration still has its inputs.
|
|
13417
|
+
const mcpRestoreContext = previousSessionContext ?? this.buildDisplaySessionContext();
|
|
13418
|
+
await this.#restoreMCPSelectionsForSessionContext(mcpRestoreContext, {
|
|
13299
13419
|
fallbackSelectedMCPToolNames: previousFallbackSelectedMCPToolNames,
|
|
13300
13420
|
});
|
|
13301
13421
|
} catch (mcpError) {
|
|
@@ -13324,7 +13444,7 @@ export class AgentSession {
|
|
|
13324
13444
|
this.#autoThinking = previousAutoThinking;
|
|
13325
13445
|
this.#autoResolvedLevel = previousAutoResolvedLevel;
|
|
13326
13446
|
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
13327
|
-
this
|
|
13447
|
+
this.#serviceTierByFamily = previousServiceTierByFamily;
|
|
13328
13448
|
this.#syncTodoPhasesFromBranch();
|
|
13329
13449
|
this.#resetAllAdvisorRuntimes();
|
|
13330
13450
|
this.#reconnectToAgent();
|
|
@@ -14278,7 +14398,7 @@ export class AgentSession {
|
|
|
14278
14398
|
const payload = {
|
|
14279
14399
|
model: this.agent.state.model ?? null,
|
|
14280
14400
|
thinkingLevel: this.#thinkingLevel ?? null,
|
|
14281
|
-
serviceTier: this
|
|
14401
|
+
serviceTier: this.#serviceTierEntry(),
|
|
14282
14402
|
systemPrompt: this.agent.state.systemPrompt,
|
|
14283
14403
|
tools: this.agent.state.tools.map(tool => ({
|
|
14284
14404
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type
|
|
2
|
+
import { coerceServiceTierByFamily, type ProviderPayload, type ServiceTierByFamily } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
4
4
|
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "./messages";
|
|
5
5
|
import { type CompactionEntry, EPHEMERAL_MODEL_CHANGE_ROLE, type SessionEntry } from "./session-entries";
|
|
@@ -9,7 +9,7 @@ export interface SessionContext {
|
|
|
9
9
|
thinkingLevel?: string;
|
|
10
10
|
/** Configured thinking selector (`"auto"` or a concrete level) from the latest change. */
|
|
11
11
|
configuredThinkingLevel?: string;
|
|
12
|
-
serviceTier?:
|
|
12
|
+
serviceTier?: ServiceTierByFamily;
|
|
13
13
|
/** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
14
14
|
models: Record<string, string>;
|
|
15
15
|
/** Names of TTSR rules that have been injected this session */
|
|
@@ -138,7 +138,7 @@ export function buildSessionContext(
|
|
|
138
138
|
// Extract settings and find compaction
|
|
139
139
|
let thinkingLevel: string | undefined = "off";
|
|
140
140
|
let configuredThinkingLevel: string | undefined;
|
|
141
|
-
let serviceTier:
|
|
141
|
+
let serviceTier: ServiceTierByFamily | undefined;
|
|
142
142
|
const models: Record<string, string> = {};
|
|
143
143
|
let compaction: CompactionEntry | null = null;
|
|
144
144
|
const injectedTtsrRulesSet = new Set<string>();
|
|
@@ -169,7 +169,7 @@ export function buildSessionContext(
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
} else if (entry.type === "service_tier_change") {
|
|
172
|
-
serviceTier = entry.serviceTier
|
|
172
|
+
serviceTier = coerceServiceTierByFamily(entry.serviceTier);
|
|
173
173
|
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
174
174
|
// Legacy fallback: infer default model from assistant messages only
|
|
175
175
|
// when no explicit `model_change` (role=default) entry has been
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { ImageContent, MessageAttribution,
|
|
2
|
+
import type { ImageContent, MessageAttribution, ServiceTierByFamily, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
|
|
4
4
|
export const CURRENT_SESSION_VERSION = 3;
|
|
5
5
|
|
|
@@ -73,7 +73,7 @@ export interface ModelChangeEntry extends SessionEntryBase {
|
|
|
73
73
|
|
|
74
74
|
export interface ServiceTierChangeEntry extends SessionEntryBase {
|
|
75
75
|
type: "service_tier_change";
|
|
76
|
-
serviceTier:
|
|
76
|
+
serviceTier: ServiceTierByFamily | null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
|
@@ -245,20 +245,21 @@ function countMessageMarkers(content: string): number {
|
|
|
245
245
|
return count;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
function extractFirstDisplayMessageFromPrefix(content: string): string | undefined {
|
|
249
|
+
let fallback: string | undefined;
|
|
250
|
+
let index = content.indexOf('"role"');
|
|
251
251
|
|
|
252
|
-
let index = roleIndex;
|
|
253
252
|
while (index !== -1) {
|
|
254
253
|
const role = extractStringProperty(content, "role", index);
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
const text = extractStringProperty(content, "content", index) ?? extractStringProperty(content, "text", index);
|
|
255
|
+
if (text) {
|
|
256
|
+
if (role === "user") return text;
|
|
257
|
+
if (!fallback && (role === "developer" || role === "assistant")) fallback = text;
|
|
257
258
|
}
|
|
258
259
|
index = content.indexOf('"role"', index + 6);
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
return
|
|
262
|
+
return fallback;
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
interface SessionListHeader {
|
|
@@ -394,7 +395,7 @@ async function scanSessionFile(
|
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
firstMessage ||=
|
|
398
|
+
firstMessage ||= extractFirstDisplayMessageFromPrefix(content) ?? "";
|
|
398
399
|
const messageCount = Math.max(parsedMessageCount, countMessageMarkers(content));
|
|
399
400
|
return {
|
|
400
401
|
path: file,
|