Merge remote-tracking branch 'can1357/main' into fix/empty-stop-guard-tooluse
This commit is contained in:
@@ -91,6 +91,7 @@ import {
|
||||
extractRetryHint,
|
||||
getAgentDbPath,
|
||||
getInstallId,
|
||||
isBunTestRuntime,
|
||||
isEnoent,
|
||||
isUnexpectedSocketCloseMessage,
|
||||
logger,
|
||||
@@ -127,7 +128,6 @@ import {
|
||||
} from "../eval/py/executor";
|
||||
import { defaultEvalSessionId } from "../eval/session-id";
|
||||
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
||||
import { exportSessionToHtml } from "../export/html";
|
||||
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
||||
import type { LoadedCustomCommand } from "../extensibility/custom-commands";
|
||||
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
||||
@@ -192,6 +192,7 @@ import {
|
||||
toReasoningEffort,
|
||||
} from "../thinking";
|
||||
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
||||
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
||||
import {
|
||||
buildDiscoverableToolSearchIndex,
|
||||
collectDiscoverableTools,
|
||||
@@ -470,6 +471,12 @@ export interface SessionStats {
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export interface FreshSessionResult {
|
||||
previousSessionId: string;
|
||||
sessionId: string;
|
||||
closedProviderSessions: number;
|
||||
}
|
||||
|
||||
/** Internal marker for hook messages queued through the agent loop */
|
||||
// ============================================================================
|
||||
// Constants
|
||||
@@ -921,6 +928,7 @@ export class AgentSession {
|
||||
#agentId: string | undefined;
|
||||
#agentRegistry: AgentRegistry | undefined;
|
||||
#providerSessionId: string | undefined;
|
||||
#freshProviderSessionId: string | undefined;
|
||||
#isDisposed = false;
|
||||
// Extension system
|
||||
#extensionRunner: ExtensionRunner | undefined = undefined;
|
||||
@@ -1035,6 +1043,7 @@ export class AgentSession {
|
||||
|
||||
#acquirePowerAssertion(): void {
|
||||
if (process.platform !== "darwin") return;
|
||||
if (isBunTestRuntime()) return;
|
||||
if (this.#powerAssertion) return;
|
||||
const idle = this.settings.get("power.preventIdleSleep");
|
||||
const system = this.settings.get("power.preventSystemSleep");
|
||||
@@ -1273,6 +1282,14 @@ export class AgentSession {
|
||||
return this.#modelRegistry;
|
||||
}
|
||||
|
||||
get asyncJobManager(): AsyncJobManager | undefined {
|
||||
return this.#asyncJobManager;
|
||||
}
|
||||
|
||||
getAgentId(): string | undefined {
|
||||
return this.#agentId;
|
||||
}
|
||||
|
||||
/** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
|
||||
nextToolChoice(): ToolChoice | undefined {
|
||||
return this.#toolChoiceQueue.nextToolChoice();
|
||||
@@ -1679,7 +1696,7 @@ export class AgentSession {
|
||||
// Abort the stream immediately — do not gate on extension callbacks
|
||||
this.#ttsrAbortPending = true;
|
||||
this.#ensureTtsrResumePromise();
|
||||
this.agent.abort();
|
||||
this.agent.abort(this.#formatTtsrAbortReason(matches));
|
||||
// Notify extensions (fire-and-forget, does not block abort)
|
||||
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
||||
// Schedule retry after a short delay
|
||||
@@ -2160,6 +2177,12 @@ export class AgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
#formatTtsrAbortReason(rules: Rule[]): string {
|
||||
const label = rules.length === 1 ? "rule" : "rules";
|
||||
const ruleNames = rules.map(rule => rule.name).join(", ");
|
||||
return `TTSR matched ${label}: ${ruleNames}`;
|
||||
}
|
||||
|
||||
/** Get TTSR injection payload and clear pending injections. */
|
||||
#getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
|
||||
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
||||
@@ -2183,13 +2206,20 @@ export class AgentSession {
|
||||
* project, `~`-relative when it lives under home, else the raw path.
|
||||
*/
|
||||
#displayRulePath(rulePath: string): string {
|
||||
const cwdRel = relativePathWithinRoot(this.sessionManager.getCwd(), rulePath);
|
||||
const cwdRel =
|
||||
relativePathWithinRoot(this.sessionManager.getCwd(), rulePath) ??
|
||||
this.#displayPathWithinRoot(this.sessionManager.getCwd(), rulePath);
|
||||
if (cwdRel) return cwdRel;
|
||||
const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
|
||||
if (homeRel) return `~/${homeRel}`;
|
||||
return rulePath;
|
||||
}
|
||||
|
||||
#displayPathWithinRoot(root: string, candidate: string): string | null {
|
||||
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
||||
return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
|
||||
}
|
||||
|
||||
#addPendingTtsrInjections(rules: Rule[]): void {
|
||||
const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
|
||||
for (const rule of rules) {
|
||||
@@ -2944,6 +2974,10 @@ export class AgentSession {
|
||||
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
||||
}
|
||||
|
||||
#activeProviderSessionId(sessionId?: string): string {
|
||||
return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set agent.sessionId from the session manager and install a dynamic
|
||||
* metadata resolver so every Anthropic API request carries
|
||||
@@ -2956,7 +2990,7 @@ export class AgentSession {
|
||||
* `#syncAgentSessionId()` on every such event.
|
||||
*/
|
||||
#syncAgentSessionId(sessionId?: string): void {
|
||||
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
||||
const sid = this.#activeProviderSessionId(sessionId);
|
||||
this.agent.sessionId = sid;
|
||||
this.agent.setMetadataResolver((provider: string) =>
|
||||
buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
|
||||
@@ -2964,14 +2998,14 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
||||
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
||||
if (this.settings.get("memory.backend") !== "hindsight") return;
|
||||
const sid = this.agent.sessionId;
|
||||
if (!sid) return;
|
||||
this.getHindsightSessionState()?.setSessionId(sid);
|
||||
}
|
||||
|
||||
#rekeyMnemopiMemoryForCurrentSessionId(): void {
|
||||
if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
|
||||
if (this.settings.get("memory.backend") !== "mnemopi") return;
|
||||
const sid = this.agent.sessionId;
|
||||
if (!sid) return;
|
||||
this.getMnemopiSessionState()?.setSessionId(sid);
|
||||
@@ -2979,14 +3013,14 @@ export class AgentSession {
|
||||
|
||||
/** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
|
||||
#resetHindsightConversationTrackingIfHindsight(): void {
|
||||
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
||||
if (this.settings.get("memory.backend") !== "hindsight") return;
|
||||
const state = this.getHindsightSessionState();
|
||||
if (!state || state.aliasOf) return;
|
||||
state.resetConversationTracking();
|
||||
}
|
||||
|
||||
#resetMnemopiConversationTrackingIfMnemopi(): void {
|
||||
if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
|
||||
if (this.settings.get("memory.backend") !== "mnemopi") return;
|
||||
const state = this.getMnemopiSessionState();
|
||||
if (!state || state.aliasOf) return;
|
||||
state.resetConversationTracking();
|
||||
@@ -3086,6 +3120,23 @@ export class AgentSession {
|
||||
this.#providerSessionState.clear();
|
||||
}
|
||||
|
||||
freshSession(): FreshSessionResult | undefined {
|
||||
if (this.isStreaming) return undefined;
|
||||
const previousSessionId = this.sessionId;
|
||||
const closedProviderSessions = this.#providerSessionState.size;
|
||||
this.#closeAllProviderSessions("fresh session");
|
||||
this.#freshProviderSessionId = Bun.randomUUIDv7();
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
this.agent.appendOnlyContext?.invalidateForModelChange();
|
||||
return {
|
||||
previousSessionId,
|
||||
sessionId: this.sessionId,
|
||||
closedProviderSessions,
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Read-only State Access
|
||||
// =========================================================================
|
||||
@@ -3325,12 +3376,14 @@ export class AgentSession {
|
||||
|
||||
// ── Generic tool discovery (covers built-in + MCP + extension) ────────────
|
||||
|
||||
/** Resolve effective discovery mode: tools.discoveryMode wins; mcp.discoveryMode is back-compat alias. */
|
||||
/** Resolve effective discovery mode from the current registry size. */
|
||||
#resolveEffectiveDiscoveryMode(): "off" | "mcp-only" | "all" {
|
||||
const toolsMode = this.settings.get("tools.discoveryMode");
|
||||
if (toolsMode !== "off") return toolsMode as "off" | "mcp-only" | "all";
|
||||
if (this.settings.get("mcp.discoveryMode")) return "mcp-only";
|
||||
return "off";
|
||||
const mode = resolveEffectiveToolDiscoveryMode(
|
||||
this.settings,
|
||||
countToolsForAutoDiscovery(this.#toolRegistry.keys()),
|
||||
);
|
||||
if (mode !== "off") return mode;
|
||||
return this.#mcpDiscoveryEnabled ? "mcp-only" : "off";
|
||||
}
|
||||
|
||||
isToolDiscoveryEnabled(): boolean {
|
||||
@@ -3665,7 +3718,7 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
|
||||
const backend = resolveMemoryBackend(this.settings);
|
||||
const backend = await resolveMemoryBackend(this.settings);
|
||||
if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
|
||||
|
||||
try {
|
||||
@@ -3988,7 +4041,7 @@ export class AgentSession {
|
||||
|
||||
/** Current session ID */
|
||||
get sessionId(): string {
|
||||
return this.#providerSessionId ?? this.sessionManager.getSessionId();
|
||||
return this.#activeProviderSessionId();
|
||||
}
|
||||
getEvalSessionId(): string | null {
|
||||
if (this.#parentEvalSessionId !== undefined) return this.#parentEvalSessionId;
|
||||
@@ -5087,8 +5140,13 @@ export class AgentSession {
|
||||
|
||||
/**
|
||||
* Abort current operation and wait for agent to become idle.
|
||||
*
|
||||
* `reason` (e.g. `USER_INTERRUPT_LABEL`) rides the agent's `AbortController`
|
||||
* and surfaces verbatim on the aborted assistant message's `errorMessage`, so
|
||||
* the transcript can distinguish a deliberate user interrupt from an opaque
|
||||
* abort. Omit it for internal/lifecycle aborts.
|
||||
*/
|
||||
async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
|
||||
async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
|
||||
this.abortRetry();
|
||||
this.#promptGeneration++;
|
||||
this.#scheduledHiddenNextTurnGeneration = undefined;
|
||||
@@ -5097,7 +5155,7 @@ export class AgentSession {
|
||||
this.abortBash();
|
||||
this.abortEval();
|
||||
const postPromptDrain = this.#cancelPostPromptTasks();
|
||||
this.agent.abort();
|
||||
this.agent.abort(options?.reason);
|
||||
await postPromptDrain;
|
||||
await this.agent.waitForIdle();
|
||||
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
||||
@@ -5114,6 +5172,19 @@ export class AgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort active work, then immediately resume the agent so queued steer/follow-up
|
||||
* messages drain instead of waiting for another natural turn boundary.
|
||||
*/
|
||||
async interruptAndFlushQueuedMessages(options?: { reason?: string }): Promise<void> {
|
||||
if (!this.agent.hasQueuedMessages()) return;
|
||||
await this.abort({ reason: options?.reason });
|
||||
if (!this.agent.hasQueuedMessages()) return;
|
||||
if (this.isCompacting || this.isGeneratingHandoff) return;
|
||||
await this.#maybeRestoreRetryFallbackPrimary();
|
||||
await this.agent.continue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new session, optionally with initial messages and parent tracking.
|
||||
* Clears all messages and starts a new session.
|
||||
@@ -5158,6 +5229,7 @@ export class AgentSession {
|
||||
}
|
||||
await this.sessionManager.newSession(options);
|
||||
this.setTodoPhases([]);
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -5255,6 +5327,7 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
// Update agent session ID
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -6091,7 +6164,7 @@ export class AgentSession {
|
||||
messagesToSummarize: AgentMessage[];
|
||||
turnPrefixMessages: AgentMessage[];
|
||||
}): Promise<string | undefined> {
|
||||
const backend = resolveMemoryBackend(this.settings);
|
||||
const backend = await resolveMemoryBackend(this.settings);
|
||||
if (!backend.preCompactionContext) return undefined;
|
||||
const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
||||
try {
|
||||
@@ -6222,6 +6295,7 @@ export class AgentSession {
|
||||
this.#cancelOwnAsyncJobs();
|
||||
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
||||
this.agent.reset();
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -8150,8 +8224,10 @@ export class AgentSession {
|
||||
|
||||
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
||||
if (!switchedCredential && currentSelector) {
|
||||
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
||||
switchedModel = await this.#tryRetryModelFallback(currentSelector);
|
||||
if (retrySettings.modelFallback) {
|
||||
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
||||
switchedModel = await this.#tryRetryModelFallback(currentSelector);
|
||||
}
|
||||
if (switchedModel) {
|
||||
delayMs = 0;
|
||||
} else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
||||
@@ -8952,6 +9028,7 @@ export class AgentSession {
|
||||
const previousTools = [...this.agent.state.tools];
|
||||
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
||||
const previousSystemPrompt = this.agent.state.systemPrompt;
|
||||
const previousFreshProviderSessionId = this.#freshProviderSessionId;
|
||||
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
||||
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
||||
: undefined;
|
||||
@@ -8963,6 +9040,9 @@ export class AgentSession {
|
||||
|
||||
try {
|
||||
await this.sessionManager.setSessionFile(sessionPath);
|
||||
if (switchingToDifferentSession) {
|
||||
this.#freshProviderSessionId = undefined;
|
||||
}
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -9072,6 +9152,7 @@ export class AgentSession {
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.sessionManager.restoreState(previousSessionState);
|
||||
this.#freshProviderSessionId = previousFreshProviderSessionId;
|
||||
this.#syncAgentSessionId(previousSessionState.sessionId);
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -9170,6 +9251,7 @@ export class AgentSession {
|
||||
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
||||
}
|
||||
this.#syncTodoPhasesFromBranch();
|
||||
this.#freshProviderSessionId = undefined;
|
||||
this.#syncAgentSessionId();
|
||||
this.#rekeyHindsightMemoryForCurrentSessionId();
|
||||
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
||||
@@ -9598,6 +9680,7 @@ export class AgentSession {
|
||||
*/
|
||||
async exportToHtml(outputPath?: string): Promise<string> {
|
||||
const themeName = getCurrentThemeName();
|
||||
const { exportSessionToHtml } = await import("../export/html");
|
||||
return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user