|
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
|
|
createToolScopedAbortReason,
|
|
|
|
|
resolveTelemetry,
|
|
|
|
|
type StreamFn,
|
|
|
|
|
TERMINAL_TOOL_RESULT_ABORT_REASON,
|
|
|
|
|
ThinkingLevel,
|
|
|
|
|
type ToolChoiceDirective,
|
|
|
|
|
} from "@oh-my-pi/pi-agent-core";
|
|
|
|
@@ -84,6 +85,7 @@ import type {
|
|
|
|
|
AssistantMessageEvent,
|
|
|
|
|
AssistantRetryRecovery,
|
|
|
|
|
AssistantRetryRecoveryKind,
|
|
|
|
|
CodexCompactionContext,
|
|
|
|
|
Context,
|
|
|
|
|
ImageContent,
|
|
|
|
|
Message,
|
|
|
|
@@ -117,6 +119,7 @@ import {
|
|
|
|
|
streamSimple,
|
|
|
|
|
} from "@oh-my-pi/pi-ai";
|
|
|
|
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
|
|
|
import { resetOpenAICodexHistoryAfterCompaction } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
|
|
|
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
|
|
|
import { GeminiHeaderRunDetector, isGeminiThinkingModel } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
|
|
|
import { type RepeatedToolCallDetection, ToolCallLoopGuard } from "@oh-my-pi/pi-ai/utils/tool-call-loop-guard";
|
|
|
|
@@ -154,6 +157,7 @@ import {
|
|
|
|
|
AdvisorTranscriptRecorder,
|
|
|
|
|
advisorTranscriptFilename,
|
|
|
|
|
formatAdvisorBatchContent,
|
|
|
|
|
getOrCreateAdvisorProviderSessionId,
|
|
|
|
|
isAdvisorInterruptImmuneTurnActive,
|
|
|
|
|
isInterruptingSeverity,
|
|
|
|
|
resolveAdvisorDeliveryChannel,
|
|
|
|
@@ -273,6 +277,7 @@ import toolCallLoopRedirectTemplate from "../prompts/system/tool-call-loop-redir
|
|
|
|
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
|
|
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
|
|
|
import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
|
|
|
|
|
import vibeModeActivePrompt from "../prompts/system/vibe-mode-active.md" with { type: "text" };
|
|
|
|
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
|
|
|
import {
|
|
|
|
|
deobfuscateAssistantContent,
|
|
|
|
@@ -283,6 +288,7 @@ import {
|
|
|
|
|
stripPendingSecretPlaceholderSuffix,
|
|
|
|
|
} from "../secrets/obfuscator";
|
|
|
|
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
|
|
|
import { usesCodexTaskPrompt } from "../task/prompt-policy";
|
|
|
|
|
import {
|
|
|
|
|
AUTO_THINKING,
|
|
|
|
|
type ConfiguredThinkingLevel,
|
|
|
|
@@ -294,7 +300,7 @@ import {
|
|
|
|
|
shouldDisableReasoning,
|
|
|
|
|
toReasoningEffort,
|
|
|
|
|
} from "../thinking";
|
|
|
|
|
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/text";
|
|
|
|
|
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/message-preproc";
|
|
|
|
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
|
|
|
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
|
|
|
import {
|
|
|
|
@@ -326,6 +332,7 @@ import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallba
|
|
|
|
|
import { formatLocalCalendarDate } from "../utils/local-date";
|
|
|
|
|
import { generateSessionTitle } from "../utils/title-generator";
|
|
|
|
|
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
|
|
|
|
import type { VibeModeState } from "../vibe/state";
|
|
|
|
|
import type { AuthStorage } from "./auth-storage";
|
|
|
|
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
|
|
|
import {
|
|
|
|
@@ -603,6 +610,20 @@ function compactionDeadEndWarning(remedies: string): string {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createCodexCompactionContext(options: {
|
|
|
|
|
trigger: CodexCompactionContext["trigger"];
|
|
|
|
|
reason: CodexCompactionContext["reason"];
|
|
|
|
|
phase: CodexCompactionContext["phase"];
|
|
|
|
|
}): CodexCompactionContext {
|
|
|
|
|
return {
|
|
|
|
|
operationId: crypto.randomUUID(),
|
|
|
|
|
trigger: options.trigger,
|
|
|
|
|
reason: options.reason,
|
|
|
|
|
phase: options.phase,
|
|
|
|
|
strategy: "memento",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Per-turn prune cache window. A tool result whose all-message suffix exceeds
|
|
|
|
|
* this is in the warm, already-sent prompt-cache prefix: re-writing it costs the
|
|
|
|
@@ -694,6 +715,8 @@ export interface AgentSessionConfig {
|
|
|
|
|
modelRegistry: ModelRegistry;
|
|
|
|
|
/** Tool registry for LSP and settings */
|
|
|
|
|
toolRegistry?: Map<string, AgentTool>;
|
|
|
|
|
/** Creates the tools registered only while `/vibe` mode is active. */
|
|
|
|
|
createVibeTools?: () => AgentTool[];
|
|
|
|
|
/** Tool names whose current registry entry is still the built-in implementation. */
|
|
|
|
|
builtInToolNames?: Iterable<string>;
|
|
|
|
|
/** Update tool-session predicates that render guidance from the live active tool set. */
|
|
|
|
@@ -794,6 +817,8 @@ export interface AgentSessionConfig {
|
|
|
|
|
* so that credential sticky selection is consistent with the session's streaming calls.
|
|
|
|
|
*/
|
|
|
|
|
providerSessionId?: string;
|
|
|
|
|
/** Marks `agent.promptCacheKey` as fork-inherited so incompatible route changes can clear it. */
|
|
|
|
|
providerPromptCacheKeySource?: "explicit" | "fork";
|
|
|
|
|
/**
|
|
|
|
|
* Full advisor toolset, pre-built in `createAgentSession` against a distinct,
|
|
|
|
|
* advisor-scoped `ToolSession` (its own `-advisor` session/agent id) so the
|
|
|
|
@@ -1062,7 +1087,7 @@ function parseRetryFallbackSelector(
|
|
|
|
|
const trimmed = selector.trim();
|
|
|
|
|
if (!trimmed) return undefined;
|
|
|
|
|
const parsed = parseModelString(trimmed, {
|
|
|
|
|
allowMaxAlias: true,
|
|
|
|
|
allowMaxSuffix: true,
|
|
|
|
|
allowAutoAlias: true,
|
|
|
|
|
isLiteralModelId: (provider, id) => modelLookup?.find(provider, id) !== undefined,
|
|
|
|
|
});
|
|
|
|
@@ -1181,6 +1206,7 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
|
|
pasteToEditor: () => {},
|
|
|
|
|
getEditorText: () => "",
|
|
|
|
|
editor: async () => undefined,
|
|
|
|
|
addAutocompleteProvider: () => {},
|
|
|
|
|
get theme() {
|
|
|
|
|
return theme;
|
|
|
|
|
},
|
|
|
|
@@ -1587,6 +1613,7 @@ export class AgentSession {
|
|
|
|
|
#advisorPrimaryTurnsCompleted = 0;
|
|
|
|
|
#advisorInterruptImmuneTurnStart: number | undefined;
|
|
|
|
|
#planModeState: PlanModeState | undefined;
|
|
|
|
|
#vibeModeState: VibeModeState | undefined;
|
|
|
|
|
#goalModeState: GoalModeState | undefined;
|
|
|
|
|
#goalRuntime: GoalRuntime;
|
|
|
|
|
#advisorEnabled = false;
|
|
|
|
@@ -1599,6 +1626,8 @@ export class AgentSession {
|
|
|
|
|
#advisors: ActiveAdvisor[] = [];
|
|
|
|
|
/** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
|
|
|
|
|
#advisorConfigs?: AdvisorConfig[];
|
|
|
|
|
/** Provider-facing UUIDv7 identities keyed by primary provider session and advisor slug. */
|
|
|
|
|
#advisorProviderSessionIds = new Map<string, string>();
|
|
|
|
|
/** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
|
|
|
|
|
* used as the open barrier for the next build so two writers never share a file. */
|
|
|
|
|
#advisorRecorderClosed: Promise<void> = Promise.resolve();
|
|
|
|
@@ -1697,6 +1726,7 @@ export class AgentSession {
|
|
|
|
|
#agentKind: "main" | "sub" = "main";
|
|
|
|
|
#providerSessionId: string | undefined;
|
|
|
|
|
#freshProviderSessionId: string | undefined;
|
|
|
|
|
#inheritedProviderPromptCacheKey: string | undefined;
|
|
|
|
|
#isDisposed = false;
|
|
|
|
|
// Extension system
|
|
|
|
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
|
|
@@ -1720,6 +1750,8 @@ export class AgentSession {
|
|
|
|
|
|
|
|
|
|
// Tool registry and prompt builder for extensions
|
|
|
|
|
#toolRegistry: Map<string, AgentTool>;
|
|
|
|
|
#createVibeTools: (() => AgentTool[]) | undefined;
|
|
|
|
|
#installedVibeToolNames = new Set<string>();
|
|
|
|
|
#transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
|
|
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
|
|
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
|
|
@@ -1846,6 +1878,7 @@ export class AgentSession {
|
|
|
|
|
* Cleared before every new prompt turn so the next turn evaluates cleanly.
|
|
|
|
|
*/
|
|
|
|
|
#yieldTerminationPending = false;
|
|
|
|
|
#synchronouslyTerminatedYieldToolCallIds = new Set<string>();
|
|
|
|
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
|
|
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
|
|
|
readonly rawSseDebugBuffer: RawSseDebugBuffer;
|
|
|
|
@@ -2082,6 +2115,7 @@ export class AgentSession {
|
|
|
|
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
|
|
|
this.#validateRetryFallbackChains();
|
|
|
|
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
|
|
|
this.#createVibeTools = config.createVibeTools;
|
|
|
|
|
this.#builtInToolNames = new Set(config.builtInToolNames ?? []);
|
|
|
|
|
this.#requestedToolNames = config.requestedToolNames;
|
|
|
|
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
|
|
@@ -2212,6 +2246,8 @@ export class AgentSession {
|
|
|
|
|
this.#agentId = config.agentId;
|
|
|
|
|
this.#agentKind = config.agentKind ?? "main";
|
|
|
|
|
this.#providerSessionId = config.providerSessionId;
|
|
|
|
|
this.#inheritedProviderPromptCacheKey =
|
|
|
|
|
config.providerPromptCacheKeySource === "fork" ? this.agent.promptCacheKey : undefined;
|
|
|
|
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
|
|
|
const event: AgentEvent = {
|
|
|
|
|
type: "message_update",
|
|
|
|
@@ -2222,8 +2258,8 @@ export class AgentSession {
|
|
|
|
|
this.#maybeAbortStreamingEdit(event);
|
|
|
|
|
this.#maybeInterruptGeminiHeaderRunaway(message, assistantMessageEvent);
|
|
|
|
|
});
|
|
|
|
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
|
|
|
this.agent.afterToolCall = ctx => this.#ttsrAfterToolCall(ctx);
|
|
|
|
|
// Tool-result hook owns synchronous post-tool actions that must affect the current loop.
|
|
|
|
|
this.agent.afterToolCall = ctx => this.#afterToolCall(ctx);
|
|
|
|
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
|
|
|
this.#syncAgentSessionId();
|
|
|
|
|
this.#syncTodoPhasesFromBranch();
|
|
|
|
@@ -2474,21 +2510,29 @@ export class AgentSession {
|
|
|
|
|
if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
|
|
|
|
|
if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
|
|
|
|
|
|
|
|
|
|
const names = config.tools?.length ? new Set(config.tools) : ADVISOR_DEFAULT_TOOL_NAMES;
|
|
|
|
|
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
|
|
|
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
|
|
|
|
|
|
|
|
const advisorSessionId = this.#advisorSessionId(slug);
|
|
|
|
|
const primaryProviderSessionId = this.sessionId;
|
|
|
|
|
const advisorSessionLabel = slug
|
|
|
|
|
? `${primaryProviderSessionId}-advisor-${slug}`
|
|
|
|
|
: `${primaryProviderSessionId}-advisor`;
|
|
|
|
|
const advisorProviderSessionId = getOrCreateAdvisorProviderSessionId(
|
|
|
|
|
this.#advisorProviderSessionIds,
|
|
|
|
|
primaryProviderSessionId,
|
|
|
|
|
slug,
|
|
|
|
|
);
|
|
|
|
|
const appendOnlyContext = new AppendOnlyContextManager();
|
|
|
|
|
|
|
|
|
|
// Thread the primary's telemetry into the advisor loop so the advisor
|
|
|
|
|
// model's GenAI spans + usage/cost hooks fire stamped with the advisor's
|
|
|
|
|
// own identity. `conversationId` is cleared so the advisor loop falls back
|
|
|
|
|
// to its own session id; undefined telemetry stays undefined.
|
|
|
|
|
// model's GenAI spans + usage/cost hooks fire stamped with the local advisor
|
|
|
|
|
// identity. `conversationId` is cleared so provider telemetry falls back to
|
|
|
|
|
// the UUIDv7 provider session id, not the local `-advisor` label.
|
|
|
|
|
const advisorTelemetry = this.agent.telemetry
|
|
|
|
|
? {
|
|
|
|
|
...this.agent.telemetry,
|
|
|
|
|
agent: {
|
|
|
|
|
id: advisorSessionId,
|
|
|
|
|
id: advisorSessionLabel,
|
|
|
|
|
name: slug ? `${MODEL_ROLES.advisor.name}: ${advisorName}` : MODEL_ROLES.advisor.name,
|
|
|
|
|
description: formatModelString(advisorModel),
|
|
|
|
|
},
|
|
|
|
@@ -2500,10 +2544,10 @@ export class AgentSession {
|
|
|
|
|
// advisor's requests cache, route, and obfuscate like the main turn.
|
|
|
|
|
// `promptCacheKey` preserves an explicitly pinned provider cache key
|
|
|
|
|
// unchanged so tan/shared-session advisor calls read the exact shard the
|
|
|
|
|
// parent turn populated, while keeping only `sessionId` advisor-scoped;
|
|
|
|
|
// sessions without a pinned key fall back to the advisor session id for
|
|
|
|
|
// stable advisor-local caching (see can1357/oh-my-pi#3639).
|
|
|
|
|
const advisorPromptCacheKey = this.agent.promptCacheKey ?? advisorSessionId;
|
|
|
|
|
// parent turn populated. Otherwise the advisor uses its provider UUIDv7 so
|
|
|
|
|
// Codex request identity remains UUID-shaped while local labels keep the
|
|
|
|
|
// `-advisor` suffix.
|
|
|
|
|
const advisorPromptCacheKey = this.agent.promptCacheKey ?? advisorProviderSessionId;
|
|
|
|
|
const advisorAgent = new Agent({
|
|
|
|
|
initialState: {
|
|
|
|
|
systemPrompt,
|
|
|
|
@@ -2512,11 +2556,11 @@ export class AgentSession {
|
|
|
|
|
tools: [adviseTool, ...tools],
|
|
|
|
|
},
|
|
|
|
|
appendOnlyContext,
|
|
|
|
|
sessionId: advisorSessionId,
|
|
|
|
|
sessionId: advisorProviderSessionId,
|
|
|
|
|
promptCacheKey: advisorPromptCacheKey,
|
|
|
|
|
providerSessionState: this.#providerSessionState,
|
|
|
|
|
preferWebsockets: this.#preferWebsockets,
|
|
|
|
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
|
|
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorProviderSessionId),
|
|
|
|
|
streamFn: this.#advisorStreamFn,
|
|
|
|
|
onPayload: this.#onPayload,
|
|
|
|
|
onResponse: this.#onResponse,
|
|
|
|
@@ -2575,11 +2619,15 @@ export class AgentSession {
|
|
|
|
|
// suspect-mark a credential on a transient advisor error).
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
|
|
if (!isUsageLimitOutcome(extractHttpStatusFromError(error), message)) return;
|
|
|
|
|
await this.#modelRegistry.authStorage.markUsageLimitReached(advisorModel.provider, advisorSessionId, {
|
|
|
|
|
retryAfterMs: extractRetryHint(undefined, message),
|
|
|
|
|
baseUrl: advisorModel.baseUrl,
|
|
|
|
|
modelId: advisorModel.id,
|
|
|
|
|
});
|
|
|
|
|
await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
|
|
|
advisorModel.provider,
|
|
|
|
|
advisorProviderSessionId,
|
|
|
|
|
{
|
|
|
|
|
retryAfterMs: extractRetryHint(undefined, message),
|
|
|
|
|
baseUrl: advisorModel.baseUrl,
|
|
|
|
|
modelId: advisorModel.id,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
notifyFailure: error => {
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
|
@@ -2633,13 +2681,6 @@ export class AgentSession {
|
|
|
|
|
return this.#advisors.length > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Provider/session id for an advisor's loop. The slug suffix MUST match the
|
|
|
|
|
* advisor's transcript filename so stats/telemetry attribute the same advisor. */
|
|
|
|
|
#advisorSessionId(slug: string): string | undefined {
|
|
|
|
|
if (!this.sessionId) return undefined;
|
|
|
|
|
return slug ? `${this.sessionId}-advisor-${slug}` : `${this.sessionId}-advisor`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
|
|
|
* blocker interrupt the running agent through the steering channel; once the
|
|
|
|
@@ -2845,11 +2886,15 @@ export class AgentSession {
|
|
|
|
|
// No compaction candidates, fallback to re-prime
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const advisorSessionId = this.#advisorSessionId(advisor.slug);
|
|
|
|
|
const advisorProviderSessionId = getOrCreateAdvisorProviderSessionId(
|
|
|
|
|
this.#advisorProviderSessionIds,
|
|
|
|
|
this.sessionId,
|
|
|
|
|
advisor.slug,
|
|
|
|
|
);
|
|
|
|
|
const preparation = prepareCompaction(
|
|
|
|
|
pathEntries,
|
|
|
|
|
compactionSettings,
|
|
|
|
|
await this.#runnableCompactionCandidates(candidates, advisorSessionId),
|
|
|
|
|
await this.#runnableCompactionCandidates(candidates, advisorProviderSessionId),
|
|
|
|
|
);
|
|
|
|
|
if (!preparation) {
|
|
|
|
|
// Cannot prepare compaction, fallback to re-prime
|
|
|
|
@@ -2869,17 +2914,23 @@ export class AgentSession {
|
|
|
|
|
let lastError: unknown;
|
|
|
|
|
// Instrument the advisor's overflow-compaction one-shot like the primary
|
|
|
|
|
// compaction path so the advisor model's maintenance call also emits spans.
|
|
|
|
|
const telemetry = resolveTelemetry(agent.telemetry, advisorSessionId);
|
|
|
|
|
const telemetry = resolveTelemetry(agent.telemetry, advisorProviderSessionId);
|
|
|
|
|
|
|
|
|
|
const codexCompaction = createCodexCompactionContext({
|
|
|
|
|
trigger: "auto",
|
|
|
|
|
reason: "context_limit",
|
|
|
|
|
phase: "pre_turn",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const candidate of candidates) {
|
|
|
|
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorSessionId);
|
|
|
|
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorProviderSessionId);
|
|
|
|
|
if (!apiKey) continue;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
compactResult = await compact(
|
|
|
|
|
preparation,
|
|
|
|
|
candidate,
|
|
|
|
|
this.#modelRegistry.resolver(candidate, advisorSessionId),
|
|
|
|
|
this.#modelRegistry.resolver(candidate, advisorProviderSessionId),
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
{
|
|
|
|
@@ -2887,8 +2938,10 @@ export class AgentSession {
|
|
|
|
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
|
|
|
telemetry,
|
|
|
|
|
tools: agent.state.tools,
|
|
|
|
|
sessionId: advisorSessionId,
|
|
|
|
|
promptCacheKey: advisorSessionId,
|
|
|
|
|
sessionId: advisorProviderSessionId,
|
|
|
|
|
promptCacheKey: advisorProviderSessionId,
|
|
|
|
|
providerSessionState: this.#providerSessionState,
|
|
|
|
|
codexCompaction,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
@@ -3671,9 +3724,12 @@ export class AgentSession {
|
|
|
|
|
this.#planModeReminderAwaitingProgress = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
|
|
|
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
|
|
|
this.#yieldTerminationPending = true;
|
|
|
|
|
if (event.type === "tool_execution_end" && this.#isTerminalYieldToolResult(event)) {
|
|
|
|
|
const alreadyTerminated = this.#synchronouslyTerminatedYieldToolCallIds.delete(event.toolCallId);
|
|
|
|
|
if (!alreadyTerminated) {
|
|
|
|
|
this.#markTerminalYieldToolCall(event.toolCallId);
|
|
|
|
|
this.agent.abort(TERMINAL_TOOL_RESULT_ABORT_REASON);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TTSR: Check for pattern matches on assistant text/thinking and tool argument deltas
|
|
|
|
@@ -4418,6 +4474,21 @@ export class AgentSession {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#afterToolCall(ctx: AfterToolCallContext): AfterToolCallResult | undefined {
|
|
|
|
|
if (
|
|
|
|
|
this.#isTerminalYieldToolResult({
|
|
|
|
|
toolName: ctx.toolCall.name,
|
|
|
|
|
isError: ctx.isError,
|
|
|
|
|
result: ctx.result,
|
|
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
this.#markTerminalYieldToolCall(ctx.toolCall.id);
|
|
|
|
|
this.#synchronouslyTerminatedYieldToolCallIds.add(ctx.toolCall.id);
|
|
|
|
|
this.agent.abort(TERMINAL_TOOL_RESULT_ABORT_REASON);
|
|
|
|
|
}
|
|
|
|
|
return this.#ttsrAfterToolCall(ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** `afterToolCall` hook: fold any per-tool TTSR reminders into the result. */
|
|
|
|
|
#ttsrAfterToolCall(ctx: AfterToolCallContext): AfterToolCallResult | undefined {
|
|
|
|
|
const rules = this.#perToolTtsrInjections.get(ctx.toolCall.id);
|
|
|
|
@@ -5570,6 +5641,23 @@ export class AgentSession {
|
|
|
|
|
return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#adoptInheritedProviderPromptCacheKey(): void {
|
|
|
|
|
const key = this.sessionManager.getHeader()?.providerPromptCacheKey;
|
|
|
|
|
if (!key) return;
|
|
|
|
|
if (this.#inheritedProviderPromptCacheKey !== undefined || this.agent.promptCacheKey === undefined) {
|
|
|
|
|
this.agent.promptCacheKey = key;
|
|
|
|
|
this.#inheritedProviderPromptCacheKey = key;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#clearInheritedProviderPromptCacheKey(): void {
|
|
|
|
|
const key = this.#inheritedProviderPromptCacheKey;
|
|
|
|
|
this.#inheritedProviderPromptCacheKey = undefined;
|
|
|
|
|
if (key !== undefined && this.agent.promptCacheKey === key) {
|
|
|
|
|
this.agent.promptCacheKey = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set agent.sessionId from the session manager and install a dynamic
|
|
|
|
|
* metadata resolver so every Anthropic API request carries
|
|
|
|
@@ -6034,6 +6122,49 @@ export class AgentSession {
|
|
|
|
|
return Array.from(this.#toolRegistry.keys());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#wrapRuntimeTool(tool: AgentTool): AgentTool {
|
|
|
|
|
const wrapped = wrapToolWithMetaNotice(tool);
|
|
|
|
|
return this.#extensionRunner ? new ExtensionToolWrapper(wrapped, this.#extensionRunner) : wrapped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Registers the ephemeral vibe tools and activates them alongside `baseToolNames`.
|
|
|
|
|
*
|
|
|
|
|
* @throws When this session cannot create vibe tools or the factory returns duplicate names.
|
|
|
|
|
*/
|
|
|
|
|
async activateVibeTools(baseToolNames: string[]): Promise<void> {
|
|
|
|
|
const createVibeTools = this.#createVibeTools;
|
|
|
|
|
if (!createVibeTools) {
|
|
|
|
|
throw new Error("Vibe tools are unavailable in this session.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tools = createVibeTools();
|
|
|
|
|
const vibeToolNames = tools.map(tool => tool.name);
|
|
|
|
|
if (new Set(vibeToolNames).size !== vibeToolNames.length) {
|
|
|
|
|
throw new Error("Vibe tool names must be unique.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const tool of tools) {
|
|
|
|
|
if (this.#toolRegistry.has(tool.name)) continue;
|
|
|
|
|
this.#toolRegistry.set(tool.name, this.#wrapRuntimeTool(tool));
|
|
|
|
|
this.#builtInToolNames.add(tool.name);
|
|
|
|
|
this.#installedVibeToolNames.add(tool.name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.#applyActiveToolsByName([...new Set([...baseToolNames, ...vibeToolNames])]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Removes tools installed by {@link activateVibeTools} and activates `nextToolNames`. */
|
|
|
|
|
async deactivateVibeTools(nextToolNames: string[]): Promise<void> {
|
|
|
|
|
for (const name of this.#installedVibeToolNames) {
|
|
|
|
|
this.#toolRegistry.delete(name);
|
|
|
|
|
this.#builtInToolNames.delete(name);
|
|
|
|
|
this.#selectedDiscoveredToolNames.delete(name);
|
|
|
|
|
}
|
|
|
|
|
this.#installedVibeToolNames.clear();
|
|
|
|
|
await this.#applyActiveToolsByName(nextToolNames);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#getEditModeSession() {
|
|
|
|
|
return {
|
|
|
|
|
settings: this.settings,
|
|
|
|
@@ -6045,19 +6176,17 @@ export class AgentSession {
|
|
|
|
|
return resolveEditMode(this.#getEditModeSession());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Model key (`provider/id`) currently surfaced in the system prompt, or
|
|
|
|
|
* undefined when the model is unset or `includeModelInPrompt` is disabled.
|
|
|
|
|
*/
|
|
|
|
|
/** Cache key for model-dependent prompt content: displayed id or hidden-policy cohort. */
|
|
|
|
|
#currentPromptModelKey(): string | undefined {
|
|
|
|
|
if (!this.settings.get("includeModelInPrompt")) return undefined;
|
|
|
|
|
return this.model ? formatModelString(this.model) : undefined;
|
|
|
|
|
const model = this.model ? formatModelString(this.model) : undefined;
|
|
|
|
|
if (!model || this.settings.get("includeModelInPrompt")) return model;
|
|
|
|
|
return usesCodexTaskPrompt(model) ? "task-policy:gpt-5.6" : "task-policy:default";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async #syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
|
|
|
const currentEditMode = this.#resolveActiveEditMode();
|
|
|
|
|
const editModeChanged = previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit");
|
|
|
|
|
// The system prompt may surface the active model; a switch makes the cached prompt stale.
|
|
|
|
|
// The system prompt selects model-specific policy even when it does not display the model id.
|
|
|
|
|
const modelChanged = this.#currentPromptModelKey() !== this.#promptModelKey;
|
|
|
|
|
if (editModeChanged || modelChanged) {
|
|
|
|
|
await this.refreshBaseSystemPrompt();
|
|
|
|
@@ -6375,6 +6504,9 @@ export class AgentSession {
|
|
|
|
|
if (this.#rebuildSystemPrompt) {
|
|
|
|
|
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
|
|
|
if (signature !== this.#lastAppliedToolSignature) {
|
|
|
|
|
if (this.#lastAppliedToolSignature !== undefined) {
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
}
|
|
|
|
|
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
|
|
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
|
|
|
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
|
|
@@ -6461,9 +6593,16 @@ export class AgentSession {
|
|
|
|
|
if (!this.#rebuildSystemPrompt) return;
|
|
|
|
|
const activeToolNames = this.getActiveToolNames();
|
|
|
|
|
this.#setActiveToolNames?.(activeToolNames);
|
|
|
|
|
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
|
|
|
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
|
|
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
|
|
|
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
|
|
|
if (
|
|
|
|
|
previousBaseSystemPrompt.length !== this.#baseSystemPrompt.length ||
|
|
|
|
|
previousBaseSystemPrompt.some((part, index) => part !== this.#baseSystemPrompt[index])
|
|
|
|
|
) {
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
}
|
|
|
|
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
|
|
|
this.#promptModelKey = this.#currentPromptModelKey();
|
|
|
|
|
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
|
|
|
@@ -6593,8 +6732,8 @@ export class AgentSession {
|
|
|
|
|
*
|
|
|
|
|
* @param mcpTools The new MCP tools to register.
|
|
|
|
|
* @param options.activateAll When true, force-activates every newly registered MCP tool
|
|
|
|
|
* regardless of prior selection state. Used when an ACP client provisions MCP servers
|
|
|
|
|
* for a session where MCP discovery is disabled.
|
|
|
|
|
* regardless of prior selection state. Used when MCP discovery is disabled and tools
|
|
|
|
|
* arrive after initial session activation.
|
|
|
|
|
*/
|
|
|
|
|
async refreshMCPTools(mcpTools: CustomTool[], options?: { activateAll?: boolean }): Promise<void> {
|
|
|
|
|
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
|
|
@@ -6639,10 +6778,10 @@ export class AgentSession {
|
|
|
|
|
|
|
|
|
|
if (options?.activateAll) {
|
|
|
|
|
// Force-activate every newly registered MCP tool. This path is used
|
|
|
|
|
// when an ACP client provisions MCP servers for a session where MCP
|
|
|
|
|
// discovery is disabled — without it, getSelectedMCPToolNames()
|
|
|
|
|
// returns only already-active tools (circular deadlock: tools can
|
|
|
|
|
// only become active if they're already active).
|
|
|
|
|
// when MCP discovery is disabled and tools arrive after initial
|
|
|
|
|
// activation — without it, getSelectedMCPToolNames() returns only
|
|
|
|
|
// already-active tools (circular deadlock: tools can only become
|
|
|
|
|
// active if they're already active).
|
|
|
|
|
const newMcpNames = mcpTools.map(t => t.name);
|
|
|
|
|
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...newMcpNames])];
|
|
|
|
|
await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
|
|
|
|
@@ -6986,6 +7125,14 @@ export class AgentSession {
|
|
|
|
|
this.#goalModeState = state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getVibeModeState(): VibeModeState | undefined {
|
|
|
|
|
return this.#vibeModeState;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setVibeModeState(state: VibeModeState | undefined): void {
|
|
|
|
|
this.#vibeModeState = state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get goalRuntime(): GoalRuntime {
|
|
|
|
|
return this.#goalRuntime;
|
|
|
|
|
}
|
|
|
|
@@ -7118,6 +7265,21 @@ export class AgentSession {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendVibeModeContext(options?: { deliverAs?: "steer" | "followUp" | "nextTurn" }): Promise<void> {
|
|
|
|
|
const message = this.#buildVibeModeMessage();
|
|
|
|
|
if (!message) return;
|
|
|
|
|
await this.sendCustomMessage(
|
|
|
|
|
{
|
|
|
|
|
customType: message.customType,
|
|
|
|
|
content: message.content,
|
|
|
|
|
display: message.display,
|
|
|
|
|
details: message.details,
|
|
|
|
|
attribution: message.attribution,
|
|
|
|
|
},
|
|
|
|
|
options ? { deliverAs: options.deliverAs } : undefined,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolveRoleModel(role: string): Model | undefined {
|
|
|
|
|
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model).model;
|
|
|
|
|
}
|
|
|
|
@@ -7246,6 +7408,18 @@ export class AgentSession {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#buildVibeModeMessage(): CustomMessage | null {
|
|
|
|
|
if (!this.#vibeModeState?.enabled) return null;
|
|
|
|
|
return {
|
|
|
|
|
role: "custom",
|
|
|
|
|
customType: "vibe-mode-context",
|
|
|
|
|
content: prompt.render(vibeModeActivePrompt),
|
|
|
|
|
display: false,
|
|
|
|
|
attribution: "agent",
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#sanitizeGoalTodoText(text: string): string {
|
|
|
|
|
return escapeXmlText(text)
|
|
|
|
|
.replace(/\r\n/g, "\\n")
|
|
|
|
@@ -7683,6 +7857,10 @@ export class AgentSession {
|
|
|
|
|
if (goalModeMessage) {
|
|
|
|
|
messages.push(goalModeMessage);
|
|
|
|
|
}
|
|
|
|
|
const vibeModeMessage = this.#buildVibeModeMessage();
|
|
|
|
|
if (vibeModeMessage) {
|
|
|
|
|
messages.push(vibeModeMessage);
|
|
|
|
|
}
|
|
|
|
|
if (options?.prependMessages) {
|
|
|
|
|
messages.push(...options.prependMessages);
|
|
|
|
|
}
|
|
|
|
@@ -8312,11 +8490,10 @@ export class AgentSession {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send a user message to the agent.
|
|
|
|
|
* When deliverAs is set, queue the message instead of starting a new turn.
|
|
|
|
|
* Send a user message through the prompt flow.
|
|
|
|
|
*
|
|
|
|
|
* @param content User message content (string or content array)
|
|
|
|
|
* @param options.deliverAs Delivery mode: "steer" or "followUp"
|
|
|
|
|
* Omitted `deliverAs` starts a turn when idle and queues as a steer while streaming.
|
|
|
|
|
* Explicit `deliverAs` queues without starting a turn in either state.
|
|
|
|
|
*/
|
|
|
|
|
async sendUserMessage(
|
|
|
|
|
content: string | (TextContent | ImageContent)[],
|
|
|
|
@@ -8351,10 +8528,13 @@ export class AgentSession {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
|
|
|
|
|
// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion.
|
|
|
|
|
// `streamingBehavior: "steer"` preserves prompt-flow side effects during streaming while
|
|
|
|
|
// covering the narrow race where a stream starts before prompt() acquires the turn.
|
|
|
|
|
await this.prompt(text, {
|
|
|
|
|
expandPromptTemplates: false,
|
|
|
|
|
images,
|
|
|
|
|
streamingBehavior: "steer",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -8704,6 +8884,7 @@ export class AgentSession {
|
|
|
|
|
this.#clearCheckpointRuntimeState();
|
|
|
|
|
this.setTodoPhases([]);
|
|
|
|
|
this.#freshProviderSessionId = undefined;
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
this.#syncAgentSessionId();
|
|
|
|
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
|
|
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
|
|
@@ -8804,6 +8985,7 @@ export class AgentSession {
|
|
|
|
|
|
|
|
|
|
// Update agent session ID
|
|
|
|
|
this.#freshProviderSessionId = undefined;
|
|
|
|
|
this.#adoptInheritedProviderPromptCacheKey();
|
|
|
|
|
this.#syncAgentSessionId();
|
|
|
|
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
|
|
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
|
|
@@ -9125,6 +9307,9 @@ export class AgentSession {
|
|
|
|
|
this.#autoThinking = true;
|
|
|
|
|
this.#autoResolvedLevel = undefined;
|
|
|
|
|
this.#thinkingLevel = provisional;
|
|
|
|
|
if (!wasAuto) {
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
}
|
|
|
|
|
this.#applyThinkingLevelToAgent(provisional);
|
|
|
|
|
if (persist) {
|
|
|
|
|
this.settings.set("defaultThinkingLevel", AUTO_THINKING);
|
|
|
|
@@ -9148,6 +9333,7 @@ export class AgentSession {
|
|
|
|
|
this.#applyThinkingLevelToAgent(effectiveLevel);
|
|
|
|
|
|
|
|
|
|
if (isChanging) {
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel, effectiveLevel);
|
|
|
|
|
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
|
|
|
|
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
|
|
@@ -9166,7 +9352,7 @@ export class AgentSession {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cycle to next thinking level: off → auto → minimal..xhigh → off.
|
|
|
|
|
* Cycle to next thinking level: off → auto → minimal..max → off.
|
|
|
|
|
* @returns New selector, or undefined if model doesn't support thinking
|
|
|
|
|
*/
|
|
|
|
|
cycleThinkingLevel(): ConfiguredThinkingLevel | undefined {
|
|
|
|
@@ -9208,8 +9394,9 @@ export class AgentSession {
|
|
|
|
|
let resolved: Effort | undefined;
|
|
|
|
|
if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
|
|
|
|
|
// The user explicitly asked for maximum thinking; bypass the classifier
|
|
|
|
|
// and jump straight to the highest auto-supported level for this model.
|
|
|
|
|
resolved = clampAutoThinkingEffort(model, Effort.XHigh);
|
|
|
|
|
// (and its xhigh auto ceiling) and jump straight to the highest
|
|
|
|
|
// supported level for this model.
|
|
|
|
|
resolved = clampAutoThinkingEffort(model, Effort.Max);
|
|
|
|
|
} else {
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timer = setTimeout(() => controller.abort(), AgentSession.#AUTO_THINKING_TIMEOUT_MS);
|
|
|
|
@@ -9751,6 +9938,7 @@ export class AgentSession {
|
|
|
|
|
let firstKeptEntryId: string;
|
|
|
|
|
let tokensBefore: number;
|
|
|
|
|
let details: unknown;
|
|
|
|
|
let codexCompaction: CodexCompactionContext | undefined;
|
|
|
|
|
|
|
|
|
|
// Snapcompact runs locally first. The frame cap is sized from the live
|
|
|
|
|
// model window via #computeSnapcompactMaxFrames so the post-render context
|
|
|
|
@@ -9830,6 +10018,11 @@ export class AgentSession {
|
|
|
|
|
details = snapcompactResult.details;
|
|
|
|
|
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
|
|
|
} else {
|
|
|
|
|
codexCompaction = createCodexCompactionContext({
|
|
|
|
|
trigger: "manual",
|
|
|
|
|
reason: "user_requested",
|
|
|
|
|
phase: "standalone_turn",
|
|
|
|
|
});
|
|
|
|
|
// Generate compaction result. Only convert known abort-shaped
|
|
|
|
|
// rejections (AbortError raised while the abort signal is set,
|
|
|
|
|
// or an already-typed sentinel) into `CompactionCancelledError`
|
|
|
|
@@ -9851,6 +10044,7 @@ export class AgentSession {
|
|
|
|
|
extraContext: compactionPrep.hookContext,
|
|
|
|
|
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
|
|
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
|
|
|
codexCompaction,
|
|
|
|
|
},
|
|
|
|
|
compactionCandidates,
|
|
|
|
|
);
|
|
|
|
@@ -9893,7 +10087,11 @@ export class AgentSession {
|
|
|
|
|
this.#planReferenceSent = false;
|
|
|
|
|
this.#resetAllAdvisorRuntimes();
|
|
|
|
|
this.#syncTodoPhasesFromBranch();
|
|
|
|
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
|
|
|
if (codexCompaction) {
|
|
|
|
|
this.#resetCodexProviderAfterCompaction(codexCompaction);
|
|
|
|
|
} else {
|
|
|
|
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the saved compaction entry for the hook
|
|
|
|
|
const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
|
|
|
|
@@ -10259,6 +10457,7 @@ export class AgentSession {
|
|
|
|
|
await this.#runAutoCompaction("threshold", false, false, false, {
|
|
|
|
|
autoContinue: false,
|
|
|
|
|
triggerContextTokens: contextTokens,
|
|
|
|
|
phase: "pre_turn",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -10333,6 +10532,7 @@ export class AgentSession {
|
|
|
|
|
suppressContinuation: true,
|
|
|
|
|
suppressHandoff: true,
|
|
|
|
|
triggerContextTokens: contextTokens,
|
|
|
|
|
phase: "mid_turn",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (signal?.aborted) return;
|
|
|
|
@@ -10579,6 +10779,7 @@ export class AgentSession {
|
|
|
|
|
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
|
|
|
autoContinue,
|
|
|
|
|
triggerContextTokens: postMaintenanceContextTokens,
|
|
|
|
|
phase: "pre_turn",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
|
|
|
|
@@ -10589,6 +10790,24 @@ export class AgentSession {
|
|
|
|
|
}
|
|
|
|
|
return COMPACTION_CHECK_NONE;
|
|
|
|
|
}
|
|
|
|
|
#isTerminalYieldToolResult(event: { toolName: string; isError?: boolean; result?: { details?: unknown } }): boolean {
|
|
|
|
|
if (event.toolName !== "yield" || event.isError) return false;
|
|
|
|
|
const details = event.result?.details;
|
|
|
|
|
if (!details || typeof details !== "object") return true;
|
|
|
|
|
const record = details as Record<string, unknown>;
|
|
|
|
|
return !(
|
|
|
|
|
record.status === "success" &&
|
|
|
|
|
Array.isArray(record.type) &&
|
|
|
|
|
record.type.length > 0 &&
|
|
|
|
|
record.type.every(item => typeof item === "string")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#markTerminalYieldToolCall(toolCallId: string): void {
|
|
|
|
|
this.#lastSuccessfulYieldToolCallId = toolCallId;
|
|
|
|
|
this.#yieldTerminationPending = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#assistantMessageHasSuccessfulYieldToolCall(assistantMessage: AssistantMessage, toolCallId: string): boolean {
|
|
|
|
|
const lastToolCall = assistantMessage.content
|
|
|
|
|
.slice()
|
|
|
|
@@ -10748,21 +10967,21 @@ export class AgentSession {
|
|
|
|
|
|
|
|
|
|
this.#emptyStopRetryCount++;
|
|
|
|
|
if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
|
|
|
|
|
logger.warn("Assistant returned empty stop after retry cap", {
|
|
|
|
|
attempts: this.#emptyStopRetryCount - 1,
|
|
|
|
|
const attempts = this.#emptyStopRetryCount - 1;
|
|
|
|
|
const finalError = "Assistant returned empty stop after retry cap";
|
|
|
|
|
logger.warn(finalError, {
|
|
|
|
|
attempts,
|
|
|
|
|
model: assistantMessage.model,
|
|
|
|
|
provider: assistantMessage.provider,
|
|
|
|
|
});
|
|
|
|
|
if (this.#retryAttempt > 0) {
|
|
|
|
|
await this.#emitSessionEvent({
|
|
|
|
|
type: "auto_retry_end",
|
|
|
|
|
success: false,
|
|
|
|
|
attempt: this.#retryAttempt,
|
|
|
|
|
finalError: "Assistant returned empty stop after retry cap",
|
|
|
|
|
});
|
|
|
|
|
this.#clearPendingRecoveredRetryErrors();
|
|
|
|
|
this.#retryAttempt = 0;
|
|
|
|
|
}
|
|
|
|
|
await this.#emitSessionEvent({
|
|
|
|
|
type: "auto_retry_end",
|
|
|
|
|
success: false,
|
|
|
|
|
attempt: this.#retryAttempt > 0 ? this.#retryAttempt : attempts,
|
|
|
|
|
finalError,
|
|
|
|
|
});
|
|
|
|
|
this.#clearPendingRecoveredRetryErrors();
|
|
|
|
|
this.#retryAttempt = 0;
|
|
|
|
|
this.#resolveRetry();
|
|
|
|
|
// Tool-use orphans corrupt Anthropic message history (tool_result without
|
|
|
|
|
// matching tool_use). Always remove them even when the retry cap is hit.
|
|
|
|
@@ -10939,7 +11158,11 @@ export class AgentSession {
|
|
|
|
|
): Promise<CompactionCheckResult> {
|
|
|
|
|
const compactionEntryBefore = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
|
|
|
await this.#dropPersistedAssistantTurn(assistantMessage);
|
|
|
|
|
const result = await this.#runAutoCompaction(reason, true, false, allowDefer, options);
|
|
|
|
|
const result = await this.#runAutoCompaction(reason, true, false, allowDefer, {
|
|
|
|
|
autoContinue: options.autoContinue,
|
|
|
|
|
triggerContextTokens: options.triggerContextTokens,
|
|
|
|
|
phase: "mid_turn",
|
|
|
|
|
});
|
|
|
|
|
const compactionEntryAfter = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
|
|
|
if (result.historyRewritten !== true && compactionEntryAfter === compactionEntryBefore) {
|
|
|
|
|
this.#restoreFailedAssistantTurn(assistantMessage);
|
|
|
|
@@ -11538,6 +11761,9 @@ export class AgentSession {
|
|
|
|
|
const currentModel = this.model;
|
|
|
|
|
if (currentModel) {
|
|
|
|
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
|
|
|
if (!modelsAreEqual(currentModel, model)) {
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.agent.setModel(model);
|
|
|
|
|
|
|
|
|
@@ -11551,6 +11777,14 @@ export class AgentSession {
|
|
|
|
|
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#resetCodexProviderAfterCompaction(compaction: CodexCompactionContext): void {
|
|
|
|
|
resetOpenAICodexHistoryAfterCompaction({
|
|
|
|
|
providerSessionState: this.#providerSessionState,
|
|
|
|
|
sessionId: this.sessionId,
|
|
|
|
|
compaction,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#resetCurrentResponsesProviderSession(reason: string): void {
|
|
|
|
|
const currentModel = this.model;
|
|
|
|
|
if (currentModel?.api !== "openai-responses" && currentModel?.api !== "openai-codex-responses") {
|
|
|
|
@@ -11826,7 +12060,7 @@ export class AgentSession {
|
|
|
|
|
if (!trimmedTarget) return undefined;
|
|
|
|
|
|
|
|
|
|
const parsed = parseModelString(trimmedTarget, {
|
|
|
|
|
allowMaxAlias: true,
|
|
|
|
|
allowMaxSuffix: true,
|
|
|
|
|
allowAutoAlias: true,
|
|
|
|
|
isLiteralModelId: (provider, id) =>
|
|
|
|
|
availableModels.some(model => model.provider === provider && model.id === id),
|
|
|
|
@@ -11921,18 +12155,6 @@ export class AgentSession {
|
|
|
|
|
|
|
|
|
|
return candidates;
|
|
|
|
|
}
|
|
|
|
|
#isCompactionAuthFailure(error: unknown): boolean {
|
|
|
|
|
if (!(error instanceof Error)) return false;
|
|
|
|
|
// Real provider 401/403 — surfaced as `.status` by the compaction layer
|
|
|
|
|
// (see `createSummarizationError` in packages/agent/src/compaction/compaction.ts).
|
|
|
|
|
// Without this branch, an expired/revoked Anthropic key would bypass the
|
|
|
|
|
// authenticated-fallback path and dump the raw HTTP body into the UI.
|
|
|
|
|
const status = (error as Error & { status?: number }).status;
|
|
|
|
|
if (status === 401 || status === 403) return true;
|
|
|
|
|
// pi-native gateway synthetic for "no credential configured" (issue #986).
|
|
|
|
|
// Carries no HTTP status, so the legacy message regex stays.
|
|
|
|
|
return /auth_unavailable|no auth available/i.test(error.message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#buildCompactionAuthError(): Error {
|
|
|
|
|
const currentModel = this.model;
|
|
|
|
@@ -11982,6 +12204,7 @@ export class AgentSession {
|
|
|
|
|
tools: this.agent.state.tools,
|
|
|
|
|
sessionId: this.sessionId,
|
|
|
|
|
promptCacheKey: this.sessionId,
|
|
|
|
|
providerSessionState: this.#providerSessionState,
|
|
|
|
|
// Route every summarization HTTP request through the
|
|
|
|
|
// session's side-stream transport so the provider
|
|
|
|
|
// concurrency cap (e.g. providers.ollama-cloud.maxConcurrency)
|
|
|
|
@@ -11997,7 +12220,7 @@ export class AgentSession {
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (!this.#isCompactionAuthFailure(error)) {
|
|
|
|
|
if (!AIError.is(AIError.classify(error, candidate.api), AIError.Flag.AuthFailed)) {
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -12320,6 +12543,7 @@ export class AgentSession {
|
|
|
|
|
triggerContextTokens?: number;
|
|
|
|
|
suppressContinuation?: boolean;
|
|
|
|
|
suppressHandoff?: boolean;
|
|
|
|
|
phase?: CodexCompactionContext["phase"];
|
|
|
|
|
} = {},
|
|
|
|
|
): Promise<CompactionCheckResult> {
|
|
|
|
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
|
|
@@ -12362,7 +12586,7 @@ export class AgentSession {
|
|
|
|
|
async signal => {
|
|
|
|
|
await Promise.resolve();
|
|
|
|
|
if (signal.aborted) return;
|
|
|
|
|
await this.#runAutoCompaction(reason, willRetry, true);
|
|
|
|
|
await this.#runAutoCompaction(reason, willRetry, true, true, { phase: options.phase });
|
|
|
|
|
},
|
|
|
|
|
{ generation },
|
|
|
|
|
);
|
|
|
|
@@ -12509,6 +12733,7 @@ export class AgentSession {
|
|
|
|
|
let hookCompaction: CompactionResult | undefined;
|
|
|
|
|
let fromExtension = false;
|
|
|
|
|
let preserveData: Record<string, unknown> | undefined;
|
|
|
|
|
let codexCompaction: CodexCompactionContext | undefined;
|
|
|
|
|
|
|
|
|
|
if (this.#extensionRunner?.hasHandlers("session_before_compact")) {
|
|
|
|
|
const hookResult = (await this.#extensionRunner.emit({
|
|
|
|
@@ -12647,6 +12872,13 @@ export class AgentSession {
|
|
|
|
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
|
|
|
let compactResult: CompactionResult | undefined;
|
|
|
|
|
let lastError: unknown;
|
|
|
|
|
codexCompaction = createCodexCompactionContext({
|
|
|
|
|
trigger: "auto",
|
|
|
|
|
reason: "context_limit",
|
|
|
|
|
phase:
|
|
|
|
|
options.phase ??
|
|
|
|
|
(reason === "threshold" ? "pre_turn" : reason === "idle" ? "standalone_turn" : "mid_turn"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
|
|
|
|
|
const candidate = candidates[candidateIndex];
|
|
|
|
@@ -12679,6 +12911,8 @@ export class AgentSession {
|
|
|
|
|
tools: this.agent.state.tools,
|
|
|
|
|
sessionId: this.sessionId,
|
|
|
|
|
promptCacheKey: this.sessionId,
|
|
|
|
|
providerSessionState: this.#providerSessionState,
|
|
|
|
|
codexCompaction,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
@@ -12689,7 +12923,7 @@ export class AgentSession {
|
|
|
|
|
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
|
|
const id = AIError.classify(error, candidate.api);
|
|
|
|
|
if (this.#isCompactionAuthFailure(error)) {
|
|
|
|
|
if (AIError.is(id, AIError.Flag.AuthFailed)) {
|
|
|
|
|
lastError = this.#buildCompactionAuthError();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
@@ -12797,7 +13031,11 @@ export class AgentSession {
|
|
|
|
|
this.#planReferenceSent = false;
|
|
|
|
|
this.#resetAllAdvisorRuntimes();
|
|
|
|
|
this.#syncTodoPhasesFromBranch();
|
|
|
|
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
|
|
|
if (codexCompaction) {
|
|
|
|
|
this.#resetCodexProviderAfterCompaction(codexCompaction);
|
|
|
|
|
} else {
|
|
|
|
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the saved compaction entry for the hook
|
|
|
|
|
const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
|
|
|
|
@@ -14679,6 +14917,7 @@ export class AgentSession {
|
|
|
|
|
const previousSystemPrompt = this.agent.state.systemPrompt;
|
|
|
|
|
const previousBaseSystemPromptBeforeMemoryPromotion = this.#baseSystemPromptBeforeMemoryPromotion;
|
|
|
|
|
const previousFreshProviderSessionId = this.#freshProviderSessionId;
|
|
|
|
|
const previousInheritedProviderPromptCacheKey = this.#inheritedProviderPromptCacheKey;
|
|
|
|
|
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
|
|
|
|
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
|
|
|
|
: undefined;
|
|
|
|
@@ -14700,6 +14939,8 @@ export class AgentSession {
|
|
|
|
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
|
|
|
if (switchingToDifferentSession) {
|
|
|
|
|
this.#freshProviderSessionId = undefined;
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
this.#adoptInheritedProviderPromptCacheKey();
|
|
|
|
|
}
|
|
|
|
|
this.#syncAgentSessionId();
|
|
|
|
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
|
|
@@ -14853,6 +15094,7 @@ export class AgentSession {
|
|
|
|
|
this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
|
|
|
|
|
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
|
|
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
|
|
|
this.#inheritedProviderPromptCacheKey = previousInheritedProviderPromptCacheKey;
|
|
|
|
|
this.#checkpointState = previousCheckpointState;
|
|
|
|
|
this.#pendingRewindReport = previousPendingRewindReport;
|
|
|
|
|
this.#lastCompletedRewind = previousLastCompletedRewind;
|
|
|
|
@@ -14928,6 +15170,7 @@ export class AgentSession {
|
|
|
|
|
this.#rehydrateCheckpointRewindState();
|
|
|
|
|
this.#syncTodoPhasesFromBranch();
|
|
|
|
|
this.#freshProviderSessionId = undefined;
|
|
|
|
|
this.#clearInheritedProviderPromptCacheKey();
|
|
|
|
|
this.#syncAgentSessionId();
|
|
|
|
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
|
|
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
|
|
|