merge: sync upstream pi-mono main into sproutclaw
Some checks failed
CI / build-check-test (push) Has been cancelled
Some checks failed
CI / build-check-test (push) Has been cancelled
Merge upstream/main (195 commits) while keeping sproutclaw-specific README, webui workflow docs, and restored upstream .pi prompt templates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { basename, dirname } from "node:path";
|
||||
import type {
|
||||
Agent,
|
||||
AgentEvent,
|
||||
@@ -31,12 +31,14 @@ import {
|
||||
isContextOverflow,
|
||||
modelsAreEqual,
|
||||
resetApiProviders,
|
||||
streamSimple,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { theme } from "../modes/interactive/theme/theme.js";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.js";
|
||||
import { sleep } from "../utils/sleep.js";
|
||||
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
||||
import { type BashResult, executeBashWithOperations } from "./bash-executor.js";
|
||||
import { theme } from "../modes/interactive/theme/theme.ts";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { sleep } from "../utils/sleep.ts";
|
||||
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.ts";
|
||||
import { type BashResult, executeBashWithOperations } from "./bash-executor.ts";
|
||||
import {
|
||||
type CompactionResult,
|
||||
calculateContextTokens,
|
||||
@@ -46,10 +48,10 @@ import {
|
||||
generateBranchSummary,
|
||||
prepareCompaction,
|
||||
shouldCompact,
|
||||
} from "./compaction/index.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.js";
|
||||
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
||||
} from "./compaction/index.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.ts";
|
||||
import { createToolHtmlRenderer } from "./export-html/tool-renderer.ts";
|
||||
import {
|
||||
type ContextUsage,
|
||||
type ExtensionCommandContextActions,
|
||||
@@ -74,21 +76,21 @@ import {
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
wrapRegisteredTools,
|
||||
} from "./extensions/index.js";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||
import type { BashExecutionMessage, CustomMessage } from "./messages.js";
|
||||
import type { ModelRegistry } from "./model-registry.js";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js";
|
||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.js";
|
||||
import type { SettingsManager } from "./settings-manager.js";
|
||||
import type { SlashCommandInfo } from "./slash-commands.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.js";
|
||||
import { type BashOperations, createLocalBashOperations } from "./tools/bash.js";
|
||||
import { createAllToolDefinitions } from "./tools/index.js";
|
||||
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
||||
} from "./extensions/index.ts";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.ts";
|
||||
import type { BashExecutionMessage, CustomMessage } from "./messages.ts";
|
||||
import type { ModelRegistry } from "./model-registry.ts";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.ts";
|
||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.ts";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.ts";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.ts";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
import type { SlashCommandInfo } from "./slash-commands.ts";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.ts";
|
||||
import { type BashOperations, createLocalBashOperations } from "./tools/bash.ts";
|
||||
import { createAllToolDefinitions } from "./tools/index.ts";
|
||||
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Skill Block Parsing
|
||||
@@ -119,7 +121,12 @@ export function parseSkillBlock(text: string): ParsedSkillBlock | null {
|
||||
|
||||
/** Session-specific events that extend the core AgentEvent */
|
||||
export type AgentSessionEvent =
|
||||
| AgentEvent
|
||||
| Exclude<AgentEvent, { type: "agent_end" }>
|
||||
| {
|
||||
type: "agent_end";
|
||||
messages: AgentMessage[];
|
||||
willRetry: boolean;
|
||||
}
|
||||
| {
|
||||
type: "queue_update";
|
||||
steering: readonly string[];
|
||||
@@ -179,6 +186,7 @@ export interface AgentSessionConfig {
|
||||
export interface ExtensionBindings {
|
||||
uiContext?: ExtensionUIContext;
|
||||
commandContextActions?: ExtensionCommandContextActions;
|
||||
abortHandler?: () => void;
|
||||
shutdownHandler?: ShutdownHandler;
|
||||
onError?: ExtensionErrorListener;
|
||||
}
|
||||
@@ -251,7 +259,6 @@ export class AgentSession {
|
||||
// Event subscription state
|
||||
private _unsubscribeAgent?: () => void;
|
||||
private _eventListeners: AgentSessionEventListener[] = [];
|
||||
private _agentEventQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
||||
private _steeringMessages: string[] = [];
|
||||
@@ -271,8 +278,6 @@ export class AgentSession {
|
||||
// Retry state
|
||||
private _retryAbortController: AbortController | undefined = undefined;
|
||||
private _retryAttempt = 0;
|
||||
private _retryPromise: Promise<void> | undefined = undefined;
|
||||
private _retryResolve: (() => void) | undefined = undefined;
|
||||
|
||||
// Bash execution state
|
||||
private _bashAbortController: AbortController | undefined = undefined;
|
||||
@@ -293,6 +298,7 @@ export class AgentSession {
|
||||
private _sessionStartEvent: SessionStartEvent;
|
||||
private _extensionUIContext?: ExtensionUIContext;
|
||||
private _extensionCommandContextActions?: ExtensionCommandContextActions;
|
||||
private _extensionAbortHandler?: () => void;
|
||||
private _extensionShutdownHandler?: ShutdownHandler;
|
||||
private _extensionErrorListener?: ExtensionErrorListener;
|
||||
private _extensionErrorUnsubscriber?: () => void;
|
||||
@@ -367,6 +373,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.
|
||||
*
|
||||
@@ -382,8 +400,6 @@ export class AgentSession {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this._agentEventQueue;
|
||||
|
||||
try {
|
||||
return await runner.emitToolCall({
|
||||
type: "tool_call",
|
||||
@@ -450,54 +466,7 @@ export class AgentSession {
|
||||
private _lastAssistantMessage: AssistantMessage | undefined = undefined;
|
||||
|
||||
/** Internal handler for agent events - shared by subscribe and reconnect */
|
||||
private _handleAgentEvent = (event: AgentEvent): void => {
|
||||
// Create retry promise synchronously before queueing async processing.
|
||||
// Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()
|
||||
// as soon as agent.prompt() resolves. If _retryPromise is created only inside
|
||||
// _processAgentEvent, slow earlier queued events can delay agent_end processing
|
||||
// and waitForRetry() can miss the in-flight retry.
|
||||
this._createRetryPromiseForAgentEnd(event);
|
||||
|
||||
this._agentEventQueue = this._agentEventQueue.then(
|
||||
() => this._processAgentEvent(event),
|
||||
() => this._processAgentEvent(event),
|
||||
);
|
||||
|
||||
// Keep queue alive if an event handler fails
|
||||
this._agentEventQueue.catch(() => {});
|
||||
};
|
||||
|
||||
private _createRetryPromiseForAgentEnd(event: AgentEvent): void {
|
||||
if (event.type !== "agent_end" || this._retryPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastAssistant = this._findLastAssistantInMessages(event.messages);
|
||||
if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._retryPromise = new Promise((resolve) => {
|
||||
this._retryResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
private _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.role === "assistant") {
|
||||
return message as AssistantMessage;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _processAgentEvent(event: AgentEvent): Promise<void> {
|
||||
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
||||
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
||||
// This ensures the UI sees the updated queue state
|
||||
if (event.type === "message_start" && event.message.role === "user") {
|
||||
@@ -524,7 +493,7 @@ export class AgentSession {
|
||||
await this._emitExtensionEvent(event);
|
||||
|
||||
// Notify all listeners
|
||||
this._emit(event);
|
||||
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
||||
|
||||
// Handle session persistence
|
||||
if (event.type === "message_end") {
|
||||
@@ -568,30 +537,21 @@ export class AgentSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check auto-retry and auto-compaction after agent completes
|
||||
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = undefined;
|
||||
private _willRetryAfterAgentEnd(event: Extract<AgentEvent, { type: "agent_end" }>): boolean {
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for retryable errors first (overloaded, rate limit, server errors)
|
||||
if (this._isRetryableError(msg)) {
|
||||
const didRetry = await this._handleRetryableError(msg);
|
||||
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
||||
for (let i = event.messages.length - 1; i >= 0; i--) {
|
||||
const message = event.messages[i];
|
||||
if (message.role === "assistant") {
|
||||
return this._isRetryableError(message as AssistantMessage);
|
||||
}
|
||||
|
||||
this._resolveRetry();
|
||||
await this._checkCompaction(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the pending retry promise */
|
||||
private _resolveRetry(): void {
|
||||
if (this._retryResolve) {
|
||||
this._retryResolve();
|
||||
this._retryResolve = undefined;
|
||||
this._retryPromise = undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Extract text content from a message */
|
||||
@@ -617,7 +577,7 @@ export class AgentSession {
|
||||
|
||||
private _replaceMessageInPlace(target: AgentMessage, replacement: AgentMessage): void {
|
||||
// Agent-core stores the finalized message object in its state before emitting message_end.
|
||||
// SessionManager persistence happens later in _processAgentEvent() with event.message.
|
||||
// SessionManager persistence happens later in _handleAgentEvent() with event.message.
|
||||
// Mutating this object in place keeps agent state, later turn/agent events, listeners,
|
||||
// and the eventual SessionManager.appendMessage(event.message) persistence in sync.
|
||||
if (target === replacement) {
|
||||
@@ -963,6 +923,41 @@ export class AgentSession {
|
||||
// Prompting
|
||||
// =========================================================================
|
||||
|
||||
private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {
|
||||
try {
|
||||
await this.agent.prompt(messages);
|
||||
while (await this._handlePostAgentRun()) {
|
||||
await this.agent.continue();
|
||||
}
|
||||
} finally {
|
||||
this._flushPendingBashMessages();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handlePostAgentRun(): Promise<boolean> {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = undefined;
|
||||
if (!msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.stopReason === "error" && this._retryAttempt > 0) {
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt: this._retryAttempt,
|
||||
finalError: msg.errorMessage,
|
||||
});
|
||||
this._retryAttempt = 0;
|
||||
}
|
||||
|
||||
return await this._checkCompaction(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prompt to the agent.
|
||||
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
||||
@@ -1053,8 +1048,15 @@ export class AgentSession {
|
||||
|
||||
// Check if we need to compact before sending (catches aborted responses)
|
||||
const lastAssistant = this._findLastAssistantMessage();
|
||||
if (lastAssistant) {
|
||||
await this._checkCompaction(lastAssistant, false);
|
||||
if (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {
|
||||
try {
|
||||
await this.agent.continue();
|
||||
while (await this._handlePostAgentRun()) {
|
||||
await this.agent.continue();
|
||||
}
|
||||
} finally {
|
||||
this._flushPendingBashMessages();
|
||||
}
|
||||
}
|
||||
|
||||
// Build messages array (custom message if any, then user message)
|
||||
@@ -1114,8 +1116,7 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
preflightResult?.(true);
|
||||
await this.agent.prompt(messages);
|
||||
await this.waitForRetry();
|
||||
await this._runAgentPrompt(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1301,7 +1302,7 @@ export class AgentSession {
|
||||
this.agent.steer(appMessage);
|
||||
}
|
||||
} else if (options?.triggerTurn) {
|
||||
await this.agent.prompt(appMessage);
|
||||
await this._runAgentPrompt(appMessage);
|
||||
} else {
|
||||
this.agent.state.messages.push(appMessage);
|
||||
this.sessionManager.appendCustomMessageEntry(
|
||||
@@ -1626,7 +1627,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();
|
||||
@@ -1684,6 +1685,7 @@ export class AgentSession {
|
||||
customInstructions,
|
||||
this._compactionAbortController.signal,
|
||||
this.thinkingLevel,
|
||||
this.agent.streamFn,
|
||||
);
|
||||
summary = result.summary;
|
||||
firstKeptEntryId = result.firstKeptEntryId;
|
||||
@@ -1771,12 +1773,12 @@ export class AgentSession {
|
||||
* @param assistantMessage The assistant message to check
|
||||
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
||||
*/
|
||||
private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {
|
||||
private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
if (!settings.enabled) return;
|
||||
if (!settings.enabled) return false;
|
||||
|
||||
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
||||
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
||||
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;
|
||||
|
||||
const contextWindow = this.model?.contextWindow ?? 0;
|
||||
|
||||
@@ -1794,7 +1796,7 @@ export class AgentSession {
|
||||
const assistantIsFromBeforeCompaction =
|
||||
compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
|
||||
if (assistantIsFromBeforeCompaction) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 1: Overflow - LLM returned context overflow error
|
||||
@@ -1809,7 +1811,7 @@ export class AgentSession {
|
||||
errorMessage:
|
||||
"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this._overflowRecoveryAttempted = true;
|
||||
@@ -1819,8 +1821,7 @@ export class AgentSession {
|
||||
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
||||
this.agent.state.messages = messages.slice(0, -1);
|
||||
}
|
||||
await this._runAutoCompaction("overflow", true);
|
||||
return;
|
||||
return await this._runAutoCompaction("overflow", true);
|
||||
}
|
||||
|
||||
// Case 2: Threshold - context is getting large
|
||||
@@ -1830,7 +1831,7 @@ export class AgentSession {
|
||||
if (assistantMessage.stopReason === "error") {
|
||||
const messages = this.agent.state.messages;
|
||||
const estimate = estimateContextTokens(messages);
|
||||
if (estimate.lastUsageIndex === null) return; // No usage data at all
|
||||
if (estimate.lastUsageIndex === null) return false; // No usage data at all
|
||||
// Verify the usage source is post-compaction. Kept pre-compaction messages
|
||||
// have stale usage reflecting the old (larger) context and would falsely
|
||||
// trigger compaction right after one just finished.
|
||||
@@ -1840,21 +1841,22 @@ export class AgentSession {
|
||||
usageMsg.role === "assistant" &&
|
||||
(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
contextTokens = estimate.tokens;
|
||||
} else {
|
||||
contextTokens = calculateContextTokens(assistantMessage.usage);
|
||||
}
|
||||
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
||||
await this._runAutoCompaction("threshold", false);
|
||||
return await this._runAutoCompaction("threshold", false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Run auto-compaction with events.
|
||||
*/
|
||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<boolean> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
|
||||
this._emit({ type: "compaction_start", reason });
|
||||
@@ -1869,21 +1871,28 @@ export class AgentSession {
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
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 false;
|
||||
}
|
||||
apiKey = authResult.apiKey;
|
||||
headers = authResult.headers;
|
||||
} else {
|
||||
({ apiKey, headers } = await this._getCompactionRequestAuth(this.model));
|
||||
}
|
||||
const { apiKey, headers } = authResult;
|
||||
|
||||
const pathEntries = this.sessionManager.getBranch();
|
||||
|
||||
@@ -1896,7 +1905,7 @@ export class AgentSession {
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let extensionCompaction: CompactionResult | undefined;
|
||||
@@ -1919,7 +1928,7 @@ export class AgentSession {
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extensionResult?.compaction) {
|
||||
@@ -1949,6 +1958,7 @@ export class AgentSession {
|
||||
undefined,
|
||||
this._autoCompactionAbortController.signal,
|
||||
this.thinkingLevel,
|
||||
this.agent.streamFn,
|
||||
);
|
||||
summary = compactResult.summary;
|
||||
firstKeptEntryId = compactResult.firstKeptEntryId;
|
||||
@@ -1964,7 +1974,7 @@ export class AgentSession {
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
||||
@@ -1999,17 +2009,12 @@ export class AgentSession {
|
||||
if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") {
|
||||
this.agent.state.messages = messages.slice(0, -1);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {});
|
||||
}, 100);
|
||||
} else if (this.agent.hasQueuedMessages()) {
|
||||
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
||||
// Kick the loop so queued messages are actually delivered.
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {});
|
||||
}, 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
||||
// Continue once so queued messages are delivered.
|
||||
return this.agent.hasQueuedMessages();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
||||
this._emit({
|
||||
@@ -2023,6 +2028,7 @@ export class AgentSession {
|
||||
? `Context overflow recovery failed: ${errorMessage}`
|
||||
: `Auto-compaction failed: ${errorMessage}`,
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this._autoCompactionAbortController = undefined;
|
||||
}
|
||||
@@ -2047,6 +2053,9 @@ export class AgentSession {
|
||||
if (bindings.commandContextActions !== undefined) {
|
||||
this._extensionCommandContextActions = bindings.commandContextActions;
|
||||
}
|
||||
if (bindings.abortHandler !== undefined) {
|
||||
this._extensionAbortHandler = bindings.abortHandler;
|
||||
}
|
||||
if (bindings.shutdownHandler !== undefined) {
|
||||
this._extensionShutdownHandler = bindings.shutdownHandler;
|
||||
}
|
||||
@@ -2211,7 +2220,13 @@ export class AgentSession {
|
||||
getModel: () => this.model,
|
||||
isIdle: () => !this.isStreaming,
|
||||
getSignal: () => this.agent.signal,
|
||||
abort: () => this.abort(),
|
||||
abort: () => {
|
||||
if (this._extensionAbortHandler) {
|
||||
this._extensionAbortHandler();
|
||||
return;
|
||||
}
|
||||
void this.abort();
|
||||
},
|
||||
hasPendingMessages: () => this.pendingMessageCount > 0,
|
||||
shutdown: () => {
|
||||
this._extensionShutdownHandler?.();
|
||||
@@ -2427,43 +2442,27 @@ export class AgentSession {
|
||||
if (isContextOverflow(message, contextWindow)) return false;
|
||||
|
||||
const err = message.errorMessage;
|
||||
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, request ended without sending chunks, HTTP/2 closed before response, terminated, retry delay exceeded
|
||||
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(
|
||||
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded
|
||||
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle retryable errors with exponential backoff.
|
||||
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
||||
* Prepare a retryable error for continuation with exponential backoff.
|
||||
* @returns true if the caller should continue the agent, false otherwise
|
||||
*/
|
||||
private async _handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
||||
private async _prepareRetry(message: AssistantMessage): Promise<boolean> {
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled) {
|
||||
this._resolveRetry();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry promise is created synchronously in _handleAgentEvent for agent_end.
|
||||
// Keep a defensive fallback here in case a future refactor bypasses that path.
|
||||
if (!this._retryPromise) {
|
||||
this._retryPromise = new Promise((resolve) => {
|
||||
this._retryResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
this._retryAttempt++;
|
||||
|
||||
if (this._retryAttempt > settings.maxRetries) {
|
||||
// Max retries exceeded, emit final failure and reset
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt: this._retryAttempt - 1,
|
||||
finalError: message.errorMessage,
|
||||
});
|
||||
this._retryAttempt = 0;
|
||||
this._resolveRetry(); // Resolve so waitForRetry() completes
|
||||
// Preserve the completed attempt count so post-run handling can emit the final failure.
|
||||
this._retryAttempt--;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2491,24 +2490,16 @@ export class AgentSession {
|
||||
// Aborted during sleep - emit end event so UI can clean up
|
||||
const attempt = this._retryAttempt;
|
||||
this._retryAttempt = 0;
|
||||
this._retryAbortController = undefined;
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt,
|
||||
finalError: "Retry cancelled",
|
||||
});
|
||||
this._resolveRetry();
|
||||
return false;
|
||||
} finally {
|
||||
this._retryAbortController = undefined;
|
||||
}
|
||||
this._retryAbortController = undefined;
|
||||
|
||||
// Retry via continue() - use setTimeout to break out of event handler chain
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {
|
||||
// Retry failed - will be caught by next agent_end
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2518,26 +2509,11 @@ export class AgentSession {
|
||||
*/
|
||||
abortRetry(): void {
|
||||
this._retryAbortController?.abort();
|
||||
// Note: _retryAttempt is reset in the catch block of _autoRetry
|
||||
this._resolveRetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any in-progress retry to complete.
|
||||
* Returns immediately if no retry is in progress.
|
||||
*/
|
||||
private async waitForRetry(): Promise<void> {
|
||||
if (!this._retryPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._retryPromise;
|
||||
await this.agent.waitForIdle();
|
||||
}
|
||||
|
||||
/** Whether auto-retry is currently in progress */
|
||||
get isRetrying(): boolean {
|
||||
return this._retryPromise !== undefined;
|
||||
return this._retryAbortController !== undefined;
|
||||
}
|
||||
|
||||
/** Whether auto-retry is enabled */
|
||||
@@ -3026,7 +3002,10 @@ export class AgentSession {
|
||||
* @returns The resolved output file path.
|
||||
*/
|
||||
exportToJsonl(outputPath?: string): string {
|
||||
const filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
|
||||
const filePath = resolvePath(
|
||||
outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`,
|
||||
process.cwd(),
|
||||
);
|
||||
const dir = dirname(filePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user