This commit is contained in:
xu0o0 2026-11-05 14:37:05 -08:00 • committed by GitHub
commit f8fd5d2b96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 353 additions and 15 deletions

View file

@ -416,6 +416,8 @@ See `SessionBeforeTreeEvent` and `TreePreparation` in the types file.
## Settings ## Settings
See [Settings: Compaction](settings.md#compaction) and [Settings: Branch summaries](settings.md#branch-summaries) for summary model and thinking-level options.
Configure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`: Configure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`:
```json ```json

View file

@ -74,6 +74,13 @@ CLI tool options override this setting for one invocation. `--tools` with only `
| `compaction.reserveTokens` | number | `16384` | Free context tokens below which automatic compaction starts. | | `compaction.reserveTokens` | number | `16384` | Free context tokens below which automatic compaction starts. |
| `compaction.keepRecentTokens` | number | `20000` | Recent tokens retained without summarization. | | `compaction.keepRecentTokens` | number | `20000` | Recent tokens retained without summarization. |
| `compaction.modelOverrides` | object | None | Per-model token settings keyed by exact `provider/modelId`. | | `compaction.modelOverrides` | object | None | Per-model token settings keyed by exact `provider/modelId`. |
| `compaction.provider` | string | current session provider | Provider used for compaction summaries. |
| `compaction.model` | string | current session model | Model ID used for compaction summaries. |
| `compaction.thinkingLevel` | string | current session thinking level | Thinking level for compaction summaries: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`. |
`compaction.provider` and `compaction.model` can be set independently. An omitted component is inherited from the current session model. Explicit model settings must resolve to an existing model; invalid settings produce an error rather than falling back to the session model.
Choose a summary model whose context window can accommodate the history being summarized. Compaction does not split that history to fit a smaller summary model or automatically fall back to another model. Automatic compaction thresholds still use the session model's context window. If the summary model has a smaller window, increase `compaction.reserveTokens` (or its session-model override) to compact earlier.
<a id="per-model-compaction-overrides"></a> <a id="per-model-compaction-overrides"></a>
@ -87,6 +94,11 @@ See [Compaction Reference](compaction.md) for trigger, summarization, and valida
|---|---|---|---| |---|---|---|---|
| `branchSummary.reserveTokens` | number | `16384` | Tokens reserved when summarizing branch history. | | `branchSummary.reserveTokens` | number | `16384` | Tokens reserved when summarizing branch history. |
| `branchSummary.skipPrompt` | boolean | `false` | Skip the branch-summary prompt and default to no summary. | | `branchSummary.skipPrompt` | boolean | `false` | Skip the branch-summary prompt and default to no summary. |
| `branchSummary.provider` | string | current session provider | Provider used for branch summaries. |
| `branchSummary.model` | string | current session model | Model ID used for branch summaries. |
| `branchSummary.thinkingLevel` | string | current session thinking level | Thinking level for branch summaries: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`. |
`branchSummary.provider` and `branchSummary.model` can be set independently. An omitted component is inherited from the current session model. Explicit model settings must resolve to an existing model; invalid settings produce an error rather than falling back to the session model.
## Terminal and display ## Terminal and display

View file

@ -109,6 +109,15 @@
"compaction": { "compaction": {
"type": "object", "type": "object",
"properties": { "properties": {
"provider": {
"type": "string"
},
"model": {
"type": "string"
},
"thinkingLevel": {
"$ref": "#/$defs/ModelThinkingLevel"
},
"enabled": { "enabled": {
"type": "boolean", "type": "boolean",
"default": true "default": true
@ -151,6 +160,15 @@
"branchSummary": { "branchSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"provider": {
"type": "string"
},
"model": {
"type": "string"
},
"thinkingLevel": {
"$ref": "#/$defs/ModelThinkingLevel"
},
"reserveTokens": { "reserveTokens": {
"type": "number", "type": "number",
"description": "Tokens reserved for the prompt and LLM response.", "description": "Tokens reserved for the prompt and LLM response.",

View file

@ -605,6 +605,7 @@ export class AgentSession {
private async _getSummarizationRequestAuth( private async _getSummarizationRequestAuth(
selectedModel: Model<any>, selectedModel: Model<any>,
signal?: AbortSignal, signal?: AbortSignal,
selectedThinkingLevel: ThinkingLevel = this.thinkingLevel,
): Promise<{ ): Promise<{
model: Model<any>; model: Model<any>;
apiKey?: string; apiKey?: string;
@ -616,10 +617,10 @@ export class AgentSession {
const { model, thinkingLevel } = isVirtualModel(selectedModel) const { model, thinkingLevel } = isVirtualModel(selectedModel)
? await this._modelRuntime.resolveModel(selectedModel, convertToLlm(this.messages), { ? await this._modelRuntime.resolveModel(selectedModel, convertToLlm(this.messages), {
reason: "direct", reason: "direct",
thinkingLevel: this.thinkingLevel, thinkingLevel: selectedThinkingLevel,
signal, signal,
}) })
: { model: selectedModel, thinkingLevel: this.thinkingLevel }; : { model: selectedModel, thinkingLevel: selectedThinkingLevel };
if (this.agent.streamFunction === streamSimple) { if (this.agent.streamFunction === streamSimple) {
return { ...(await this._getRequiredRequestAuth(model, signal)), thinkingLevel }; return { ...(await this._getRequiredRequestAuth(model, signal)), thinkingLevel };
} }
@ -644,6 +645,36 @@ export class AgentSession {
} }
} }
private _getSummarizationModel(
settings: { provider?: string; model?: string; thinkingLevel?: ThinkingLevel },
fallbackModel: Model<any> | undefined,
settingsKey: "compaction" | "branchSummary",
): { model: Model<any>; thinkingLevel: ThinkingLevel } {
const hasModelOverride = settings.provider !== undefined || settings.model !== undefined;
const provider = settings.provider ?? fallbackModel?.provider;
const modelId = settings.model ?? fallbackModel?.id;
if (!provider || !modelId) {
if (hasModelOverride) {
throw new Error(
`Unable to resolve summarization model. Check ${settingsKey}.provider and ${settingsKey}.model in your settings.`,
);
}
throw new Error(formatNoModelSelectedMessage());
}
const model = hasModelOverride ? this._modelRuntime.getModel(provider, modelId) : fallbackModel;
if (!model) {
throw new Error(
`Summarization model not found: ${provider}/${modelId}. Check ${settingsKey}.provider and ${settingsKey}.model in your settings.`,
);
}
return {
model,
thinkingLevel: clampThinkingLevel(model, settings.thinkingLevel ?? this.thinkingLevel),
};
}
/** /**
* The model whose limits apply to `message`, or undefined when the message came from another * The model whose limits apply to `message`, or undefined when the message came from another
* model. Under a virtual selection, that is the physical model that produced it. * model. Under a virtual selection, that is the physical model that produced it.
@ -2852,13 +2883,15 @@ export class AgentSession {
/** Generate Pi's built-in compaction summary for manual and automatic compaction. */ /** Generate Pi's built-in compaction summary for manual and automatic compaction. */
private async _runDefaultCompaction( private async _runDefaultCompaction(
preparation: CompactionPreparation, preparation: CompactionPreparation,
model: Model<any>, model: Model<any> | undefined,
customInstructions: string | undefined, customInstructions: string | undefined,
signal: AbortSignal, signal: AbortSignal,
reason: "manual" | "threshold" | "overflow", reason: "manual" | "threshold" | "overflow",
settings: { provider?: string; model?: string; thinkingLevel?: ThinkingLevel },
): Promise<CompactionResult> { ): Promise<CompactionResult> {
// Resolve the request only when Pi summarizes itself: routing may call models or fail. // Resolve the request only when Pi summarizes itself: routing may call models or fail.
const request = await this._getSummarizationRequestAuth(model, signal); const summaryConfig = this._getSummarizationModel(settings, model, "compaction");
const request = await this._getSummarizationRequestAuth(summaryConfig.model, signal, summaryConfig.thinkingLevel);
return compact( return compact(
preparation, preparation,
request.model, request.model,
@ -2904,11 +2937,9 @@ export class AgentSession {
try { try {
const model = this.model; const model = this.model;
if (!model) {
throw new Error(formatNoModelSelectedMessage());
}
const settings = this.settingsManager.getCompactionSettings(model); const settings = this.settingsManager.getCompactionSettings(model);
// Report invalid model settings before preparation errors or extension hooks.
this._getSummarizationModel(settings, model, "compaction");
const pathEntries = this.sessionManager.getBranch(); const pathEntries = this.sessionManager.getBranch();
const preparation = prepareCompaction(pathEntries, settings); const preparation = prepareCompaction(pathEntries, settings);
@ -2966,6 +2997,7 @@ export class AgentSession {
customInstructions, customInstructions,
this._compactionAbortController.signal, this._compactionAbortController.signal,
"manual", "manual",
settings,
); );
summary = result.summary; summary = result.summary;
firstKeptEntryId = result.firstKeptEntryId; firstKeptEntryId = result.firstKeptEntryId;
@ -3233,7 +3265,7 @@ export class AgentSession {
let cancelledByExtension = false; let cancelledByExtension = false;
try { try {
if (!model) { if (!model && settings.provider === undefined && settings.model === undefined) {
return false; return false;
} }
@ -3295,6 +3327,7 @@ export class AgentSession {
undefined, undefined,
abortController.signal, abortController.signal,
reason, reason,
settings,
); );
summary = compactResult.summary; summary = compactResult.summary;
firstKeptEntryId = compactResult.firstKeptEntryId; firstKeptEntryId = compactResult.firstKeptEntryId;
@ -4118,8 +4151,15 @@ export class AgentSession {
return { cancelled: false }; return { cancelled: false };
} }
const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
// Model required for summarization // Model required for summarization
if (options.summarize && !this.model) { if (
options.summarize &&
!this.model &&
branchSummarySettings.provider === undefined &&
branchSummarySettings.model === undefined
) {
throw new Error("No model available for summarization"); throw new Error("No model available for summarization");
} }
@ -4194,9 +4234,13 @@ export class AgentSession {
let summaryUsage: Usage | undefined; let summaryUsage: Usage | undefined;
if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) { if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
const signal = abortController.signal; const signal = abortController.signal;
const branchSummarySettings = this.settingsManager.getBranchSummarySettings(); const { model, thinkingLevel } = this._getSummarizationModel(
branchSummarySettings,
this.model,
"branchSummary",
);
const result = await generateBranchSummary(entriesToSummarize, { const result = await generateBranchSummary(entriesToSummarize, {
...(await this._getSummarizationRequestAuth(this.model!, signal)), ...(await this._getSummarizationRequestAuth(model, signal, thinkingLevel)),
signal, signal,
customInstructions, customInstructions,
replaceInstructions, replaceInstructions,

View file

@ -121,6 +121,9 @@ export interface CompactionSettings {
enabled: boolean; enabled: boolean;
reserveTokens: number; reserveTokens: number;
keepRecentTokens: number; keepRecentTokens: number;
provider?: string;
model?: string;
thinkingLevel?: ThinkingLevel;
} }
export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = { export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {

View file

@ -884,18 +884,33 @@ export class SettingsManager {
enabled: boolean; enabled: boolean;
reserveTokens: number; reserveTokens: number;
keepRecentTokens: number; keepRecentTokens: number;
provider?: string;
model?: string;
thinkingLevel?: ThinkingLevel;
} { } {
return { return {
enabled: this.getCompactionEnabled(), enabled: this.getCompactionEnabled(),
reserveTokens: this.getCompactionReserveTokens(model), reserveTokens: this.getCompactionReserveTokens(model),
keepRecentTokens: this.getCompactionKeepRecentTokens(model), keepRecentTokens: this.getCompactionKeepRecentTokens(model),
provider: this.settings.compaction?.provider,
model: this.settings.compaction?.model,
thinkingLevel: this.settings.compaction?.thinkingLevel,
}; };
} }
getBranchSummarySettings(): { reserveTokens: number; skipPrompt: boolean } { getBranchSummarySettings(): {
reserveTokens: number;
skipPrompt: boolean;
provider?: string;
model?: string;
thinkingLevel?: ThinkingLevel;
} {
return { return {
reserveTokens: this.settings.branchSummary?.reserveTokens ?? SETTINGS_DEFAULTS.branchSummary.reserveTokens, reserveTokens: this.settings.branchSummary?.reserveTokens ?? SETTINGS_DEFAULTS.branchSummary.reserveTokens,
skipPrompt: this.settings.branchSummary?.skipPrompt ?? SETTINGS_DEFAULTS.branchSummary.skipPrompt, skipPrompt: this.settings.branchSummary?.skipPrompt ?? SETTINGS_DEFAULTS.branchSummary.skipPrompt,
provider: this.settings.branchSummary?.provider,
model: this.settings.branchSummary?.model,
thinkingLevel: this.settings.branchSummary?.thinkingLevel,
}; };
} }
@ -903,6 +918,23 @@ export class SettingsManager {
return this.settings.branchSummary?.skipPrompt ?? SETTINGS_DEFAULTS.branchSummary.skipPrompt; return this.settings.branchSummary?.skipPrompt ?? SETTINGS_DEFAULTS.branchSummary.skipPrompt;
} }
setSummarizationConfig(
settingsKey: "compaction" | "branchSummary",
provider: string,
model: string,
thinkingLevel: ThinkingLevel,
): void {
const settings = this.globalSettings[settingsKey] ?? {};
this.globalSettings[settingsKey] = settings;
settings.provider = provider;
settings.model = model;
settings.thinkingLevel = thinkingLevel;
this.markModified(settingsKey, "provider");
this.markModified(settingsKey, "model");
this.markModified(settingsKey, "thinkingLevel");
this.save();
}
getRetryEnabled(): boolean { getRetryEnabled(): boolean {
return this.settings.retry?.enabled ?? SETTINGS_DEFAULTS.retry.enabled; return this.settings.retry?.enabled ?? SETTINGS_DEFAULTS.retry.enabled;
} }

View file

@ -16,6 +16,9 @@ const CompactionModelOverrideSchema = Type.Object({
}); });
const CompactionSettingsSchema = Type.Object({ const CompactionSettingsSchema = Type.Object({
provider: Type.Optional(Type.String()),
model: Type.Optional(Type.String()),
thinkingLevel: Type.Optional(ModelThinkingLevelSchema),
enabled: Type.Optional(Type.Boolean({ default: SETTINGS_DEFAULTS.compaction.enabled })), enabled: Type.Optional(Type.Boolean({ default: SETTINGS_DEFAULTS.compaction.enabled })),
reserveTokens: Type.Optional(nonNegativeSafeInteger({ default: SETTINGS_DEFAULTS.compaction.reserveTokens })), reserveTokens: Type.Optional(nonNegativeSafeInteger({ default: SETTINGS_DEFAULTS.compaction.reserveTokens })),
keepRecentTokens: Type.Optional(nonNegativeSafeInteger({ default: SETTINGS_DEFAULTS.compaction.keepRecentTokens })), keepRecentTokens: Type.Optional(nonNegativeSafeInteger({ default: SETTINGS_DEFAULTS.compaction.keepRecentTokens })),
@ -27,6 +30,9 @@ const CompactionSettingsSchema = Type.Object({
}); });
const BranchSummarySettingsSchema = Type.Object({ const BranchSummarySettingsSchema = Type.Object({
provider: Type.Optional(Type.String()),
model: Type.Optional(Type.String()),
thinkingLevel: Type.Optional(ModelThinkingLevelSchema),
reserveTokens: Type.Optional( reserveTokens: Type.Optional(
Type.Number({ Type.Number({
description: "Tokens reserved for the prompt and LLM response.", description: "Tokens reserved for the prompt and LLM response.",

View file

@ -25,6 +25,31 @@ describe("SettingsManager", () => {
} }
}); });
describe("summarization settings", () => {
it("persists compaction and branch summary model configurations", () => {
const manager = SettingsManager.inMemory({
compaction: { enabled: false },
branchSummary: { skipPrompt: true },
});
manager.setSummarizationConfig("compaction", "anthropic", "compact-model", "low");
manager.setSummarizationConfig("branchSummary", "openai", "branch-model", "high");
expect(manager.getGlobalSettings().compaction).toEqual({
enabled: false,
provider: "anthropic",
model: "compact-model",
thinkingLevel: "low",
});
expect(manager.getGlobalSettings().branchSummary).toEqual({
skipPrompt: true,
provider: "openai",
model: "branch-model",
thinkingLevel: "high",
});
});
});
describe("preserves externally added settings", () => { describe("preserves externally added settings", () => {
it("should preserve enabledModels when changing thinking level", async () => { it("should preserve enabledModels when changing thinking level", async () => {
// Create initial settings file // Create initial settings file

View file

@ -57,12 +57,12 @@ function createAssistant(
function useSummaryStreamFn( function useSummaryStreamFn(
harness: Harness, harness: Harness,
summary: string, summary: string,
onRequest?: (context: TranscriptContext, options: SimpleStreamOptions | undefined) => void, onRequest?: (context: TranscriptContext, options: SimpleStreamOptions | undefined, model: Model<string>) => void,
): () => number { ): () => number {
let callCount = 0; let callCount = 0;
harness.session.agent.streamFunction = (model, context, options) => { harness.session.agent.streamFunction = (model, context, options) => {
callCount++; callCount++;
onRequest?.(context, options); onRequest?.(context, options, model);
const stream = createAssistantMessageEventStream(); const stream = createAssistantMessageEventStream();
queueMicrotask(() => { queueMicrotask(() => {
const message: AssistantMessage = { const message: AssistantMessage = {
@ -379,6 +379,90 @@ describe("AgentSession compaction characterization", () => {
); );
}); });
it("uses configured compaction model and thinking level with the session provider", async () => {
const harness = await createHarness({
models: [{ id: "faux-1" }, { id: "faux-summary", reasoning: true }],
settings: {
compaction: { keepRecentTokens: 1, model: "faux-summary", thinkingLevel: "high" },
},
});
harnesses.push(harness);
seedCompactableSession(harness);
const calls: Array<{ modelId: string; reasoning: SimpleStreamOptions["reasoning"] }> = [];
useSummaryStreamFn(harness, "summary from configured model", (_context, options, model) => {
calls.push({ modelId: model.id, reasoning: options?.reasoning });
});
const result = await harness.session.compact();
expect(result.summary).toContain("summary from configured model");
expect(calls).toEqual([{ modelId: "faux-summary", reasoning: "high" }]);
});
it.each([
{ thinkingLevel: undefined, reasoning: "medium" },
{ thinkingLevel: "high", reasoning: "high" },
{ thinkingLevel: "off", reasoning: undefined },
{ thinkingLevel: "max", reasoning: "high" },
] as const)(
"uses configured manual compaction model and thinking without mutation: $thinkingLevel",
async ({ thinkingLevel, reasoning }) => {
const harness = await createHarness({
models: [
{ id: "faux-1", reasoning: true },
{ id: "faux-configured", reasoning: true },
],
settings: {
compaction: { keepRecentTokens: 1, model: "faux-configured", thinkingLevel },
},
});
harnesses.push(harness);
harness.session.agent.state.thinkingLevel = "medium";
seedCompactableSession(harness);
const mainModel = harness.session.model;
const mainThinkingLevel = harness.session.thinkingLevel;
const settingsBefore = {
compaction: harness.settingsManager.getCompactionSettings(),
defaultProvider: harness.settingsManager.getDefaultProvider(),
defaultModel: harness.settingsManager.getDefaultModel(),
defaultThinkingLevel: harness.settingsManager.getDefaultThinkingLevel(),
};
const calls: Array<{ modelId: string; reasoning: SimpleStreamOptions["reasoning"] }> = [];
useSummaryStreamFn(harness, "configured summary", (_context, options, model) => {
calls.push({ modelId: model.id, reasoning: options?.reasoning });
});
const result = await harness.session.compact();
expect(result.summary).toContain("configured summary");
expect(calls).toEqual([{ modelId: "faux-configured", reasoning }]);
expect(harness.session.model).toBe(mainModel);
expect(harness.session.thinkingLevel).toBe(mainThinkingLevel);
expect({
compaction: harness.settingsManager.getCompactionSettings(),
defaultProvider: harness.settingsManager.getDefaultProvider(),
defaultModel: harness.settingsManager.getDefaultModel(),
defaultThinkingLevel: harness.settingsManager.getDefaultThinkingLevel(),
}).toEqual(settingsBefore);
expect(
harness.sessionManager
.getEntries()
.filter((entry) => entry.type === "model_change" || entry.type === "thinking_level_change"),
).toHaveLength(0);
},
);
it("points to the relevant settings when a summarization model is not found", async () => {
const harness = await createHarness({
settings: { compaction: { provider: "missing-provider" } },
});
harnesses.push(harness);
await expect(harness.session.compact()).rejects.toThrow(
"Summarization model not found: missing-provider/faux-1. Check compaction.provider and compaction.model in your settings.",
);
});
it("auto-compacts with a custom streamFn when registry auth is absent", async () => { it("auto-compacts with a custom streamFn when registry auth is absent", async () => {
const harness = await createHarness({ withConfiguredAuth: false }); const harness = await createHarness({ withConfiguredAuth: false });
harnesses.push(harness); harnesses.push(harness);
@ -439,6 +523,118 @@ describe("AgentSession compaction characterization", () => {
]); ]);
}); });
it("auto-compacts with the configured summarization model when the session model is absent", async () => {
const harness = await createHarness({
models: [{ id: "faux-1" }, { id: "faux-summary" }],
settings: { compaction: { keepRecentTokens: 1, provider: "faux", model: "faux-summary" } },
});
harnesses.push(harness);
seedCompactableSession(harness);
harness.session.agent.state.model = undefined as unknown as Model<string>;
const getStreamCallCount = useSummaryStreamFn(harness, "summary without session model");
const sessionInternals = harness.session as unknown as SessionWithCompactionInternals;
await sessionInternals._runAutoCompaction("threshold", false);
const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction");
expect(compactionEntries).toHaveLength(1);
expect(getStreamCallCount()).toBe(1);
});
it("uses configured branch summary model and thinking level", async () => {
const harness = await createHarness({
models: [{ id: "faux-1" }, { id: "faux-branch-summary", reasoning: true }],
settings: {
branchSummary: { provider: "faux", model: "faux-branch-summary", thinkingLevel: "medium" },
},
});
harnesses.push(harness);
const rootId = harness.sessionManager.appendMessage({
role: "user",
content: [{ type: "text", text: "root" }],
timestamp: Date.now(),
});
harness.sessionManager.appendMessage({
role: "user",
content: [{ type: "text", text: "branch work" }],
timestamp: Date.now(),
});
harness.sessionManager.appendMessage(createAssistant(harness, { stopReason: "stop", totalTokens: 10 }));
const calls: Array<{ modelId: string; reasoning: SimpleStreamOptions["reasoning"] }> = [];
harness.session.agent.state.model = undefined as unknown as Model<string>;
useSummaryStreamFn(harness, "branch summary from configured model", (_context, options, model) => {
calls.push({ modelId: model.id, reasoning: options?.reasoning });
});
const result = await harness.session.navigateTree(rootId, { summarize: true });
expect(result.summaryEntry?.summary).toContain("branch summary from configured model");
expect(calls).toEqual([{ modelId: "faux-branch-summary", reasoning: "medium" }]);
});
it.each([
{ thinkingLevel: undefined, reasoning: "medium" },
{ thinkingLevel: "high", reasoning: "high" },
{ thinkingLevel: "off", reasoning: undefined },
{ thinkingLevel: "max", reasoning: "high" },
] as const)(
"uses configured branch summary model and thinking without mutation: $thinkingLevel",
async ({ thinkingLevel, reasoning }) => {
const harness = await createHarness({
models: [
{ id: "faux-1", reasoning: true },
{ id: "faux-configured", reasoning: true },
],
settings: {
branchSummary: { provider: "faux", model: "faux-configured", thinkingLevel },
},
});
harnesses.push(harness);
harness.session.agent.state.thinkingLevel = "medium";
const rootId = harness.sessionManager.appendMessage({
role: "user",
content: [{ type: "text", text: "root" }],
timestamp: Date.now(),
});
harness.sessionManager.appendMessage({
role: "user",
content: [{ type: "text", text: "branch work" }],
timestamp: Date.now(),
});
harness.sessionManager.appendMessage(createAssistant(harness, { stopReason: "stop", totalTokens: 10 }));
const mainModel = harness.session.model;
const mainThinkingLevel = harness.session.thinkingLevel;
const settingsBefore = {
branchSummary: harness.settingsManager.getBranchSummarySettings(),
defaultProvider: harness.settingsManager.getDefaultProvider(),
defaultModel: harness.settingsManager.getDefaultModel(),
defaultThinkingLevel: harness.settingsManager.getDefaultThinkingLevel(),
};
const calls: Array<{ modelId: string; reasoning: SimpleStreamOptions["reasoning"] }> = [];
useSummaryStreamFn(harness, "branch summary from configured model", (_context, options, model) => {
calls.push({ modelId: model.id, reasoning: options?.reasoning });
});
const result = await harness.session.navigateTree(rootId, { summarize: true });
expect(result.summaryEntry?.summary).toContain("branch summary from configured model");
expect(calls).toEqual([{ modelId: "faux-configured", reasoning }]);
expect(harness.session.model).toBe(mainModel);
expect(harness.session.thinkingLevel).toBe(mainThinkingLevel);
expect({
branchSummary: harness.settingsManager.getBranchSummarySettings(),
defaultProvider: harness.settingsManager.getDefaultProvider(),
defaultModel: harness.settingsManager.getDefaultModel(),
defaultThinkingLevel: harness.settingsManager.getDefaultThinkingLevel(),
}).toEqual(settingsBefore);
expect(
harness.sessionManager
.getEntries()
.filter((entry) => entry.type === "model_change" || entry.type === "thinking_level_change"),
).toHaveLength(0);
},
);
it("compacts and resumes after a length stop below the desired output limit", async () => { it("compacts and resumes after a length stop below the desired output limit", async () => {
const harness = await createHarness({ const harness = await createHarness({
models: [{ id: "faux-1", contextWindow: 1000, maxTokens: 100 }], models: [{ id: "faux-1", contextWindow: 1000, maxTokens: 100 }],