import {
Agent,
type AgentEvent,
type AgentMessage,
type AgentTelemetryConfig,
type AgentTool,
AppendOnlyContextManager,
INTENT_FIELD,
type ThinkingLevel,
} from "@oh-my-pi/pi-agent-core";
import {
type CredentialDisabledEvent,
isUsageLimitError,
type Message,
type Model,
type SimpleStreamOptions,
streamSimple,
} from "@oh-my-pi/pi-ai";
import {
getOpenAICodexTransportDetails,
prewarmOpenAICodexResponses,
} from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import type { Component } from "@oh-my-pi/pi-tui";
import {
$env,
$flag,
extractRetryHint,
getAgentDbPath,
getAgentDir,
getProjectDir,
logger,
postmortem,
prompt,
Snowflake,
} from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
import { createAutoresearchExtension } from "./autoresearch";
import { loadCapability } from "./capability";
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
import { bucketRules } from "./capability/rule-buckets";
import { ModelRegistry } from "./config/model-registry";
import {
formatModelString,
parseModelPattern,
parseModelString,
resolveAllowedModels,
resolveModelRoleValue,
} from "./config/model-resolver";
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
import { Settings, type SkillsSettings } from "./config/settings";
import { CursorExecHandlers } from "./cursor";
import "./discovery";
import { resolveConfigValue } from "./config/resolve-config-value";
import { initializeWithSettings } from "./discovery";
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
import { defaultEvalSessionId } from "./eval/session-id";
import { TtsrManager } from "./export/ttsr";
import {
type CustomCommandsLoadResult,
type LoadedCustomCommand,
loadCustomCommands as loadCustomCommandsInternal,
} from "./extensibility/custom-commands";
import { discoverAndLoadCustomTools } from "./extensibility/custom-tools";
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
import {
discoverAndLoadExtensions,
type ExtensionContext,
type ExtensionFactory,
ExtensionRunner,
ExtensionToolWrapper,
type ExtensionUIContext,
type LoadExtensionsResult,
loadExtensionFromFactory,
loadExtensions,
type ToolDefinition,
wrapRegisteredTools,
} from "./extensibility/extensions";
import {
loadSkills as loadSkillsInternal,
type Skill,
type SkillWarning,
setActiveSkills,
} from "./extensibility/skills";
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
import type { HindsightSessionState } from "./hindsight/state";
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
import { discoverAndLoadMCPTools, MCPManager, type MCPToolsLoadResult } from "./mcp";
import { resolveMemoryBackend } from "./memory-backend";
import { getMnemopiSessionState, type MnemopiSessionState } from "./mnemopi/state";
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
import {
collectEnvSecrets,
deobfuscateSessionContext,
loadSecrets,
obfuscateMessages,
SecretObfuscator,
} from "./secrets";
import { AgentSession } from "./session/agent-session";
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
import { type CustomMessage, convertToLlm } from "./session/messages";
import { SessionManager } from "./session/session-manager";
import { closeAllConnections } from "./ssh/connection-manager";
import { unmountAll } from "./ssh/sshfs-mount";
import {
type BuildSystemPromptResult,
buildSystemPrompt as buildSystemPromptInternal,
buildSystemPromptToolMetadata,
loadProjectContextFiles as loadContextFilesInternal,
} from "./system-prompt";
import { AgentOutputManager } from "./task/output-manager";
import {
AUTO_THINKING,
type ConfiguredThinkingLevel,
parseThinkingLevel,
resolveProvisionalAutoLevel,
resolveThinkingLevelForModel,
toReasoningEffort,
} from "./thinking";
import {
collectDiscoverableTools,
type DiscoverableTool,
filterBySource,
formatDiscoverableToolServerSummary,
selectDiscoverableToolNamesByServer,
summarizeDiscoverableTools,
} from "./tool-discovery/tool-index";
import {
BashTool,
BUILTIN_TOOLS,
computeEssentialBuiltinNames,
createTools,
discoverStartupLspServers,
EditTool,
EvalTool,
FindTool,
getSearchTools,
HIDDEN_TOOLS,
isImageProviderPreference,
isSearchProviderPreference,
type LspStartupServerInfo,
loadSshTool,
ReadTool,
ResolveTool,
renderSearchToolBm25Description,
SearchTool,
setPreferredImageProvider,
setPreferredSearchProvider,
type Tool,
type ToolSession,
WebSearchTool,
WriteTool,
warmupLspServers,
} from "./tools";
import { ToolContextStore } from "./tools/context";
import { getImageGenTools } from "./tools/image-gen";
import { wrapToolWithMetaNotice } from "./tools/output-meta";
import { queueResolveHandler } from "./tools/resolve";
import { ttsTool } from "./tools/tts";
import { EventBus } from "./utils/event-bus";
import { buildNamedToolChoice } from "./utils/tool-choice";
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
type AsyncResultEntry = {
jobId: string;
result: string;
job: AsyncJob | undefined;
durationMs: number | undefined;
};
type AsyncResultJobDetails = {
jobId: string;
type?: "bash" | "task";
label?: string;
durationMs?: number;
};
type AsyncResultDetails = {
jobs: AsyncResultJobDetails[];
};
type McpNotificationEntry = {
serverName: string;
uri: string;
};
function buildAsyncResultBatchMessage(entries: AsyncResultEntry[]): CustomMessage<AsyncResultDetails> | null {
if (entries.length === 0) return null;
const jobs = entries.map(entry => ({
jobId: entry.jobId,
result: entry.result,
type: entry.job?.type,
label: entry.job?.label,
durationMs: entry.durationMs,
}));
const details: AsyncResultDetails = {
jobs: jobs.map(job => ({
jobId: job.jobId,
type: job.type,
label: job.label,
durationMs: job.durationMs,
})),
};
return {
role: "custom",
customType: "async-result",
content: prompt.render(asyncResultTemplate, {
multiple: jobs.length > 1,
jobs,
}),
display: true,
attribution: "agent",
details,
timestamp: Date.now(),
};
}
function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): AgentMessage | null {
const resources: McpNotificationEntry[] = [];
const seen = new Set<string>();
for (const entry of entries) {
const key = `${entry.serverName}\0${entry.uri}`;
if (seen.has(key)) continue;
seen.add(key);
resources.push(entry);
}
if (resources.length === 0) return null;
const lines = [`[MCP notification] ${resources.length} resource(s) updated:`];
for (const resource of resources) {
lines.push(`- server="${resource.serverName}" uri=${resource.uri}`);
}
lines.push('Use read(path="mcp://<uri>") to inspect if relevant.');
return {
role: "user",
content: [{ type: "text", text: lines.join("\n") }],
attribution: "agent",
timestamp: Date.now(),
};
}
export interface CreateAgentSessionOptions {
cwd?: string;
agentDir?: string;
spawns?: string;
authStorage?: AuthStorage;
modelRegistry?: ModelRegistry;
model?: Model;
* Used when model lookup is deferred because extension-provided models aren't registered yet. */
modelPattern?: string;
thinkingLevel?: ConfiguredThinkingLevel;
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
systemPrompt?: string[] | ((defaultPrompt: string[]) => string[]);
* Keeps persisted session files isolated while reusing provider-side caches. */
providerSessionId?: string;
customTools?: (CustomTool | ToolDefinition)[];
extensions?: ExtensionFactory[];
additionalExtensionPaths?: string[];
disableExtensionDiscovery?: boolean;
* Pre-loaded extensions (skips file discovery).
* @internal Used by CLI when extensions are loaded early to parse custom flags.
*/
preloadedExtensions?: LoadExtensionsResult;
eventBus?: EventBus;
skills?: Skill[];
rules?: Rule[];
contextFiles?: Array<{ path: string; content: string }>;
workspaceTree?: WorkspaceTree;
promptTemplates?: PromptTemplate[];
slashCommands?: FileSlashCommand[];
enableMCP?: boolean;
mcpManager?: MCPManager;
enableLsp?: boolean;
skipPythonPreflight?: boolean;
toolNames?: string[];
outputSchema?: unknown;
requireYieldTool?: boolean;
taskDepth?: number;
parentHindsightSessionState?: HindsightSessionState;
parentMnemopiSessionState?: MnemopiSessionState;
agentId?: string;
agentDisplayName?: string;
agentRegistry?: AgentRegistry;
parentTaskPrefix?: string;
parentEvalSessionId?: string;
sessionManager?: SessionManager;
localProtocolOptions?: LocalProtocolOptions;
settings?: Settings;
hasUI?: boolean;
* Opt-in OpenTelemetry instrumentation forwarded to the underlying Agent.
* Passing `{}` enables the loop's GenAI-semantic-convention spans. See
* {@link AgentTelemetryConfig} for the full surface (hooks, content capture,
* cost estimator, agent identity).
*
* Safe to enable without an OTEL SDK registered in the host: the
* `@opentelemetry/api` package returns a no-op tracer in that case.
*/
telemetry?: AgentTelemetryConfig;
autoApprove?: boolean;
}
export interface CreateAgentSessionResult {
session: AgentSession;
extensionsResult: LoadExtensionsResult;
setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
mcpManager?: MCPManager;
modelFallbackMessage?: string;
lspServers?: LspStartupServerInfo[];
eventBus: EventBus;
}
export type { PromptTemplate } from "./config/prompt-templates";
export { Settings, type SkillsSettings } from "./config/settings";
export type { CustomCommand, CustomCommandFactory } from "./extensibility/custom-commands/types";
export type { CustomTool, CustomToolFactory } from "./extensibility/custom-tools/types";
export type * from "./extensibility/extensions";
export type { Skill } from "./extensibility/skills";
export type { FileSlashCommand } from "./extensibility/slash-commands";
export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp";
export type { Tool } from "./tools";
export { buildDirectoryTree, buildWorkspaceTree, type DirectoryTree, type WorkspaceTree } from "./workspace-tree";
export {
BashTool,
BUILTIN_TOOLS,
createTools,
EditTool,
EvalTool,
FindTool,
HIDDEN_TOOLS,
loadSshTool,
ReadTool,
ResolveTool,
SearchTool,
type ToolSession,
WebSearchTool,
WriteTool,
};
function getDefaultAgentDir(): string {
return getAgentDir();
}
* Create an AuthStorage instance.
*
* Default: local SQLite store at `<agentDir>/agent.db`.
*
* Broker mode: when `OMP_AUTH_BROKER_URL` is set, credentials are pulled from
* a remote auth-broker over the wire. Refresh tokens never leave the broker;
* the client receives access tokens with `refresh = "__remote__"` and calls
* back into the broker through the {@link AuthStorageOptions.refreshOAuthCredential}
* override to re-mint access tokens when needed.
*/
export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise<AuthStorage> {
const brokerConfig = await resolveAuthBrokerConfig();
if (brokerConfig) {
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
const initialResult = await client.fetchSnapshot();
if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot");
const store = new RemoteAuthCredentialStore({ client, initialSnapshot: initialResult.snapshot });
const storage = new AuthStorage(store, {
configValueResolver: resolveConfigValue,
sourceLabel: `broker ${brokerConfig.url}`,
});
await storage.reload();
return storage;
}
const dbPath = getAgentDbPath(agentDir);
const storage = await AuthStorage.create(dbPath, {
configValueResolver: resolveConfigValue,
sourceLabel: `local ${dbPath}`,
});
await storage.reload();
return storage;
}
* Discover extensions from cwd.
*/
export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsResult> {
const resolvedCwd = cwd ?? getProjectDir();
return discoverAndLoadExtensions([], resolvedCwd);
}
* Load the discovered/configured extensions for a session — everything {@link
* createAgentSession} would load except the inline factory extensions it appends
* itself. Extracted so the CLI can resolve extension-registered flags (and thus
* classify `@file` arguments extension-aware) *before* a session — and its
* terminal breadcrumb — is created, then hand the result back through
* {@link CreateAgentSessionOptions.preloadedExtensions} so the work is not
* repeated. Keep this the single source of the discovery branch logic.
*/
export async function loadSessionExtensions(
options: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths">,
cwd: string,
settings: Settings,
eventBus: EventBus,
): Promise<LoadExtensionsResult> {
let result: LoadExtensionsResult;
if (options.disableExtensionDiscovery) {
const configuredPaths = options.additionalExtensionPaths ?? [];
result = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
} else {
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
result = await logger.time(
"discoverAndLoadExtensions",
discoverAndLoadExtensions,
configuredPaths,
cwd,
eventBus,
disabledExtensionIds,
);
}
for (const { path, error } of result.errors) {
logger.error("Failed to load extension", { path, error });
}
return result;
}
* Discover skills from cwd and agentDir.
*/
export async function discoverSkills(
cwd?: string,
_agentDir?: string,
settings?: SkillsSettings,
): Promise<{ skills: Skill[]; warnings: SkillWarning[] }> {
return await loadSkillsInternal({
...settings,
cwd: cwd ?? getProjectDir(),
});
}
* Discover context files (AGENTS.md) walking up from cwd.
* Returns files sorted by depth (farther from cwd first, so closer files appear last/more prominent).
*/
export async function discoverContextFiles(
cwd?: string,
_agentDir?: string,
): Promise<Array<{ path: string; content: string; depth?: number }>> {
return await loadContextFilesInternal({
cwd: cwd ?? getProjectDir(),
});
}
* Discover prompt templates from cwd and agentDir.
*/
export async function discoverPromptTemplates(cwd?: string, agentDir?: string): Promise<PromptTemplate[]> {
return await loadPromptTemplatesInternal({
cwd: cwd ?? getProjectDir(),
agentDir: agentDir ?? getDefaultAgentDir(),
});
}
* Discover file-based slash commands from commands/ directories.
*/
export async function discoverSlashCommands(cwd?: string): Promise<FileSlashCommand[]> {
return loadSlashCommandsInternal({ cwd: cwd ?? getProjectDir() });
}
* Discover custom commands (TypeScript slash commands) from cwd and agentDir.
*/
export async function discoverCustomTSCommands(cwd?: string, agentDir?: string): Promise<CustomCommandsLoadResult> {
const resolvedCwd = cwd ?? getProjectDir();
const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
return loadCustomCommandsInternal({
cwd: resolvedCwd,
agentDir: resolvedAgentDir,
});
}
* Discover MCP servers from .mcp.json files.
* Returns the manager and loaded tools.
*/
export async function discoverMCPServers(cwd?: string): Promise<MCPToolsLoadResult> {
const resolvedCwd = cwd ?? getProjectDir();
return discoverAndLoadMCPTools(resolvedCwd);
}
export interface BuildSystemPromptOptions {
tools?: Tool[];
skills?: Skill[];
contextFiles?: Array<{ path: string; content: string }>;
cwd?: string;
appendPrompt?: string;
repeatToolDescriptions?: boolean;
}
* Build the default provider-facing system prompt blocks.
*
* The returned `systemPrompt` preserves the stable harness prompt and dynamic project context
* as separate entries so providers can cache prompt prefixes without concatenating blocks.
*/
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
return await buildSystemPromptInternal({
cwd: options.cwd,
skills: options.skills,
contextFiles: options.contextFiles,
appendSystemPrompt: options.appendPrompt,
repeatToolDescriptions: options.repeatToolDescriptions,
});
}
function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
return {
sessionManager: ctx.sessionManager,
modelRegistry: ctx.modelRegistry,
model: ctx.model,
isIdle: ctx.isIdle,
hasQueuedMessages: ctx.hasPendingMessages,
abort: ctx.abort,
};
}
function isCustomTool(tool: CustomTool | ToolDefinition): tool is CustomTool {
return !(tool as any).__isToolDefinition;
}
const TOOL_DEFINITION_MARKER = Symbol("__isToolDefinition");
const MAX_MCP_INSTRUCTIONS_LENGTH = 4000;
let sshCleanupRegistered = false;
async function cleanupSshResources(): Promise<void> {
const results = await Promise.allSettled([closeAllConnections(), unmountAll()]);
for (const result of results) {
if (result.status === "rejected") {
logger.warn("SSH cleanup failed", { error: String(result.reason) });
}
}
}
function registerSshCleanup(): void {
if (sshCleanupRegistered) return;
sshCleanupRegistered = true;
postmortem.register("ssh-cleanup", cleanupSshResources);
}
let pythonCleanupRegistered = false;
function registerPythonCleanup(): void {
if (pythonCleanupRegistered) return;
pythonCleanupRegistered = true;
postmortem.register("python-cleanup", disposeAllKernelSessions);
}
* Resolve whether to enable append-only context mode based on the setting and provider.
*
* - `"on"` → always enable
* - `"off"` → never enable
* - `"auto"` → enable for DeepSeek (prefix-caching provider)
*/
function resolveAppendOnlyMode(setting: "auto" | "on" | "off" | undefined, provider: string): boolean {
switch (setting ?? "auto") {
case "on":
return true;
case "off":
return false;
default:
return provider === "deepseek";
}
}
function customToolToDefinition(tool: CustomTool): ToolDefinition {
const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
name: tool.name,
label: tool.label,
description: tool.description,
parameters: tool.parameters,
hidden: tool.hidden,
deferrable: tool.deferrable,
mcpServerName: tool.mcpServerName,
mcpToolName: tool.mcpToolName,
execute: (toolCallId, params, signal, onUpdate, ctx) =>
tool.execute(toolCallId, params, onUpdate, createCustomToolContext(ctx), signal),
onSession: tool.onSession ? (event, ctx) => tool.onSession?.(event, createCustomToolContext(ctx)) : undefined,
renderCall: tool.renderCall,
renderResult: tool.renderResult
? (result, options, theme): Component => {
const component = tool.renderResult?.(
result,
{ expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
theme,
);
return component ?? ({ render: () => [] } as unknown as Component);
}
: undefined,
[TOOL_DEFINITION_MARKER]: true,
};
return definition;
}
function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
return api => {
for (const tool of tools) {
api.registerTool(customToolToDefinition(tool));
}
const runOnSession = async (event: CustomToolSessionEvent, ctx: ExtensionContext) => {
for (const tool of tools) {
if (!tool.onSession) continue;
try {
await tool.onSession(event, createCustomToolContext(ctx));
} catch (err) {
logger.warn("Custom tool onSession error", { tool: tool.name, error: String(err) });
}
}
};
api.on("session_start", async (_event, ctx) =>
runOnSession({ reason: "start", previousSessionFile: undefined }, ctx),
);
api.on("session_switch", async (event, ctx) =>
runOnSession({ reason: "switch", previousSessionFile: event.previousSessionFile }, ctx),
);
api.on("session_branch", async (event, ctx) =>
runOnSession({ reason: "branch", previousSessionFile: event.previousSessionFile }, ctx),
);
api.on("session_tree", async (_event, ctx) =>
runOnSession({ reason: "tree", previousSessionFile: undefined }, ctx),
);
api.on("session_shutdown", async (_event, ctx) =>
runOnSession({ reason: "shutdown", previousSessionFile: undefined }, ctx),
);
api.on("auto_compaction_start", async (event, ctx) =>
runOnSession({ reason: "auto_compaction_start", trigger: event.reason, action: event.action }, ctx),
);
api.on("auto_compaction_end", async (event, ctx) =>
runOnSession(
{
reason: "auto_compaction_end",
action: event.action,
result: event.result,
aborted: event.aborted,
willRetry: event.willRetry,
errorMessage: event.errorMessage,
},
ctx,
),
);
api.on("auto_retry_start", async (event, ctx) =>
runOnSession(
{
reason: "auto_retry_start",
attempt: event.attempt,
maxAttempts: event.maxAttempts,
delayMs: event.delayMs,
errorMessage: event.errorMessage,
},
ctx,
),
);
api.on("auto_retry_end", async (event, ctx) =>
runOnSession(
{
reason: "auto_retry_end",
success: event.success,
attempt: event.attempt,
finalError: event.finalError,
},
ctx,
),
);
api.on("ttsr_triggered", async (event, ctx) =>
runOnSession({ reason: "ttsr_triggered", rules: event.rules }, ctx),
);
api.on("todo_reminder", async (event, ctx) =>
runOnSession(
{
reason: "todo_reminder",
todos: event.todos,
attempt: event.attempt,
maxAttempts: event.maxAttempts,
},
ctx,
),
);
};
}
* Build LoadedCustomCommand entries for all MCP prompts across connected servers.
* These are re-created whenever prompts change (setOnPromptsChanged callback).
*/
function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] {
const commands: LoadedCustomCommand[] = [];
for (const serverName of manager.getConnectedServers()) {
const prompts = manager.getServerPrompts(serverName);
if (!prompts?.length) continue;
for (const prompt of prompts) {
const commandName = `${serverName}:${prompt.name}`;
commands.push({
path: `mcp:${commandName}`,
resolvedPath: `mcp:${commandName}`,
source: "bundled",
command: {
name: commandName,
description: prompt.description ?? `MCP prompt from ${serverName}`,
async execute(args: string[]) {
const promptArgs: Record<string, string> = {};
for (const arg of args) {
const eqIdx = arg.indexOf("=");
if (eqIdx > 0) {
promptArgs[arg.slice(0, eqIdx)] = arg.slice(eqIdx + 1);
}
}
const result = await manager.executePrompt(serverName, prompt.name, promptArgs);
if (!result) return "";
const parts: string[] = [];
for (const msg of result.messages) {
const contentItems = Array.isArray(msg.content) ? msg.content : [msg.content];
for (const item of contentItems) {
if (item.type === "text") {
parts.push(item.text);
} else if (item.type === "resource") {
const resource = item.resource;
if (resource.text) parts.push(resource.text);
}
}
}
return parts.join("\n\n");
},
},
});
}
}
return commands;
}
* Create an AgentSession with the specified options.
*
* @example
* ```typescript
* // Minimal - uses defaults
* const { session } = await createAgentSession();
*
* // With explicit model
* import { getModel } from '@oh-my-pi/pi-ai';
* const { session } = await createAgentSession({
* model: getModel('anthropic', 'claude-opus-4-5'),
* thinkingLevel: 'high',
* });
*
* // Continue previous session
* const { session, modelFallbackMessage } = await createAgentSession({
* continueSession: true,
* });
*
* // Full control
* const { session } = await createAgentSession({
* model: myModel,
* getApiKey: async () => Bun.env.MY_KEY,
* systemPrompt: ['You are helpful.'],
* tools: codingTools({ cwd: getProjectDir() }),
* skills: [],
* sessionManager: SessionManager.inMemory(),
* });
* ```
*/
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
const cwd = options.cwd ?? getProjectDir();
const agentDir = options.agentDir ?? getDefaultAgentDir();
const eventBus = options.eventBus ?? new EventBus();
registerSshCleanup();
registerPythonCleanup();
const modelRegistry =
options.modelRegistry ??
new ModelRegistry(options.authStorage ?? (await logger.time("discoverModels", discoverAuthStorage, agentDir)));
const authStorage = modelRegistry.authStorage;
if (options.authStorage && options.authStorage !== authStorage) {
throw new Error(
"options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
);
}
const startupCredentialDisabledEvents: CredentialDisabledEvent[] = [];
let credentialDisabledTarget: ExtensionRunner | undefined;
const unsubscribeCredentialDisabled: (() => void) | undefined = authStorage.onCredentialDisabled(event => {
if (credentialDisabledTarget) {
void credentialDisabledTarget.emitCredentialDisabled(event);
} else {
startupCredentialDisabledEvents.push(event);
}
});
const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir }));
logger.time("initializeWithSettings", initializeWithSettings, settings);
if (!options.modelRegistry) {
modelRegistry.refreshInBackground();
}
const STARTUP_SCAN_DEADLINE_MS = 5000;
const workspaceTreePromise: Promise<WorkspaceTree> = options.workspaceTree
? Promise.resolve(options.workspaceTree)
: logger.time("buildWorkspaceTree", () => buildWorkspaceTree(cwd, { timeoutMs: STARTUP_SCAN_DEADLINE_MS }));
workspaceTreePromise.catch(() => {});
const contextFilesPromise = options.contextFiles
? Promise.resolve(options.contextFiles)
: logger.time("discoverContextFiles", discoverContextFiles, cwd, agentDir);
contextFilesPromise.catch(() => {});
const promptTemplatesPromise = options.promptTemplates
? Promise.resolve(options.promptTemplates)
: logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir);
promptTemplatesPromise.catch(() => {});
const slashCommandsPromise = options.slashCommands
? Promise.resolve(options.slashCommands)
: logger.time("discoverSlashCommands", discoverSlashCommands, cwd);
slashCommandsPromise.catch(() => {});
const skillsSettings = settings.getGroup("skills");
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
const discoveredSkillsPromise =
options.skills === undefined
? logger.time("discoverSkills", discoverSkills, cwd, agentDir, {
...skillsSettings,
disabledExtensions: disabledExtensionIds,
})
: undefined;
discoveredSkillsPromise?.catch(() => {});
const webSearchProvider = settings.get("providers.webSearch");
if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
setPreferredSearchProvider(webSearchProvider);
}
const imageProvider = settings.get("providers.image");
if (isImageProviderPreference(imageProvider)) {
setPreferredImageProvider(imageProvider);
}
const sessionManager =
options.sessionManager ??
logger.time("sessionManager", () =>
SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
);
const providerSessionId = options.providerSessionId ?? sessionManager.getSessionId();
const modelApiKeyAvailability = new Map<string, boolean>();
const getModelAvailabilityKey = (candidate: Model): string =>
`${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
const hasModelApiKey = async (candidate: Model): Promise<boolean> => {
const availabilityKey = getModelAvailabilityKey(candidate);
const cached = modelApiKeyAvailability.get(availabilityKey);
if (cached !== undefined) {
return cached;
}
const hasKey = !!(await modelRegistry.getApiKey(candidate, providerSessionId));
modelApiKeyAvailability.set(availabilityKey, hasKey);
return hasKey;
};
let obfuscator: SecretObfuscator | undefined;
if (settings.get("secrets.enabled")) {
const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
const envEntries = collectEnvSecrets();
const allEntries = [...envEntries, ...fileEntries];
if (allEntries.length > 0) {
obfuscator = new SecretObfuscator(allEntries);
}
}
const secretsEnabled = obfuscator?.hasSecrets() === true;
const existingSession = logger.time("loadSessionContext", () =>
deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
);
const existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
const hasExistingSession = existingBranch.length > 0;
const hasThinkingEntry = existingBranch.some(entry => entry.type === "thinking_level_change");
const hasServiceTierEntry = existingBranch.some(entry => entry.type === "service_tier_change");
const hasExplicitModel = options.model !== undefined || options.modelPattern !== undefined;
const modelMatchPreferences = {
usageOrder: settings.getStorage()?.getModelUsageOrder(),
};
const allowedModels = await logger.time("resolveAllowedModels", () =>
resolveAllowedModels(modelRegistry, settings, modelMatchPreferences),
);
const defaultRoleSpec = logger.time("resolveDefaultModelRole", () =>
resolveModelRoleValue(settings.getModelRole("default"), allowedModels, {
settings,
matchPreferences: modelMatchPreferences,
modelRegistry,
}),
);
let model = options.model;
let modelFallbackMessage: string | undefined;
const defaultModelStr = existingSession.models.default;
if (!hasExplicitModel && !model && hasExistingSession && defaultModelStr) {
await logger.time("restoreSessionModel", async () => {
const parsedModel = parseModelString(defaultModelStr);
if (parsedModel) {
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
if (restoredModel && (await hasModelApiKey(restoredModel))) {
model = restoredModel;
}
}
if (!model) {
modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
}
});
}
if (!hasExplicitModel && !model && defaultRoleSpec.model) {
const settingsDefaultModel = defaultRoleSpec.model;
logger.time("resolveSettingsDefaultModel", () => {
model = settingsDefaultModel;
});
}
const taskDepth = options.taskDepth ?? 0;
let thinkingLevel = options.thinkingLevel;
if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) {
thinkingLevel = parseThinkingLevel(existingSession.thinkingLevel);
}
if (thinkingLevel === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) {
thinkingLevel = defaultRoleSpec.thinkingLevel;
}
if (thinkingLevel === undefined && model?.thinking?.defaultLevel !== undefined) {
thinkingLevel = model.thinking.defaultLevel;
}
if (thinkingLevel === undefined) {
thinkingLevel = settings.get("defaultThinkingLevel");
}
const autoThinking = thinkingLevel === AUTO_THINKING;
let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel;
if (model) {
const resolvedModel = model;
effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
autoThinking
? resolveProvisionalAutoLevel(resolvedModel)
: resolveThinkingLevelForModel(resolvedModel, effectiveThinkingLevel),
);
preconnectModelHost(model.baseUrl);
}
let skills: Skill[];
let skillWarnings: SkillWarning[];
if (options.skills !== undefined) {
skills = options.skills;
skillWarnings = [];
} else {
const discovered = await (discoveredSkillsPromise ?? Promise.resolve({ skills: [], warnings: [] }));
skills = discovered.skills;
skillWarnings = discovered.warnings;
}
const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time("discoverTtsrRules", async () => {
const ttsrSettings = settings.getGroup("ttsr");
const ttsrManager = new TtsrManager(ttsrSettings);
const rulesResult =
options.rules !== undefined
? { items: options.rules, warnings: undefined }
: await loadCapability<Rule>(ruleCapability.id, { cwd });
const { rulebookRules, alwaysApplyRules } = bucketRules(rulesResult.items, ttsrManager, {
builtinRules: ttsrSettings.builtinRules,
disabledRules: ttsrSettings.disabledRules,
});
if (existingSession.injectedTtsrRules.length > 0) {
ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
}
return { ttsrManager, rulebookRules, alwaysApplyRules };
});
const raceWithDeadline = async <T>(name: string, work: Promise<T>): Promise<T | undefined> => {
let timedOut = false;
const result = await Promise.race([
work,
Bun.sleep(STARTUP_SCAN_DEADLINE_MS).then(() => {
timedOut = true;
return undefined;
}),
]);
if (timedOut) {
logger.warn("Startup scan exceeded deadline; deferring to system prompt fallback", {
name,
timeoutMs: STARTUP_SCAN_DEADLINE_MS,
cwd,
});
}
return result;
};
const [contextFiles, resolvedWorkspaceTree] = await Promise.all([
contextFilesPromise,
raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
]);
let agent: Agent;
let session!: AgentSession;
let hasSession = false;
let hasRegistered = false;
const enableLsp = options.enableLsp ?? true;
const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
const ASYNC_PREVIEW_MAX_CHARS = 4_000;
const formatAsyncResultForFollowUp = async (result: string): Promise<string> => {
if (result.length <= ASYNC_INLINE_RESULT_MAX_CHARS) {
return result;
}
const preview = `${result.slice(0, ASYNC_PREVIEW_MAX_CHARS)}\n\n[Output truncated. Showing first ${ASYNC_PREVIEW_MAX_CHARS.toLocaleString()} characters.]`;
try {
const { path: artifactPath, id: artifactId } = await sessionManager.allocateArtifactPath("async");
if (artifactPath && artifactId) {
await Bun.write(artifactPath, result);
return `${preview}\nFull output: artifact://${artifactId}`;
}
} catch (error) {
logger.warn("Failed to persist async follow-up artifact", {
error: error instanceof Error ? error.message : String(error),
});
}
return preview;
};
const asyncJobManager =
backgroundJobsEnabled && !options.parentTaskPrefix
? new AsyncJobManager({
maxRunningJobs: asyncMaxJobs,
onJobComplete: async (jobId, result, job) => {
if (!session || asyncJobManager!.isDeliverySuppressed(jobId)) return;
const formattedResult = await formatAsyncResultForFollowUp(result);
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
jobId,
result: formattedResult,
job,
durationMs,
});
},
})
: undefined;
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
const resolvedAgentDisplayName =
options.agentDisplayName ?? ((options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main");
const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
try {
const getActiveModelString = (): string | undefined => {
const activeModel = agent?.state.model;
if (activeModel) return formatModelString(activeModel);
if (model) return formatModelString(model);
return undefined;
};
const toolSession: ToolSession = {
get cwd() {
return sessionManager.getCwd();
},
hasUI: options.hasUI ?? false,
enableLsp,
get hasEditTool() {
const requestedToolNames = options.toolNames
? [...new Set(options.toolNames.map(name => name.toLowerCase()))]
: undefined;
return !requestedToolNames || requestedToolNames.includes("edit");
},
skipPythonPreflight: options.skipPythonPreflight,
contextFiles,
workspaceTree: resolvedWorkspaceTree,
skills,
eventBus,
outputSchema: options.outputSchema,
requireYieldTool: options.requireYieldTool,
taskDepth: options.taskDepth ?? 0,
getSessionFile: () => sessionManager.getSessionFile() ?? null,
getEvalKernelOwnerId: () => evalKernelOwnerId,
getEvalSessionId: () =>
session?.getEvalSessionId() ?? options.parentEvalSessionId ?? defaultEvalSessionId(toolSession),
assertEvalExecutionAllowed: () => session?.assertEvalExecutionAllowed(),
trackEvalExecution: (execution, abortController) =>
session ? session.trackEvalExecution(execution, abortController) : execution,
getSessionId: () => sessionManager.getSessionId?.() ?? null,
getHindsightSessionState: () => session?.getHindsightSessionState(),
getMnemopiSessionState: () => getMnemopiSessionState(session),
getAgentId: () => resolvedAgentId,
getToolByName: name => session?.getToolByName(name),
agentRegistry,
getSessionSpawns: () => options.spawns ?? "*",
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
getActiveModelString,
getPlanModeState: () => session?.getPlanModeState(),
getGoalModeState: () => session?.getGoalModeState(),
getGoalRuntime: () => session?.goalRuntime,
getUsageStatistics: () => sessionManager.getUsageStatistics(),
getTurnBudget: () => sessionManager.getTurnBudget(),
recordEvalSubagentUsage: output => sessionManager.recordEvalSubagentOutput(output),
getClientBridge: () => session?.clientBridge,
getCompactContext: () => session.formatCompactContext(),
getTodoPhases: () => session.getTodoPhases(),
setTodoPhases: phases => session.setTodoPhases(phases),
isMCPDiscoveryEnabled: () => session.isMCPDiscoveryEnabled(),
getSelectedMCPToolNames: () => session.getSelectedMCPToolNames(),
activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
isToolDiscoveryEnabled: () => session.isToolDiscoveryEnabled(),
getDiscoverableTools: filter => session.getDiscoverableTools(filter),
getDiscoverableToolSearchIndex: () => session.getDiscoverableToolSearchIndex(),
getSelectedDiscoveredToolNames: () => session.getSelectedDiscoveredToolNames(),
activateDiscoveredTools: toolNames => session.activateDiscoveredTools(toolNames),
getCheckpointState: () => session.getCheckpointState(),
setCheckpointState: state => session.setCheckpointState(state ?? undefined),
getToolChoiceQueue: () => session.toolChoiceQueue,
buildToolChoice: name => {
const m = session.model;
return m ? buildNamedToolChoice(name, m) : undefined;
},
steer: msg =>
session.agent.steer({
role: "custom",
customType: msg.customType,
content: msg.content,
display: false,
details: msg.details,
attribution: "agent",
timestamp: Date.now(),
}),
peekQueueInvoker: () => session.peekQueueInvoker(),
peekStandingResolveHandler: () => session.peekStandingResolveHandler(),
setStandingResolveHandler: handler => session.setStandingResolveHandler(handler),
allocateOutputArtifact: async toolType => {
try {
return await sessionManager.allocateArtifactPath(toolType);
} catch {
return {};
}
},
getArtifactManager: () => sessionManager.getArtifactManager(),
settings,
authStorage,
modelRegistry,
getTelemetry: () => agent?.telemetry,
};
const getArtifactsDir = () => sessionManager.getArtifactsDir();
if (!options.parentTaskPrefix) {
setActiveSkills(skills);
setActiveRules([...rulebookRules, ...alwaysApplyRules]);
if (asyncJobManager) AsyncJobManager.setInstance(asyncJobManager);
}
const localProtocolOptions = options.localProtocolOptions ?? {
getArtifactsDir,
getSessionId: () => sessionManager.getSessionId?.() ?? null,
};
if (options.localProtocolOptions) {
LocalProtocolHandler.setOverride(options.localProtocolOptions);
}
toolSession.getArtifactsDir = getArtifactsDir;
toolSession.localProtocolOptions = localProtocolOptions;
toolSession.agentOutputManager = new AgentOutputManager(
getArtifactsDir,
options.parentTaskPrefix ? { parentPrefix: options.parentTaskPrefix } : undefined,
);
const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
let mcpManager: MCPManager | undefined = options.mcpManager;
toolSession.mcpManager = mcpManager;
const enableMCP = options.enableMCP ?? true;
const customTools: CustomTool[] = [];
if (enableMCP && !mcpManager) {
const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
onConnecting: serverNames => {
if (options.hasUI && serverNames.length > 0) {
process.stderr.write(`${chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…`)}\n`);
}
},
enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
filterExa: true,
filterBrowser: settings.get("browser.enabled") ?? false,
cacheStorage: settings.getStorage(),
authStorage,
});
mcpManager = mcpResult.manager;
toolSession.mcpManager = mcpManager;
if (settings.get("mcp.notifications")) {
mcpManager.setNotificationsEnabled(true);
}
if (mcpResult.exaApiKeys.length > 0 && !$env.EXA_API_KEY) {
Bun.env.EXA_API_KEY = mcpResult.exaApiKeys[0];
}
for (const { path, error } of mcpResult.errors) {
logger.error("MCP tool load failed", { path, error });
}
if (mcpResult.tools.length > 0) {
customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
}
}
if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
if (imageGenTools.length > 0) {
customTools.push(...(imageGenTools as unknown as CustomTool[]));
}
if (settings.get("tts.enabled")) {
customTools.push(ttsTool as unknown as CustomTool);
}
if (options.toolNames?.includes("web_search")) {
customTools.push(...getSearchTools());
}
const builtInToolNames = builtinTools.map(t => t.name);
const discoveredCustomTools = await logger.time(
"discoverAndLoadCustomTools",
discoverAndLoadCustomTools,
[],
cwd,
builtInToolNames,
action => queueResolveHandler(toolSession, action),
);
for (const { path, error } of discoveredCustomTools.errors) {
logger.error("Custom tool load failed", { path, error });
}
if (discoveredCustomTools.tools.length > 0) {
customTools.push(...discoveredCustomTools.tools.map(loaded => loaded.tool));
}
const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
inlineExtensions.push(createAutoresearchExtension);
if (customTools.length > 0) {
inlineExtensions.push(createCustomToolsExtension(customTools));
}
const extensionsResult: LoadExtensionsResult =
options.preloadedExtensions ?? (await loadSessionExtensions(options, cwd, settings, eventBus));
if (inlineExtensions.length > 0) {
for (let i = 0; i < inlineExtensions.length; i++) {
const factory = inlineExtensions[i];
const loaded = await loadExtensionFromFactory(
factory,
cwd,
eventBus,
extensionsResult.runtime,
`<inline-${i}>`,
);
extensionsResult.extensions.push(loaded);
}
}
const activeExtensionSources = extensionsResult.extensions.map(extension => extension.path);
modelRegistry.syncExtensionSources(activeExtensionSources);
for (const sourceId of new Set(activeExtensionSources)) {
modelRegistry.clearSourceRegistrations(sourceId);
}
if (extensionsResult.runtime.pendingProviderRegistrations.length > 0) {
for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
modelRegistry.registerProvider(name, config, sourceId);
}
extensionsResult.runtime.pendingProviderRegistrations = [];
}
if (!model && options.modelPattern) {
const availableModels = modelRegistry.getAll();
const matchPreferences = {
usageOrder: settings.getStorage()?.getModelUsageOrder(),
};
const { model: resolved } = parseModelPattern(options.modelPattern, availableModels, matchPreferences, {
modelRegistry,
});
if (resolved) {
model = resolved;
modelFallbackMessage = undefined;
} else {
modelFallbackMessage = `Model "${options.modelPattern}" not found`;
}
}
if (!model && !options.modelPattern) {
const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
for (const candidate of fallbackCandidates) {
if (await hasModelApiKey(candidate)) {
model = candidate;
break;
}
}
if (model) {
if (modelFallbackMessage) {
modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
}
} else {
const patterns = settings.get("enabledModels");
modelFallbackMessage =
patterns && patterns.length > 0
? `No model available matching enabledModels (${patterns.join(", ")}) with usable credentials. Configure auth for an allowed provider or adjust enabledModels.`
: "No models available. Use /login or set an API key environment variable. Then use /model to select a model.";
}
}
const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
? { commands: [], errors: [] }
: await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
if (!options.disableExtensionDiscovery) {
for (const { path, error } of customCommandsResult.errors) {
logger.error("Failed to load custom command", { path, error });
}
}
const extensionRunner: ExtensionRunner = new ExtensionRunner(
extensionsResult.extensions,
extensionsResult.runtime,
cwd,
sessionManager,
modelRegistry,
);
credentialDisabledTarget = extensionRunner;
for (const event of startupCredentialDisabledEvents.splice(0)) {
void extensionRunner.emitCredentialDisabled(event);
}
const getSessionContext = () => ({
sessionManager,
modelRegistry,
model: agent.state.model,
isIdle: () => !session.isStreaming,
hasQueuedMessages: () => session.queuedMessageCount > 0,
abort: () => {
session.abort();
},
settings,
autoApprove: options.autoApprove ?? false,
});
const toolContextStore = new ToolContextStore(getSessionContext);
const registeredTools = extensionRunner.getAllRegisteredTools();
const allCustomTools = [
...registeredTools,
...(options.customTools?.map(tool => {
const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
return { definition, extensionPath: "<sdk>" };
}) ?? []),
];
const wrappedExtensionTools: Tool[] = wrapRegisteredTools(allCustomTools, extensionRunner);
const toolRegistry = new Map<string, Tool>();
for (const tool of builtinTools) {
toolRegistry.set(tool.name, tool);
}
if (!toolRegistry.has("goal") && settings.get("goal.enabled")) {
const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
if (goalTool) {
toolRegistry.set(goalTool.name, wrapToolWithMetaNotice(goalTool));
}
}
for (const tool of wrappedExtensionTools) {
toolRegistry.set(tool.name, tool);
}
for (const tool of toolRegistry.values()) {
toolRegistry.set(tool.name, new ExtensionToolWrapper(tool, extensionRunner));
}
if (model?.provider === "cursor") {
toolRegistry.delete("edit");
}
const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
const planModeAvailable = settings.get("plan.enabled");
const needsResolveTool = hasDeferrableTools || planModeAvailable;
if (!needsResolveTool) {
toolRegistry.delete("resolve");
} else if (!toolRegistry.has("resolve")) {
const resolveTool = await logger.time("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
if (resolveTool) {
toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
}
}
const reloadSshTool = async (): Promise<AgentTool | null> => {
if (!requestedToolNameSet.has("ssh")) return null;
const sshTool = (await loadSshTool({
...toolSession,
cwd: sessionManager.getCwd(),
})) as unknown as AgentTool | null;
if (!sshTool) return null;
const wrapped = wrapToolWithMetaNotice(sshTool);
return new ExtensionToolWrapper(wrapped, extensionRunner) as AgentTool;
};
let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
const cursorExecHandlers = new CursorExecHandlers({
cwd,
tools: toolRegistry,
getToolContext: () => toolContextStore.getContext(),
emitEvent: event => cursorEventEmitter?.(event),
});
const repeatToolDescriptions = settings.get("repeatToolDescriptions");
const eagerTasks = settings.get("task.eager");
const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
const rebuildSystemPrompt = async (
toolNames: string[],
tools: Map<string, AgentTool>,
): Promise<BuildSystemPromptResult> => {
toolContextStore.setToolNames(toolNames);
const discoverableMCPTools: DiscoverableTool[] = mcpDiscoveryEnabled
? filterBySource(collectDiscoverableTools(tools.values()), "mcp")
: [];
const activeToolNames = new Set(toolNames);
const discoverableBuiltinTools: DiscoverableTool[] =
effectiveDiscoveryMode === "all"
? collectDiscoverableTools(
Array.from(tools.values()).filter(
tool => tool.loadMode === "discoverable" && !activeToolNames.has(tool.name),
),
{ source: "builtin" },
)
: [];
const discoverableToolsForDesc: DiscoverableTool[] = [...discoverableBuiltinTools, ...discoverableMCPTools];
const discoverableToolSummary = summarizeDiscoverableTools(discoverableToolsForDesc);
const hasDiscoverableTools =
mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableToolsForDesc.length > 0;
const promptTools = buildSystemPromptToolMetadata(tools, {
search_tool_bm25: { description: renderSearchToolBm25Description(discoverableToolsForDesc) },
});
const memoryBackend = resolveMemoryBackend(settings);
const memoryInstructions = await memoryBackend.buildDeveloperInstructions(agentDir, settings, session);
const serverInstructions = mcpManager?.getServerInstructions();
let appendPrompt: string | undefined = memoryInstructions ?? undefined;
if (serverInstructions && serverInstructions.size > 0) {
const parts: string[] = [];
if (appendPrompt) parts.push(appendPrompt);
parts.push(
"## MCP Server Instructions\n\nThe following instructions are provided by connected MCP servers. They are server-controlled and may not be verified.",
);
for (const [srvName, srvInstructions] of serverInstructions) {
const truncated =
srvInstructions.length > MAX_MCP_INSTRUCTIONS_LENGTH
? `${srvInstructions.slice(0, MAX_MCP_INSTRUCTIONS_LENGTH)}\n[truncated]`
: srvInstructions;
parts.push(`### ${srvName}\n${truncated}`);
}
appendPrompt = parts.join("\n\n");
}
const defaultPrompt = await buildSystemPromptInternal({
cwd,
skills,
contextFiles,
tools: promptTools,
toolNames,
rules: rulebookRules,
alwaysApplyRules,
skillsSettings: settings.getGroup("skills"),
appendSystemPrompt: appendPrompt,
repeatToolDescriptions,
intentField,
mcpDiscoveryMode: hasDiscoverableTools,
mcpDiscoveryServerSummaries: discoverableToolSummary.servers.map(formatDiscoverableToolServerSummary),
eagerTasks,
secretsEnabled,
workspaceTree: workspaceTreePromise,
memoryRootEnabled: memoryBackend.id === "local",
});
if (options.systemPrompt === undefined) {
return defaultPrompt;
}
if (Array.isArray(options.systemPrompt)) {
return { systemPrompt: options.systemPrompt };
}
return {
systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
};
};
const toolNamesFromRegistry = Array.from(toolRegistry.keys());
const explicitlyRequestedToolNames = options.toolNames
? [...new Set(options.toolNames.map(name => name.toLowerCase()))]
: undefined;
if (
options.requireYieldTool === true &&
explicitlyRequestedToolNames &&
!explicitlyRequestedToolNames.includes("yield")
) {
explicitlyRequestedToolNames.push("yield");
}
const requestedToolNames = explicitlyRequestedToolNames ?? toolNamesFromRegistry;
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
const requestedToolNameSet = new Set(normalizedRequested);
const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
toolsDiscoveryModeSetting !== "off"
? (toolsDiscoveryModeSetting as "off" | "mcp-only" | "all")
: settings.get("mcp.discoveryMode")
? "mcp-only"
: "off";
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off";
const defaultInactiveToolNames = new Set(
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
);
const requestedActiveToolNames = normalizedRequested.filter(name => name !== "goal");
const initialRequestedActiveToolNames = options.toolNames
? requestedActiveToolNames
: requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
const explicitlyRequestedMCPToolNames = options.toolNames
? requestedActiveToolNames.filter(name => name.startsWith("mcp__"))
: [];
const discoveryDefaultServers = new Set(
(settings.get("mcp.discoveryDefaultServers") ?? []).map(serverName => serverName.trim()).filter(Boolean),
);
const discoveryDefaultServerToolNames = mcpDiscoveryEnabled
? selectDiscoverableToolNamesByServer(
filterBySource(collectDiscoverableTools(toolRegistry.values()), "mcp"),
discoveryDefaultServers,
)
: [];
let initialSelectedMCPToolNames: string[] = [];
let defaultSelectedMCPToolNames: string[] = [];
let initialToolNames = [...initialRequestedActiveToolNames];
if (mcpDiscoveryEnabled) {
const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(name =>
toolRegistry.has(name),
);
defaultSelectedMCPToolNames = [
...new Set([...discoveryDefaultServerToolNames, ...explicitlyRequestedMCPToolNames]),
];
initialSelectedMCPToolNames = existingSession.hasPersistedMCPToolSelection
? restoredSelectedMCPToolNames
: [...new Set([...restoredSelectedMCPToolNames, ...defaultSelectedMCPToolNames])];
initialToolNames = [
...new Set([
...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp__")),
...initialSelectedMCPToolNames,
]),
];
}
const alwaysInclude: string[] = [
...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
];
for (const name of alwaysInclude) {
if (mcpDiscoveryEnabled && name.startsWith("mcp__")) {
continue;
}
if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
initialToolNames.push(name);
}
}
if (effectiveDiscoveryMode === "all") {
const essentialBuiltinNames = new Set(computeEssentialBuiltinNames(settings));
const explicitlyRequestedToolNames = new Set(options.toolNames?.map(name => name.toLowerCase()) ?? []);
const restoredDiscoveredNames = new Set(existingSession.selectedMCPToolNames);
initialToolNames = initialToolNames.filter(name => {
const tool = toolRegistry.get(name);
if (!tool?.loadMode) return true;
if (tool.loadMode === "essential") return true;
if (essentialBuiltinNames.has(name)) return true;
if (explicitlyRequestedToolNames.has(name)) return true;
if (restoredDiscoveredNames.has(name)) return true;
return false;
});
}
agentRegistry.register({
id: resolvedAgentId,
displayName: resolvedAgentDisplayName,
kind: (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main",
parentId: options.parentTaskPrefix,
session: null,
sessionFile: sessionManager.getSessionFile() ?? null,
status: "running",
});
hasRegistered = true;
const { systemPrompt } = await logger.time(
"buildSystemPrompt",
rebuildSystemPrompt,
initialToolNames,
toolRegistry,
);
const promptTemplates = await promptTemplatesPromise;
toolSession.promptTemplates = promptTemplates;
const slashCommands = await slashCommandsPromise;
const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
const converted = convertToLlm(messages);
if (!settings.get("images.blockImages")) {
return converted;
}
return converted.map(msg => {
if (msg.role === "user" || msg.role === "toolResult") {
const content = msg.content;
if (Array.isArray(content)) {
const hasImages = content.some(c => c.type === "image");
if (hasImages) {
const filteredContent = content
.map(c =>
c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c,
)
.filter((c, i, arr) => {
if (!(c.type === "text" && c.text === "Image reading is disabled." && i > 0)) return true;
const prev = arr[i - 1];
return !(prev.type === "text" && prev.text === "Image reading is disabled.");
});
return { ...msg, content: filteredContent };
}
}
}
return msg;
});
};
const convertToLlmFinal = (messages: AgentMessage[]): Message[] => {
const converted = convertToLlmWithBlockImages(messages);
if (!obfuscator?.hasSecrets()) return converted;
return obfuscateMessages(obfuscator, converted);
};
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
return await extensionRunner.emitContext(messages);
};
const onPayload = async (payload: unknown, _model?: Model) => {
return await extensionRunner.emitBeforeProviderRequest(payload);
};
const onResponse: SimpleStreamOptions["onResponse"] = async (response, model) => {
await extensionRunner.emitAfterProviderResponse(response, model);
};
const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
toolContextStore.setUIContext(uiContext, hasUI);
};
const initialTools = initialToolNames
.map(name => toolRegistry.get(name))
.filter((tool): tool is AgentTool => tool !== undefined);
const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "off";
const preferOpenAICodexWebsockets =
openaiWebsocketSetting === "on" ? true : openaiWebsocketSetting === "off" ? false : undefined;
const serviceTierSetting = settings.get("serviceTier");
const initialServiceTier = hasServiceTierEntry
? existingSession.serviceTier
: serviceTierSetting === "none"
? undefined
: serviceTierSetting;
agent = new Agent({
initialState: {
systemPrompt,
model,
thinkingLevel: toReasoningEffort(effectiveThinkingLevel),
tools: initialTools,
},
convertToLlm: convertToLlmFinal,
onPayload,
onResponse,
sessionId: providerSessionId,
transformContext,
steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
interruptMode: settings.get("interruptMode") ?? "immediate",
thinkingBudgets: settings.getGroup("thinkingBudgets"),
temperature: settings.get("temperature") >= 0 ? settings.get("temperature") : undefined,
topP: settings.get("topP") >= 0 ? settings.get("topP") : undefined,
topK: settings.get("topK") >= 0 ? settings.get("topK") : undefined,
minP: settings.get("minP") >= 0 ? settings.get("minP") : undefined,
presencePenalty: settings.get("presencePenalty") >= 0 ? settings.get("presencePenalty") : undefined,
repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
serviceTier: initialServiceTier,
hideThinkingSummary: settings.get("hideThinkingBlock"),
kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
preferWebsockets: preferOpenAICodexWebsockets,
getToolContext: tc => toolContextStore.getContext(tc),
getApiKey: async provider => {
const key = await modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
if (!key) {
throw new Error(`No API key found for provider "${provider}"`);
}
return key;
},
streamFn: (streamModel, context, streamOptions) => {
const openrouterRoutingPreset = settings.get("providers.openrouterVariant");
const openrouterVariant =
openrouterRoutingPreset && openrouterRoutingPreset !== "default" ? openrouterRoutingPreset : undefined;
return streamSimple(streamModel, context, {
...streamOptions,
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
onAuthError: async (provider, oldKey, error) => {
const message = error instanceof Error ? error.message : String(error);
if (isUsageLimitError(message)) {
const retryAfterMs = extractRetryHint(undefined, message);
const switched = await modelRegistry.authStorage.markUsageLimitReached(provider, agent.sessionId, {
retryAfterMs,
signal: streamOptions?.signal,
});
logger.debug("Retrying provider request after usage-limit block", {
provider,
switched,
retryAfterMs,
error: message,
});
if (!switched) return undefined;
return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
}
await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, {
signal: streamOptions?.signal,
sessionId: agent.sessionId,
});
logger.debug("Retrying provider request after credential invalidation", {
provider,
error: message,
});
return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
},
});
},
cursorExecHandlers,
transformToolCallArguments: (args, _toolName) => {
let result = args;
const maxTimeout = settings.get("tools.maxTimeout");
if (maxTimeout > 0 && typeof result.timeout === "number") {
result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
}
if (obfuscator?.hasSecrets()) {
result = obfuscator.deobfuscateObject(result);
}
return result;
},
intentTracing: !!intentField,
getToolChoice: () => session?.nextToolChoice(),
telemetry: options.telemetry,
appendOnlyContext: model
? resolveAppendOnlyMode(settings.get("provider.appendOnlyContext"), model.provider)
? new AppendOnlyContextManager()
: undefined
: undefined,
});
cursorEventEmitter = event => agent.emitExternalEvent(event);
if (hasExistingSession) {
agent.replaceMessages(existingSession.messages);
} else {
if (model) {
sessionManager.appendModelChange(`${model.provider}/${model.id}`);
}
if (!autoThinking) {
sessionManager.appendThinkingLevelChange(effectiveThinkingLevel);
}
if (initialServiceTier) {
sessionManager.appendServiceTierChange(initialServiceTier);
}
}
session = new AgentSession({
agent,
thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
sessionManager,
settings,
evalKernelOwnerId,
ownedAsyncJobManager: asyncJobManager,
scopedModels: options.scopedModels,
promptTemplates,
slashCommands,
extensionRunner,
customCommands: customCommandsResult.commands,
skills,
skillWarnings,
skillsSettings: settings.getGroup("skills"),
modelRegistry,
toolRegistry,
transformContext,
onPayload,
onResponse,
convertToLlm: convertToLlmFinal,
rebuildSystemPrompt,
reloadSshTool,
requestedToolNames: requestedToolNameSet,
getMcpServerInstructions: mcpManager
? () => {
const raw = mcpManager.getServerInstructions();
if (!raw || raw.size === 0) return raw;
const out = new Map<string, string>();
for (const [name, text] of raw) {
out.set(
name,
text.length > MAX_MCP_INSTRUCTIONS_LENGTH ? text.slice(0, MAX_MCP_INSTRUCTIONS_LENGTH) : text,
);
}
return out;
}
: undefined,
mcpDiscoveryEnabled,
initialSelectedMCPToolNames,
defaultSelectedMCPToolNames,
persistInitialMCPToolSelection: !hasExistingSession,
defaultSelectedMCPServerNames: [...discoveryDefaultServers],
ttsrManager,
obfuscator,
agentId: resolvedAgentId,
agentRegistry,
providerSessionId: options.providerSessionId,
parentEvalSessionId: options.parentEvalSessionId,
});
hasSession = true;
if (asyncJobManager) {
session.yieldQueue.register<AsyncResultEntry>("async-result", {
isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId),
build: buildAsyncResultBatchMessage,
});
}
session.yieldQueue.register<McpNotificationEntry>("mcp-notification", {
build: buildMcpNotificationBatchMessage,
});
agentRegistry.attachSession(resolvedAgentId, session, sessionManager.getSessionFile() ?? null);
{
const originalDispose = session.dispose.bind(session);
session.dispose = async () => {
try {
await originalDispose();
} finally {
agentRegistry.unregister(resolvedAgentId);
unsubscribeCredentialDisabled?.();
}
};
}
if (model?.api === "openai-codex-responses") {
const codexModel = model;
const codexTransport = getOpenAICodexTransportDetails(codexModel, {
sessionId: providerSessionId,
baseUrl: codexModel.baseUrl,
preferWebsockets: preferOpenAICodexWebsockets,
providerSessionState: session.providerSessionState,
});
if (codexTransport.websocketPreferred) {
void (async () => {
try {
const codexPrewarmApiKey = await modelRegistry.getApiKey(codexModel, providerSessionId);
if (!codexPrewarmApiKey) return;
await logger.time("prewarmOpenAICodexResponses", prewarmOpenAICodexResponses, codexModel, {
apiKey: codexPrewarmApiKey,
sessionId: providerSessionId,
preferWebsockets: preferOpenAICodexWebsockets,
providerSessionState: session.providerSessionState,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.debug("Codex websocket prewarm failed", {
error: errorMessage,
provider: codexModel.provider,
model: codexModel.id,
});
}
})();
}
}
let lspServers: CreateAgentSessionResult["lspServers"];
if (enableLsp && options.hasUI && settings.get("lsp.diagnosticsOnWrite")) {
lspServers = discoverStartupLspServers(cwd);
if (lspServers.length > 0) {
void (async () => {
try {
const result = await logger.time("warmupLspServers", warmupLspServers, cwd);
const serversByName = new Map(result.servers.map(server => [server.name, server] as const));
for (const server of lspServers ?? []) {
const next = serversByName.get(server.name);
if (!next) continue;
server.status = next.status;
server.fileTypes = next.fileTypes;
server.error = next.error;
}
const event: LspStartupEvent = {
type: "completed",
servers: result.servers,
};
eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.warn("LSP server warmup failed", { cwd, error: errorMessage });
for (const server of lspServers ?? []) {
server.status = "error";
server.error = errorMessage;
}
const event: LspStartupEvent = {
type: "failed",
error: errorMessage,
};
eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
}
})();
}
}
logger.time("startMemoryStartupTask", () =>
Promise.resolve(
resolveMemoryBackend(settings).start({
session,
settings,
modelRegistry,
agentDir,
taskDepth,
parentHindsightSessionState: options.parentHindsightSessionState,
parentMnemopiSessionState: options.parentMnemopiSessionState,
}),
),
);
if (mcpManager && !options.mcpManager) {
mcpManager.setOnToolsChanged(tools => {
void session.refreshMCPTools(tools);
});
mcpManager.setOnPromptsChanged(serverName => {
const promptCommands = buildMCPPromptCommands(mcpManager);
session.setMCPPromptCommands(promptCommands);
logger.debug("MCP prompt commands refreshed", { path: `mcp:${serverName}` });
});
const notificationDebounceTimers = new Map<string, Timer>();
const clearDebounceTimers = () => {
for (const timer of notificationDebounceTimers.values()) clearTimeout(timer);
notificationDebounceTimers.clear();
};
postmortem.register("mcp-notification-cleanup", clearDebounceTimers);
mcpManager.setOnResourcesChanged((serverName, uri) => {
logger.debug("MCP resources changed", { path: `mcp:${serverName}`, uri });
if (!settings.get("mcp.notifications")) return;
const debounceMs = settings.get("mcp.notificationDebounceMs");
const key = `${serverName}:${uri}`;
const existing = notificationDebounceTimers.get(key);
if (existing) clearTimeout(existing);
notificationDebounceTimers.set(
key,
setTimeout(() => {
notificationDebounceTimers.delete(key);
if (!settings.get("mcp.notifications")) return;
session.yieldQueue.enqueue<McpNotificationEntry>("mcp-notification", { serverName, uri });
}, debounceMs),
);
});
}
return {
session,
extensionsResult,
setToolUIContext,
mcpManager,
modelFallbackMessage,
lspServers,
eventBus,
};
} catch (error) {
unsubscribeCredentialDisabled?.();
try {
if (hasSession) {
await session.dispose();
} else {
if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
await disposeKernelSessionsByOwner(evalKernelOwnerId);
}
} catch (cleanupError) {
logger.warn("Failed to clean up createAgentSession resources after startup error", {
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
});
}
throw error;
}
}
* Best-effort preconnect to the model's API host. Bun's `fetch.preconnect`
* primes DNS + TCP + TLS + H2 so the first real request reuses the warm
* connection. Errors are swallowed: preconnect is an optimization, never a
* hard dependency.
*/
function preconnectModelHost(baseUrl: string | undefined): void {
if (!baseUrl) return;
const preconnect = (globalThis.fetch as typeof fetch & { preconnect?: (url: string) => void }).preconnect;
if (typeof preconnect !== "function") return;
try {
preconnect(baseUrl);
} catch {
}
}