fix(coding-agent): route compaction through streamFn

closes #4484
This commit is contained in:
Armin Ronacher
2026-05-17 01:57:03 +02:00
parent 2c708492e3
commit 35f807cfaf
4 changed files with 154 additions and 27 deletions

View File

@@ -31,6 +31,7 @@ import {
isContextOverflow,
modelsAreEqual,
resetApiProviders,
streamSimple,
} from "@earendil-works/pi-ai";
import { theme } from "../modes/interactive/theme/theme.js";
import { stripFrontmatter } from "../utils/frontmatter.js";
@@ -367,6 +368,18 @@ export class AgentSession {
throw new Error(formatNoApiKeyFoundMessage(model.provider));
}
private async _getCompactionRequestAuth(model: Model<any>): Promise<{
apiKey?: string;
headers?: Record<string, string>;
}> {
if (this.agent.streamFn === streamSimple) {
return this._getRequiredRequestAuth(model);
}
const result = await this._modelRegistry.getApiKeyAndHeaders(model);
return result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};
}
/**
* Install tool hooks once on the Agent instance.
*
@@ -1618,7 +1631,7 @@ export class AgentSession {
throw new Error(formatNoModelSelectedMessage());
}
const { apiKey, headers } = await this._getRequiredRequestAuth(this.model);
const { apiKey, headers } = await this._getCompactionRequestAuth(this.model);
const pathEntries = this.sessionManager.getBranch();
const settings = this.settingsManager.getCompactionSettings();
@@ -1676,6 +1689,7 @@ export class AgentSession {
customInstructions,
this._compactionAbortController.signal,
this.thinkingLevel,
this.agent.streamFn,
);
summary = result.summary;
firstKeptEntryId = result.firstKeptEntryId;
@@ -1864,18 +1878,25 @@ export class AgentSession {
return;
}
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
if (!authResult.ok || !authResult.apiKey) {
this._emit({
type: "compaction_end",
reason,
result: undefined,
aborted: false,
willRetry: false,
});
return;
let apiKey: string | undefined;
let headers: Record<string, string> | undefined;
if (this.agent.streamFn === streamSimple) {
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
if (!authResult.ok || !authResult.apiKey) {
this._emit({
type: "compaction_end",
reason,
result: undefined,
aborted: false,
willRetry: false,
});
return;
}
apiKey = authResult.apiKey;
headers = authResult.headers;
} else {
({ apiKey, headers } = await this._getCompactionRequestAuth(this.model));
}
const { apiKey, headers } = authResult;
const pathEntries = this.sessionManager.getBranch();
@@ -1941,6 +1962,7 @@ export class AgentSession {
undefined,
this._autoCompactionAbortController.signal,
this.thinkingLevel,
this.agent.streamFn,
);
summary = compactResult.summary;
firstKeptEntryId = compactResult.firstKeptEntryId;

View File

@@ -5,8 +5,8 @@
* and after compaction the session is reloaded.
*/
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { AssistantMessage, Model, Usage } from "@earendil-works/pi-ai";
import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai";
import { completeSimple } from "@earendil-works/pi-ai";
import {
convertToLlm,
@@ -523,6 +523,34 @@ Use this EXACT format:
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
function createSummarizationOptions(
model: Model<any>,
maxTokens: number,
apiKey: string | undefined,
headers: Record<string, string> | undefined,
signal: AbortSignal | undefined,
thinkingLevel: ThinkingLevel | undefined,
): SimpleStreamOptions {
const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers };
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
options.reasoning = thinkingLevel;
}
return options;
}
async function completeSummarization(
model: Model<any>,
context: Context,
options: SimpleStreamOptions,
streamFn?: StreamFn,
): Promise<AssistantMessage> {
if (!streamFn) {
return completeSimple(model, context, options);
}
const stream = await streamFn(model, context, options);
return stream.result();
}
/**
* Generate a summary of the conversation using the LLM.
* If previousSummary is provided, uses the update prompt to merge.
@@ -531,12 +559,13 @@ export async function generateSummary(
currentMessages: AgentMessage[],
model: Model<any>,
reserveTokens: number,
apiKey: string,
apiKey: string | undefined,
headers?: Record<string, string>,
signal?: AbortSignal,
customInstructions?: string,
previousSummary?: string,
thinkingLevel?: ThinkingLevel,
streamFn?: StreamFn,
): Promise<string> {
const maxTokens = Math.min(
Math.floor(0.8 * reserveTokens),
@@ -569,15 +598,13 @@ export async function generateSummary(
},
];
const completionOptions =
model.reasoning && thinkingLevel && thinkingLevel !== "off"
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
: { maxTokens, signal, apiKey, headers };
const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel);
const response = await completeSimple(
const response = await completeSummarization(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
completionOptions,
streamFn,
);
if (response.stopReason === "error") {
@@ -720,11 +747,12 @@ Be concise. Focus on what's needed to understand the kept suffix.`;
export async function compact(
preparation: CompactionPreparation,
model: Model<any>,
apiKey: string,
apiKey: string | undefined,
headers?: Record<string, string>,
customInstructions?: string,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
streamFn?: StreamFn,
): Promise<CompactionResult> {
const {
firstKeptEntryId,
@@ -754,6 +782,7 @@ export async function compact(
customInstructions,
previousSummary,
thinkingLevel,
streamFn,
)
: Promise.resolve("No prior history."),
generateTurnPrefixSummary(
@@ -764,6 +793,7 @@ export async function compact(
headers,
signal,
thinkingLevel,
streamFn,
),
]);
// Merge into single summary
@@ -780,6 +810,7 @@ export async function compact(
customInstructions,
previousSummary,
thinkingLevel,
streamFn,
);
}
@@ -806,10 +837,11 @@ async function generateTurnPrefixSummary(
messages: AgentMessage[],
model: Model<any>,
reserveTokens: number,
apiKey: string,
apiKey: string | undefined,
headers?: Record<string, string>,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
streamFn?: StreamFn,
): Promise<string> {
const maxTokens = Math.min(
Math.floor(0.5 * reserveTokens),
@@ -826,12 +858,11 @@ async function generateTurnPrefixSummary(
},
];
const response = await completeSimple(
const response = await completeSummarization(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
model.reasoning && thinkingLevel && thinkingLevel !== "off"
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
: { maxTokens, signal, apiKey, headers },
createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),
streamFn,
);
if (response.stopReason === "error") {