mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Merge c42686d983 into 80d4d3c869
This commit is contained in:
commit
f8fd5d2b96
9 changed files with 353 additions and 15 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue