@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/package.json +8 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +43 -10
  5. package/src/async/support.ts +5 -0
  6. package/src/cli/list-models.ts +96 -57
  7. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  8. package/src/commit/model-selection.ts +16 -13
  9. package/src/config/mcp-schema.json +1 -1
  10. package/src/config/model-equivalence.ts +675 -0
  11. package/src/config/model-registry.ts +242 -45
  12. package/src/config/model-resolver.ts +282 -65
  13. package/src/config/settings-schema.ts +27 -3
  14. package/src/config/settings.ts +1 -1
  15. package/src/cursor.ts +64 -23
  16. package/src/edit/index.ts +254 -89
  17. package/src/edit/modes/chunk.ts +336 -57
  18. package/src/edit/modes/hashline.ts +51 -26
  19. package/src/edit/modes/patch.ts +16 -10
  20. package/src/edit/modes/replace.ts +15 -7
  21. package/src/edit/renderer.ts +248 -94
  22. package/src/export/html/template.css +82 -0
  23. package/src/export/html/template.generated.ts +1 -1
  24. package/src/export/html/template.js +614 -97
  25. package/src/extensibility/custom-tools/types.ts +0 -3
  26. package/src/extensibility/extensions/loader.ts +16 -0
  27. package/src/extensibility/extensions/runner.ts +2 -7
  28. package/src/extensibility/extensions/types.ts +8 -4
  29. package/src/internal-urls/docs-index.generated.ts +4 -4
  30. package/src/internal-urls/jobs-protocol.ts +2 -1
  31. package/src/ipy/executor.ts +447 -52
  32. package/src/ipy/kernel.ts +39 -13
  33. package/src/lsp/client.ts +55 -1
  34. package/src/lsp/index.ts +8 -0
  35. package/src/lsp/types.ts +6 -0
  36. package/src/main.ts +6 -2
  37. package/src/memories/index.ts +7 -6
  38. package/src/modes/acp/acp-agent.ts +4 -1
  39. package/src/modes/components/bash-execution.ts +16 -4
  40. package/src/modes/components/model-selector.ts +221 -64
  41. package/src/modes/components/status-line/presets.ts +17 -6
  42. package/src/modes/components/status-line/segments.ts +15 -0
  43. package/src/modes/components/status-line-segment-editor.ts +1 -0
  44. package/src/modes/components/status-line.ts +7 -1
  45. package/src/modes/components/tool-execution.ts +145 -75
  46. package/src/modes/controllers/command-controller.ts +42 -1
  47. package/src/modes/controllers/event-controller.ts +4 -1
  48. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  49. package/src/modes/controllers/input-controller.ts +9 -3
  50. package/src/modes/controllers/selector-controller.ts +17 -6
  51. package/src/modes/interactive-mode.ts +19 -3
  52. package/src/modes/print-mode.ts +13 -4
  53. package/src/modes/prompt-action-autocomplete.ts +3 -5
  54. package/src/modes/rpc/rpc-mode.ts +8 -2
  55. package/src/modes/shared.ts +2 -2
  56. package/src/modes/types.ts +1 -0
  57. package/src/modes/utils/ui-helpers.ts +1 -0
  58. package/src/prompts/system/system-prompt.md +5 -1
  59. package/src/prompts/tools/bash.md +16 -1
  60. package/src/prompts/tools/cancel-job.md +1 -1
  61. package/src/prompts/tools/chunk-edit.md +191 -163
  62. package/src/prompts/tools/hashline.md +11 -11
  63. package/src/prompts/tools/patch.md +10 -5
  64. package/src/prompts/tools/{await.md → poll.md} +1 -1
  65. package/src/prompts/tools/read-chunk.md +12 -3
  66. package/src/prompts/tools/read.md +9 -0
  67. package/src/prompts/tools/task.md +2 -2
  68. package/src/prompts/tools/vim.md +98 -0
  69. package/src/prompts/tools/write.md +1 -0
  70. package/src/sdk.ts +758 -725
  71. package/src/session/agent-session.ts +187 -40
  72. package/src/session/session-manager.ts +50 -4
  73. package/src/slash-commands/builtin-registry.ts +17 -0
  74. package/src/task/executor.ts +9 -5
  75. package/src/task/index.ts +3 -5
  76. package/src/task/types.ts +2 -2
  77. package/src/tools/bash.ts +240 -57
  78. package/src/tools/cancel-job.ts +2 -1
  79. package/src/tools/find.ts +5 -2
  80. package/src/tools/grep.ts +77 -8
  81. package/src/tools/index.ts +48 -19
  82. package/src/tools/inspect-image.ts +1 -1
  83. package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
  84. package/src/tools/python.ts +293 -278
  85. package/src/tools/read.ts +218 -1
  86. package/src/tools/sqlite-reader.ts +623 -0
  87. package/src/tools/submit-result.ts +5 -2
  88. package/src/tools/todo-write.ts +8 -2
  89. package/src/tools/vim.ts +966 -0
  90. package/src/tools/write.ts +187 -1
  91. package/src/utils/commit-message-generator.ts +1 -0
  92. package/src/utils/edit-mode.ts +2 -1
  93. package/src/utils/git.ts +24 -1
  94. package/src/utils/session-color.ts +55 -0
  95. package/src/utils/title-generator.ts +16 -7
  96. package/src/vim/buffer.ts +309 -0
  97. package/src/vim/commands.ts +382 -0
  98. package/src/vim/engine.ts +2426 -0
  99. package/src/vim/parser.ts +151 -0
  100. package/src/vim/render.ts +252 -0
  101. package/src/vim/types.ts +197 -0
package/src/sdk.ts CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  getOpenAICodexTransportDetails,
12
12
  prewarmOpenAICodexResponses,
13
13
  } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
14
- import { SearchDb } from "@oh-my-pi/pi-natives";
15
14
  import type { Component } from "@oh-my-pi/pi-tui";
16
15
  import {
17
16
  $env,
@@ -19,13 +18,13 @@ import {
19
18
  getAgentDbPath,
20
19
  getAgentDir,
21
20
  getProjectDir,
22
- getSearchDbDir,
23
21
  logger,
24
22
  postmortem,
25
23
  prompt,
24
+ Snowflake,
26
25
  } from "@oh-my-pi/pi-utils";
27
26
  import chalk from "chalk";
28
- import { AsyncJobManager } from "./async";
27
+ import { AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
29
28
  import { createAutoresearchExtension } from "./autoresearch";
30
29
  import { loadCapability } from "./capability";
31
30
  import { type Rule, ruleCapability } from "./capability/rule";
@@ -73,7 +72,7 @@ import {
73
72
  RuleProtocolHandler,
74
73
  SkillProtocolHandler,
75
74
  } from "./internal-urls";
76
- import { disposeAllKernelSessions } from "./ipy/executor";
75
+ import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./ipy/executor";
77
76
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
78
77
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
79
78
  import {
@@ -148,8 +147,6 @@ export interface CreateAgentSessionOptions {
148
147
  authStorage?: AuthStorage;
149
148
  /** Model registry. Default: discoverModels(authStorage, agentDir) */
150
149
  modelRegistry?: ModelRegistry;
151
- /** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
152
- searchDb?: SearchDb;
153
150
 
154
151
  /** Model to use. Default: from settings, else first available */
155
152
  model?: Model;
@@ -202,6 +199,8 @@ export interface CreateAgentSessionOptions {
202
199
  enableLsp?: boolean;
203
200
  /** Skip Python kernel availability check and prelude warmup */
204
201
  skipPythonPreflight?: boolean;
202
+ /** Force Python prelude warmup even when test env would normally skip it */
203
+ forcePythonWarmup?: boolean;
205
204
 
206
205
  /** Tool names explicitly requested (enables disabled-by-default tools) */
207
206
  toolNames?: string[];
@@ -402,7 +401,6 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
402
401
  sessionManager: ctx.sessionManager,
403
402
  modelRegistry: ctx.modelRegistry,
404
403
  model: ctx.model,
405
- searchDb: ctx.searchDb,
406
404
  isIdle: ctx.isIdle,
407
405
  hasQueuedMessages: ctx.hasPendingMessages,
408
406
  abort: ctx.abort,
@@ -728,6 +726,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
728
726
  resolveModelRoleValue(settings.getModelRole("default"), modelRegistry.getAvailable(), {
729
727
  settings,
730
728
  matchPreferences: modelMatchPreferences,
729
+ modelRegistry,
731
730
  }),
732
731
  );
733
732
  let model = options.model;
@@ -834,10 +833,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
834
833
  );
835
834
 
836
835
  let agent: Agent;
837
- let session: AgentSession;
838
-
836
+ let session!: AgentSession;
837
+ let hasSession = false;
839
838
  const enableLsp = options.enableLsp ?? true;
840
- const asyncEnabled = settings.get("async.enabled");
839
+ const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
841
840
  const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
842
841
  const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
843
842
  const ASYNC_PREVIEW_MAX_CHARS = 4_000;
@@ -861,12 +860,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
861
860
 
862
861
  return preview;
863
862
  };
864
- const asyncJobManager = asyncEnabled
863
+ const asyncJobManager = backgroundJobsEnabled
865
864
  ? new AsyncJobManager({
866
865
  maxRunningJobs: asyncMaxJobs,
867
866
  onJobComplete: async (jobId, result, job) => {
868
- if (!session) return;
867
+ if (!session || asyncJobManager!.isDeliverySuppressed(jobId)) return;
869
868
  const formattedResult = await formatAsyncResultForFollowUp(result);
869
+ if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
870
+
870
871
  const message = prompt.render(asyncResultTemplate, { jobId, result: formattedResult });
871
872
  const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
872
873
  await session.sendCustomMessage(
@@ -888,435 +889,423 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
888
889
  })
889
890
  : undefined;
890
891
 
891
- const searchDb = options.searchDb ?? new SearchDb(getSearchDbDir(agentDir));
892
- const toolSession: ToolSession = {
893
- cwd,
894
- hasUI: options.hasUI ?? false,
895
- enableLsp,
896
- get hasEditTool() {
897
- return !options.toolNames || options.toolNames.includes("edit");
898
- },
899
- skipPythonPreflight: options.skipPythonPreflight,
900
- contextFiles,
901
- skills,
902
- eventBus,
903
- outputSchema: options.outputSchema,
904
- requireSubmitResultTool: options.requireSubmitResultTool,
905
- taskDepth: options.taskDepth ?? 0,
906
- getSessionFile: () => sessionManager.getSessionFile() ?? null,
907
- getSessionId: () => sessionManager.getSessionId?.() ?? null,
908
- getSessionSpawns: () => options.spawns ?? "*",
909
- getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
910
- getActiveModelString: () => {
892
+ const pythonKernelOwnerId = `agent-session:${Snowflake.next()}`;
893
+
894
+ try {
895
+ const getActiveModelString = (): string | undefined => {
911
896
  const activeModel = agent?.state.model;
912
897
  if (activeModel) return formatModelString(activeModel);
913
- // Fall back to initial model during tool creation (before agent exists)
914
898
  if (model) return formatModelString(model);
915
899
  return undefined;
916
- },
917
- getPlanModeState: () => session.getPlanModeState(),
918
- getCompactContext: () => session.formatCompactContext(),
919
- getTodoPhases: () => session.getTodoPhases(),
920
- setTodoPhases: phases => session.setTodoPhases(phases),
921
- isMCPDiscoveryEnabled: () => session.isMCPDiscoveryEnabled(),
922
- getDiscoverableMCPTools: () => session.getDiscoverableMCPTools(),
923
- getDiscoverableMCPSearchIndex: () => session.getDiscoverableMCPSearchIndex(),
924
- getSelectedMCPToolNames: () => session.getSelectedMCPToolNames(),
925
- activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
926
- getCheckpointState: () => session.getCheckpointState(),
927
- setCheckpointState: state => session.setCheckpointState(state ?? undefined),
928
- getToolChoiceQueue: () => session.toolChoiceQueue,
929
- buildToolChoice: name => {
930
- const m = session.model;
931
- return m ? buildNamedToolChoice(name, m) : undefined;
932
- },
933
- steer: msg =>
934
- session.agent.steer({
935
- role: "custom",
936
- customType: msg.customType,
937
- content: msg.content,
938
- display: false,
939
- details: msg.details,
940
- attribution: "agent",
941
- timestamp: Date.now(),
942
- }),
943
- peekQueueInvoker: () => session.peekQueueInvoker(),
944
- allocateOutputArtifact: async toolType => {
945
- try {
946
- return await sessionManager.allocateArtifactPath(toolType);
947
- } catch {
948
- return {};
949
- }
950
- },
951
- settings,
952
- authStorage,
953
- modelRegistry,
954
- asyncJobManager,
955
- searchDb,
956
- };
957
-
958
- // Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, mcp://, local://)
959
- const internalRouter = new InternalUrlRouter();
960
- const getArtifactsDir = () => sessionManager.getArtifactsDir();
961
- internalRouter.register(new AgentProtocolHandler({ getArtifactsDir }));
962
- internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
963
- internalRouter.register(
964
- new MemoryProtocolHandler({
965
- getMemoryRoot: () => getMemoryRoot(agentDir, settings.getCwd()),
966
- }),
967
- );
968
- internalRouter.register(
969
- new LocalProtocolHandler({
970
- getArtifactsDir,
971
- getSessionId: () => sessionManager.getSessionId(),
972
- }),
973
- );
974
- internalRouter.register(
975
- new SkillProtocolHandler({
976
- getSkills: () => skills,
977
- }),
978
- );
979
- internalRouter.register(
980
- new RuleProtocolHandler({
981
- getRules: () => [...rulebookRules, ...alwaysApplyRules],
982
- }),
983
- );
984
- internalRouter.register(new PiProtocolHandler());
985
- internalRouter.register(new JobsProtocolHandler({ getAsyncJobManager: () => asyncJobManager }));
986
- internalRouter.register(new McpProtocolHandler({ getMcpManager: () => mcpManager }));
987
- toolSession.internalRouter = internalRouter;
988
- toolSession.getArtifactsDir = getArtifactsDir;
989
- toolSession.agentOutputManager = new AgentOutputManager(
990
- getArtifactsDir,
991
- options.parentTaskPrefix ? { parentPrefix: options.parentTaskPrefix } : undefined,
992
- );
993
-
994
- // Create built-in tools (already wrapped with meta notice formatting)
995
- const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
996
-
997
- // Discover MCP tools from .mcp.json files
998
- let mcpManager: MCPManager | undefined;
999
- const enableMCP = options.enableMCP ?? true;
1000
- const customTools: CustomTool[] = [];
1001
- if (enableMCP) {
1002
- const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
1003
- onConnecting: serverNames => {
1004
- if (options.hasUI && serverNames.length > 0) {
1005
- process.stderr.write(`${chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…`)}\n`);
900
+ };
901
+ const toolSession: ToolSession = {
902
+ cwd,
903
+ hasUI: options.hasUI ?? false,
904
+ enableLsp,
905
+ get hasEditTool() {
906
+ const requestedToolNames = options.toolNames
907
+ ? [...new Set(options.toolNames.map(name => name.toLowerCase()))]
908
+ : undefined;
909
+ return !requestedToolNames || requestedToolNames.includes("edit");
910
+ },
911
+ skipPythonPreflight: options.skipPythonPreflight,
912
+ forcePythonWarmup: options.forcePythonWarmup,
913
+ contextFiles,
914
+ skills,
915
+ eventBus,
916
+ outputSchema: options.outputSchema,
917
+ requireSubmitResultTool: options.requireSubmitResultTool,
918
+ taskDepth: options.taskDepth ?? 0,
919
+ getSessionFile: () => sessionManager.getSessionFile() ?? null,
920
+ getPythonKernelOwnerId: () => pythonKernelOwnerId,
921
+ assertPythonExecutionAllowed: () => session?.assertPythonExecutionAllowed(),
922
+ trackPythonExecution: (execution, abortController) =>
923
+ session ? session.trackPythonExecution(execution, abortController) : execution,
924
+ getSessionId: () => sessionManager.getSessionId?.() ?? null,
925
+ getSessionSpawns: () => options.spawns ?? "*",
926
+ getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
927
+ getActiveModelString,
928
+ getPlanModeState: () => session.getPlanModeState(),
929
+ getCompactContext: () => session.formatCompactContext(),
930
+ getTodoPhases: () => session.getTodoPhases(),
931
+ setTodoPhases: phases => session.setTodoPhases(phases),
932
+ isMCPDiscoveryEnabled: () => session.isMCPDiscoveryEnabled(),
933
+ getDiscoverableMCPTools: () => session.getDiscoverableMCPTools(),
934
+ getDiscoverableMCPSearchIndex: () => session.getDiscoverableMCPSearchIndex(),
935
+ getSelectedMCPToolNames: () => session.getSelectedMCPToolNames(),
936
+ activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
937
+ getCheckpointState: () => session.getCheckpointState(),
938
+ setCheckpointState: state => session.setCheckpointState(state ?? undefined),
939
+ getToolChoiceQueue: () => session.toolChoiceQueue,
940
+ buildToolChoice: name => {
941
+ const m = session.model;
942
+ return m ? buildNamedToolChoice(name, m) : undefined;
943
+ },
944
+ steer: msg =>
945
+ session.agent.steer({
946
+ role: "custom",
947
+ customType: msg.customType,
948
+ content: msg.content,
949
+ display: false,
950
+ details: msg.details,
951
+ attribution: "agent",
952
+ timestamp: Date.now(),
953
+ }),
954
+ peekQueueInvoker: () => session.peekQueueInvoker(),
955
+ allocateOutputArtifact: async toolType => {
956
+ try {
957
+ return await sessionManager.allocateArtifactPath(toolType);
958
+ } catch {
959
+ return {};
1006
960
  }
1007
961
  },
1008
- enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
1009
- // Always filter Exa - we have native integration
1010
- filterExa: true,
1011
- // Filter browser MCP servers when builtin browser tool is active
1012
- filterBrowser: settings.get("browser.enabled") ?? false,
1013
- cacheStorage: settings.getStorage(),
962
+ settings,
1014
963
  authStorage,
1015
- });
1016
- mcpManager = mcpResult.manager;
1017
- toolSession.mcpManager = mcpManager;
1018
-
1019
- if (settings.get("mcp.notifications")) {
1020
- mcpManager.setNotificationsEnabled(true);
1021
- }
1022
- // If we extracted Exa API keys from MCP configs and EXA_API_KEY isn't set, use the first one
1023
- if (mcpResult.exaApiKeys.length > 0 && !$env.EXA_API_KEY) {
1024
- Bun.env.EXA_API_KEY = mcpResult.exaApiKeys[0];
1025
- }
964
+ modelRegistry,
965
+ asyncJobManager,
966
+ };
1026
967
 
1027
- // Log MCP errors
1028
- for (const { path, error } of mcpResult.errors) {
1029
- logger.error("MCP tool load failed", { path, error });
1030
- }
968
+ // Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, mcp://, local://)
969
+ const internalRouter = new InternalUrlRouter();
970
+ const getArtifactsDir = () => sessionManager.getArtifactsDir();
971
+ internalRouter.register(new AgentProtocolHandler({ getArtifactsDir }));
972
+ internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
973
+ internalRouter.register(
974
+ new MemoryProtocolHandler({
975
+ getMemoryRoot: () => getMemoryRoot(agentDir, settings.getCwd()),
976
+ }),
977
+ );
978
+ internalRouter.register(
979
+ new LocalProtocolHandler({
980
+ getArtifactsDir,
981
+ getSessionId: () => sessionManager.getSessionId(),
982
+ }),
983
+ );
984
+ internalRouter.register(
985
+ new SkillProtocolHandler({
986
+ getSkills: () => skills,
987
+ }),
988
+ );
989
+ internalRouter.register(
990
+ new RuleProtocolHandler({
991
+ getRules: () => [...rulebookRules, ...alwaysApplyRules],
992
+ }),
993
+ );
994
+ internalRouter.register(new PiProtocolHandler());
995
+ internalRouter.register(new JobsProtocolHandler({ getAsyncJobManager: () => asyncJobManager }));
996
+ internalRouter.register(new McpProtocolHandler({ getMcpManager: () => mcpManager }));
997
+ toolSession.internalRouter = internalRouter;
998
+ toolSession.getArtifactsDir = getArtifactsDir;
999
+ toolSession.agentOutputManager = new AgentOutputManager(
1000
+ getArtifactsDir,
1001
+ options.parentTaskPrefix ? { parentPrefix: options.parentTaskPrefix } : undefined,
1002
+ );
1031
1003
 
1032
- if (mcpResult.tools.length > 0) {
1033
- // MCP tools are LoadedCustomTool, extract the tool property
1034
- customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
1035
- }
1036
- }
1004
+ // Create built-in tools (already wrapped with meta notice formatting)
1005
+ const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
1006
+
1007
+ // Discover MCP tools from .mcp.json files
1008
+ let mcpManager: MCPManager | undefined;
1009
+ const enableMCP = options.enableMCP ?? true;
1010
+ const customTools: CustomTool[] = [];
1011
+ if (enableMCP) {
1012
+ const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
1013
+ onConnecting: serverNames => {
1014
+ if (options.hasUI && serverNames.length > 0) {
1015
+ process.stderr.write(`${chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…`)}\n`);
1016
+ }
1017
+ },
1018
+ enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
1019
+ // Always filter Exa - we have native integration
1020
+ filterExa: true,
1021
+ // Filter browser MCP servers when builtin browser tool is active
1022
+ filterBrowser: settings.get("browser.enabled") ?? false,
1023
+ cacheStorage: settings.getStorage(),
1024
+ authStorage,
1025
+ });
1026
+ mcpManager = mcpResult.manager;
1027
+ toolSession.mcpManager = mcpManager;
1037
1028
 
1038
- // Add Gemini image tools if GEMINI_API_KEY (or GOOGLE_API_KEY) is available
1039
- const geminiImageTools = await logger.time("getGeminiImageTools", getGeminiImageTools);
1040
- if (geminiImageTools.length > 0) {
1041
- customTools.push(...(geminiImageTools as unknown as CustomTool[]));
1042
- }
1029
+ if (settings.get("mcp.notifications")) {
1030
+ mcpManager.setNotificationsEnabled(true);
1031
+ }
1032
+ // If we extracted Exa API keys from MCP configs and EXA_API_KEY isn't set, use the first one
1033
+ if (mcpResult.exaApiKeys.length > 0 && !$env.EXA_API_KEY) {
1034
+ Bun.env.EXA_API_KEY = mcpResult.exaApiKeys[0];
1035
+ }
1043
1036
 
1044
- // Add web search tools
1045
- if (options.toolNames?.includes("web_search")) {
1046
- customTools.push(...getSearchTools());
1047
- }
1037
+ // Log MCP errors
1038
+ for (const { path, error } of mcpResult.errors) {
1039
+ logger.error("MCP tool load failed", { path, error });
1040
+ }
1048
1041
 
1049
- // Discover and load custom tools from .omp/tools/, .claude/tools/, etc.
1050
- const builtInToolNames = builtinTools.map(t => t.name);
1051
- const discoveredCustomTools = await logger.time(
1052
- "discoverAndLoadCustomTools",
1053
- discoverAndLoadCustomTools,
1054
- [],
1055
- cwd,
1056
- builtInToolNames,
1057
- action => queueResolveHandler(toolSession, action),
1058
- );
1059
- for (const { path, error } of discoveredCustomTools.errors) {
1060
- logger.error("Custom tool load failed", { path, error });
1061
- }
1062
- if (discoveredCustomTools.tools.length > 0) {
1063
- customTools.push(...discoveredCustomTools.tools.map(loaded => loaded.tool));
1064
- }
1042
+ if (mcpResult.tools.length > 0) {
1043
+ // MCP tools are LoadedCustomTool, extract the tool property
1044
+ customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
1045
+ }
1046
+ }
1065
1047
 
1066
- const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
1067
- inlineExtensions.push(createAutoresearchExtension);
1068
- if (customTools.length > 0) {
1069
- inlineExtensions.push(createCustomToolsExtension(customTools));
1070
- }
1048
+ // Add Gemini image tools if GEMINI_API_KEY (or GOOGLE_API_KEY) is available
1049
+ const geminiImageTools = await logger.time("getGeminiImageTools", getGeminiImageTools);
1050
+ if (geminiImageTools.length > 0) {
1051
+ customTools.push(...(geminiImageTools as unknown as CustomTool[]));
1052
+ }
1071
1053
 
1072
- // Load extensions (discovers from standard locations + configured paths)
1073
- let extensionsResult: LoadExtensionsResult;
1074
- if (options.disableExtensionDiscovery) {
1075
- const configuredPaths = options.additionalExtensionPaths ?? [];
1076
- extensionsResult = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
1077
- for (const { path, error } of extensionsResult.errors) {
1078
- logger.error("Failed to load extension", { path, error });
1054
+ // Add web search tools
1055
+ if (options.toolNames?.includes("web_search")) {
1056
+ customTools.push(...getSearchTools());
1079
1057
  }
1080
- } else if (options.preloadedExtensions) {
1081
- extensionsResult = options.preloadedExtensions;
1082
- } else {
1083
- // Merge CLI extension paths with settings extension paths
1084
- const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
1085
- const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
1086
- extensionsResult = await logger.time(
1087
- "discoverAndLoadExtensions",
1088
- discoverAndLoadExtensions,
1089
- configuredPaths,
1058
+
1059
+ // Discover and load custom tools from .omp/tools/, .claude/tools/, etc.
1060
+ const builtInToolNames = builtinTools.map(t => t.name);
1061
+ const discoveredCustomTools = await logger.time(
1062
+ "discoverAndLoadCustomTools",
1063
+ discoverAndLoadCustomTools,
1064
+ [],
1090
1065
  cwd,
1091
- eventBus,
1092
- disabledExtensionIds,
1066
+ builtInToolNames,
1067
+ action => queueResolveHandler(toolSession, action),
1093
1068
  );
1094
- for (const { path, error } of extensionsResult.errors) {
1095
- logger.error("Failed to load extension", { path, error });
1069
+ for (const { path, error } of discoveredCustomTools.errors) {
1070
+ logger.error("Custom tool load failed", { path, error });
1071
+ }
1072
+ if (discoveredCustomTools.tools.length > 0) {
1073
+ customTools.push(...discoveredCustomTools.tools.map(loaded => loaded.tool));
1096
1074
  }
1097
- }
1098
1075
 
1099
- // Load inline extensions from factories
1100
- if (inlineExtensions.length > 0) {
1101
- for (let i = 0; i < inlineExtensions.length; i++) {
1102
- const factory = inlineExtensions[i];
1103
- const loaded = await loadExtensionFromFactory(
1104
- factory,
1076
+ const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
1077
+ inlineExtensions.push(createAutoresearchExtension);
1078
+ if (customTools.length > 0) {
1079
+ inlineExtensions.push(createCustomToolsExtension(customTools));
1080
+ }
1081
+
1082
+ // Load extensions (discovers from standard locations + configured paths)
1083
+ let extensionsResult: LoadExtensionsResult;
1084
+ if (options.disableExtensionDiscovery) {
1085
+ const configuredPaths = options.additionalExtensionPaths ?? [];
1086
+ extensionsResult = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
1087
+ for (const { path, error } of extensionsResult.errors) {
1088
+ logger.error("Failed to load extension", { path, error });
1089
+ }
1090
+ } else if (options.preloadedExtensions) {
1091
+ extensionsResult = options.preloadedExtensions;
1092
+ } else {
1093
+ // Merge CLI extension paths with settings extension paths
1094
+ const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
1095
+ const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
1096
+ extensionsResult = await logger.time(
1097
+ "discoverAndLoadExtensions",
1098
+ discoverAndLoadExtensions,
1099
+ configuredPaths,
1105
1100
  cwd,
1106
1101
  eventBus,
1107
- extensionsResult.runtime,
1108
- `<inline-${i}>`,
1102
+ disabledExtensionIds,
1109
1103
  );
1110
- extensionsResult.extensions.push(loaded);
1104
+ for (const { path, error } of extensionsResult.errors) {
1105
+ logger.error("Failed to load extension", { path, error });
1106
+ }
1111
1107
  }
1112
- }
1113
1108
 
1114
- // Process provider registrations queued during extension loading.
1115
- // This must happen before the runner is created so that models registered by
1116
- // extensions are available for model selection on session resume / fallback.
1117
- const activeExtensionSources = extensionsResult.extensions.map(extension => extension.path);
1118
- modelRegistry.syncExtensionSources(activeExtensionSources);
1119
- for (const sourceId of new Set(activeExtensionSources)) {
1120
- modelRegistry.clearSourceRegistrations(sourceId);
1121
- }
1122
- if (extensionsResult.runtime.pendingProviderRegistrations.length > 0) {
1123
- for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
1124
- modelRegistry.registerProvider(name, config, sourceId);
1109
+ // Load inline extensions from factories
1110
+ if (inlineExtensions.length > 0) {
1111
+ for (let i = 0; i < inlineExtensions.length; i++) {
1112
+ const factory = inlineExtensions[i];
1113
+ const loaded = await loadExtensionFromFactory(
1114
+ factory,
1115
+ cwd,
1116
+ eventBus,
1117
+ extensionsResult.runtime,
1118
+ `<inline-${i}>`,
1119
+ );
1120
+ extensionsResult.extensions.push(loaded);
1121
+ }
1125
1122
  }
1126
- extensionsResult.runtime.pendingProviderRegistrations = [];
1127
- }
1128
1123
 
1129
- // Resolve deferred --model pattern now that extension models are registered.
1130
- if (!model && options.modelPattern) {
1131
- const availableModels = modelRegistry.getAll();
1132
- const matchPreferences = {
1133
- usageOrder: settings.getStorage()?.getModelUsageOrder(),
1134
- };
1135
- const { model: resolved } = parseModelPattern(options.modelPattern, availableModels, matchPreferences);
1136
- if (resolved) {
1137
- model = resolved;
1138
- modelFallbackMessage = undefined;
1139
- } else {
1140
- modelFallbackMessage = `Model "${options.modelPattern}" not found`;
1124
+ // Process provider registrations queued during extension loading.
1125
+ // This must happen before the runner is created so that models registered by
1126
+ // extensions are available for model selection on session resume / fallback.
1127
+ const activeExtensionSources = extensionsResult.extensions.map(extension => extension.path);
1128
+ modelRegistry.syncExtensionSources(activeExtensionSources);
1129
+ for (const sourceId of new Set(activeExtensionSources)) {
1130
+ modelRegistry.clearSourceRegistrations(sourceId);
1131
+ }
1132
+ if (extensionsResult.runtime.pendingProviderRegistrations.length > 0) {
1133
+ for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
1134
+ modelRegistry.registerProvider(name, config, sourceId);
1135
+ }
1136
+ extensionsResult.runtime.pendingProviderRegistrations = [];
1141
1137
  }
1142
- }
1143
1138
 
1144
- // Fall back to first available model with a valid API key.
1145
- // Skip fallback if the user explicitly requested a model via --model that wasn't found.
1146
- if (!model && !options.modelPattern) {
1147
- const allModels = modelRegistry.getAll();
1148
- for (const candidate of allModels) {
1149
- if (await hasModelApiKey(candidate)) {
1150
- model = candidate;
1151
- break;
1139
+ // Resolve deferred --model pattern now that extension models are registered.
1140
+ if (!model && options.modelPattern) {
1141
+ const availableModels = modelRegistry.getAll();
1142
+ const matchPreferences = {
1143
+ usageOrder: settings.getStorage()?.getModelUsageOrder(),
1144
+ };
1145
+ const { model: resolved } = parseModelPattern(options.modelPattern, availableModels, matchPreferences, {
1146
+ modelRegistry,
1147
+ });
1148
+ if (resolved) {
1149
+ model = resolved;
1150
+ modelFallbackMessage = undefined;
1151
+ } else {
1152
+ modelFallbackMessage = `Model "${options.modelPattern}" not found`;
1152
1153
  }
1153
1154
  }
1154
- if (model) {
1155
- if (modelFallbackMessage) {
1156
- modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
1155
+
1156
+ // Fall back to first available model with a valid API key.
1157
+ // Skip fallback if the user explicitly requested a model via --model that wasn't found.
1158
+ if (!model && !options.modelPattern) {
1159
+ const allModels = modelRegistry.getAll();
1160
+ for (const candidate of allModels) {
1161
+ if (await hasModelApiKey(candidate)) {
1162
+ model = candidate;
1163
+ break;
1164
+ }
1165
+ }
1166
+ if (model) {
1167
+ if (modelFallbackMessage) {
1168
+ modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
1169
+ }
1170
+ } else {
1171
+ modelFallbackMessage =
1172
+ "No models available. Use /login or set an API key environment variable. Then use /model to select a model.";
1157
1173
  }
1158
- } else {
1159
- modelFallbackMessage =
1160
- "No models available. Use /login or set an API key environment variable. Then use /model to select a model.";
1161
1174
  }
1162
- }
1163
1175
 
1164
- // Discover custom commands (TypeScript slash commands)
1165
- const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
1166
- ? { commands: [], errors: [] }
1167
- : await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
1168
- if (!options.disableExtensionDiscovery) {
1169
- for (const { path, error } of customCommandsResult.errors) {
1170
- logger.error("Failed to load custom command", { path, error });
1176
+ // Discover custom commands (TypeScript slash commands)
1177
+ const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
1178
+ ? { commands: [], errors: [] }
1179
+ : await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
1180
+ if (!options.disableExtensionDiscovery) {
1181
+ for (const { path, error } of customCommandsResult.errors) {
1182
+ logger.error("Failed to load custom command", { path, error });
1183
+ }
1171
1184
  }
1172
- }
1173
1185
 
1174
- let extensionRunner: ExtensionRunner | undefined;
1175
- if (extensionsResult.extensions.length > 0) {
1176
- extensionRunner = new ExtensionRunner(
1177
- extensionsResult.extensions,
1178
- extensionsResult.runtime,
1179
- cwd,
1180
- sessionManager,
1181
- modelRegistry,
1182
- );
1183
- }
1186
+ let extensionRunner: ExtensionRunner | undefined;
1187
+ if (extensionsResult.extensions.length > 0) {
1188
+ extensionRunner = new ExtensionRunner(
1189
+ extensionsResult.extensions,
1190
+ extensionsResult.runtime,
1191
+ cwd,
1192
+ sessionManager,
1193
+ modelRegistry,
1194
+ );
1195
+ }
1184
1196
 
1185
- const getSessionContext = () => ({
1186
- sessionManager,
1187
- modelRegistry,
1188
- model: agent.state.model,
1189
- isIdle: () => !session.isStreaming,
1190
- hasQueuedMessages: () => session.queuedMessageCount > 0,
1191
- abort: () => {
1192
- session.abort();
1193
- },
1194
- settings,
1195
- });
1196
- const toolContextStore = new ToolContextStore(getSessionContext);
1197
-
1198
- const registeredTools = extensionRunner?.getAllRegisteredTools() ?? [];
1199
- let wrappedExtensionTools: Tool[];
1200
-
1201
- if (extensionRunner) {
1202
- // With extension runner: convert CustomTools to ToolDefinitions and wrap all together
1203
- const allCustomTools = [
1204
- ...registeredTools,
1205
- ...(options.customTools?.map(tool => {
1206
- const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
1207
- return { definition, extensionPath: "<sdk>" };
1208
- }) ?? []),
1209
- ];
1210
- wrappedExtensionTools = wrapRegisteredTools(allCustomTools, extensionRunner);
1211
- } else {
1212
- // Without extension runner: wrap CustomTools directly with CustomToolAdapter
1213
- // ToolDefinition items require ExtensionContext and cannot be used without a runner
1214
- const customToolContext = (): CustomToolContext => ({
1197
+ const getSessionContext = () => ({
1215
1198
  sessionManager,
1216
1199
  modelRegistry,
1217
- model: agent?.state.model,
1218
- searchDb,
1219
- isIdle: () => !session?.isStreaming,
1220
- hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
1221
- abort: () => session?.abort(),
1200
+ model: agent.state.model,
1201
+ isIdle: () => !session.isStreaming,
1202
+ hasQueuedMessages: () => session.queuedMessageCount > 0,
1203
+ abort: () => {
1204
+ session.abort();
1205
+ },
1222
1206
  settings,
1223
1207
  });
1224
- wrappedExtensionTools = (options.customTools ?? [])
1225
- .filter(isCustomTool)
1226
- .map(tool => CustomToolAdapter.wrap(tool, customToolContext));
1227
- }
1228
-
1229
- // All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
1230
- const toolRegistry = new Map<string, Tool>();
1231
- for (const tool of builtinTools) {
1232
- toolRegistry.set(tool.name, tool);
1233
- }
1234
- for (const tool of wrappedExtensionTools) {
1235
- toolRegistry.set(tool.name, tool);
1236
- }
1237
- if (extensionRunner) {
1238
- for (const tool of toolRegistry.values()) {
1239
- toolRegistry.set(tool.name, new ExtensionToolWrapper(tool, extensionRunner));
1208
+ const toolContextStore = new ToolContextStore(getSessionContext);
1209
+
1210
+ const registeredTools = extensionRunner?.getAllRegisteredTools() ?? [];
1211
+ let wrappedExtensionTools: Tool[];
1212
+
1213
+ if (extensionRunner) {
1214
+ // With extension runner: convert CustomTools to ToolDefinitions and wrap all together
1215
+ const allCustomTools = [
1216
+ ...registeredTools,
1217
+ ...(options.customTools?.map(tool => {
1218
+ const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
1219
+ return { definition, extensionPath: "<sdk>" };
1220
+ }) ?? []),
1221
+ ];
1222
+ wrappedExtensionTools = wrapRegisteredTools(allCustomTools, extensionRunner);
1223
+ } else {
1224
+ // Without extension runner: wrap CustomTools directly with CustomToolAdapter
1225
+ // ToolDefinition items require ExtensionContext and cannot be used without a runner
1226
+ const customToolContext = (): CustomToolContext => ({
1227
+ sessionManager,
1228
+ modelRegistry,
1229
+ model: agent?.state.model,
1230
+ isIdle: () => !session?.isStreaming,
1231
+ hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
1232
+ abort: () => session?.abort(),
1233
+ settings,
1234
+ });
1235
+ wrappedExtensionTools = (options.customTools ?? [])
1236
+ .filter(isCustomTool)
1237
+ .map(tool => CustomToolAdapter.wrap(tool, customToolContext));
1240
1238
  }
1241
- }
1242
- if (model?.provider === "cursor") {
1243
- toolRegistry.delete("edit");
1244
- }
1245
1239
 
1246
- const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
1247
- if (!hasDeferrableTools) {
1248
- toolRegistry.delete("resolve");
1249
- } else if (!toolRegistry.has("resolve")) {
1250
- const resolveTool = await logger.time("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
1251
- if (resolveTool) {
1252
- toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
1240
+ // All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
1241
+ const toolRegistry = new Map<string, Tool>();
1242
+ for (const tool of builtinTools) {
1243
+ toolRegistry.set(tool.name, tool);
1244
+ }
1245
+ for (const tool of wrappedExtensionTools) {
1246
+ toolRegistry.set(tool.name, tool);
1247
+ }
1248
+ if (extensionRunner) {
1249
+ for (const tool of toolRegistry.values()) {
1250
+ toolRegistry.set(tool.name, new ExtensionToolWrapper(tool, extensionRunner));
1251
+ }
1252
+ }
1253
+ if (model?.provider === "cursor") {
1254
+ toolRegistry.delete("edit");
1253
1255
  }
1254
- }
1255
-
1256
- let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
1257
- const cursorExecHandlers = new CursorExecHandlers({
1258
- cwd,
1259
- tools: toolRegistry,
1260
- getToolContext: () => toolContextStore.getContext(),
1261
- emitEvent: event => cursorEventEmitter?.(event),
1262
- });
1263
1256
 
1264
- const repeatToolDescriptions = settings.get("repeatToolDescriptions");
1265
- const eagerTasks = settings.get("task.eager");
1266
- const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
1267
- const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1268
- toolContextStore.setToolNames(toolNames);
1269
- const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
1270
- const discoverableMCPSummary = summarizeDiscoverableMCPTools(discoverableMCPTools);
1271
- const hasDiscoverableMCPTools =
1272
- mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableMCPTools.length > 0;
1273
- const promptTools = buildSystemPromptToolMetadata(tools, {
1274
- search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1275
- });
1276
- const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
1277
-
1278
- // Build combined append prompt: memory instructions + MCP server instructions
1279
- const serverInstructions = mcpManager?.getServerInstructions();
1280
- let appendPrompt: string | undefined = memoryInstructions ?? undefined;
1281
- if (serverInstructions && serverInstructions.size > 0) {
1282
- const MAX_INSTRUCTIONS_LENGTH = 4000;
1283
- const parts: string[] = [];
1284
- if (appendPrompt) parts.push(appendPrompt);
1285
- parts.push(
1286
- "## MCP Server Instructions\n\nThe following instructions are provided by connected MCP servers. They are server-controlled and may not be verified.",
1287
- );
1288
- for (const [srvName, srvInstructions] of serverInstructions) {
1289
- const truncated =
1290
- srvInstructions.length > MAX_INSTRUCTIONS_LENGTH
1291
- ? `${srvInstructions.slice(0, MAX_INSTRUCTIONS_LENGTH)}\n[truncated]`
1292
- : srvInstructions;
1293
- parts.push(`### ${srvName}\n${truncated}`);
1257
+ const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
1258
+ if (!hasDeferrableTools) {
1259
+ toolRegistry.delete("resolve");
1260
+ } else if (!toolRegistry.has("resolve")) {
1261
+ const resolveTool = await logger.time("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
1262
+ if (resolveTool) {
1263
+ toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
1294
1264
  }
1295
- appendPrompt = parts.join("\n\n");
1296
1265
  }
1297
- const defaultPrompt = await buildSystemPromptInternal({
1266
+
1267
+ let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
1268
+ const cursorExecHandlers = new CursorExecHandlers({
1298
1269
  cwd,
1299
- skills,
1300
- contextFiles,
1301
- tools: promptTools,
1302
- toolNames,
1303
- rules: rulebookRules,
1304
- alwaysApplyRules,
1305
- skillsSettings: settings.getGroup("skills"),
1306
- appendSystemPrompt: appendPrompt,
1307
- repeatToolDescriptions,
1308
- intentField,
1309
- mcpDiscoveryMode: hasDiscoverableMCPTools,
1310
- mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1311
- eagerTasks,
1312
- secretsEnabled,
1270
+ tools: toolRegistry,
1271
+ getToolContext: () => toolContextStore.getContext(),
1272
+ emitEvent: event => cursorEventEmitter?.(event),
1313
1273
  });
1314
1274
 
1315
- if (options.systemPrompt === undefined) {
1316
- return defaultPrompt;
1317
- }
1318
- if (typeof options.systemPrompt === "string") {
1319
- return await buildSystemPromptInternal({
1275
+ const repeatToolDescriptions = settings.get("repeatToolDescriptions");
1276
+ const eagerTasks = settings.get("task.eager");
1277
+ const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
1278
+ const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1279
+ toolContextStore.setToolNames(toolNames);
1280
+ const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
1281
+ const discoverableMCPSummary = summarizeDiscoverableMCPTools(discoverableMCPTools);
1282
+ const hasDiscoverableMCPTools =
1283
+ mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableMCPTools.length > 0;
1284
+ const promptTools = buildSystemPromptToolMetadata(tools, {
1285
+ search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1286
+ });
1287
+ const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
1288
+
1289
+ // Build combined append prompt: memory instructions + MCP server instructions
1290
+ const serverInstructions = mcpManager?.getServerInstructions();
1291
+ let appendPrompt: string | undefined = memoryInstructions ?? undefined;
1292
+ if (serverInstructions && serverInstructions.size > 0) {
1293
+ const MAX_INSTRUCTIONS_LENGTH = 4000;
1294
+ const parts: string[] = [];
1295
+ if (appendPrompt) parts.push(appendPrompt);
1296
+ parts.push(
1297
+ "## MCP Server Instructions\n\nThe following instructions are provided by connected MCP servers. They are server-controlled and may not be verified.",
1298
+ );
1299
+ for (const [srvName, srvInstructions] of serverInstructions) {
1300
+ const truncated =
1301
+ srvInstructions.length > MAX_INSTRUCTIONS_LENGTH
1302
+ ? `${srvInstructions.slice(0, MAX_INSTRUCTIONS_LENGTH)}\n[truncated]`
1303
+ : srvInstructions;
1304
+ parts.push(`### ${srvName}\n${truncated}`);
1305
+ }
1306
+ appendPrompt = parts.join("\n\n");
1307
+ }
1308
+ const defaultPrompt = await buildSystemPromptInternal({
1320
1309
  cwd,
1321
1310
  skills,
1322
1311
  contextFiles,
@@ -1325,7 +1314,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1325
1314
  rules: rulebookRules,
1326
1315
  alwaysApplyRules,
1327
1316
  skillsSettings: settings.getGroup("skills"),
1328
- customPrompt: options.systemPrompt,
1329
1317
  appendSystemPrompt: appendPrompt,
1330
1318
  repeatToolDescriptions,
1331
1319
  intentField,
@@ -1334,362 +1322,407 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1334
1322
  eagerTasks,
1335
1323
  secretsEnabled,
1336
1324
  });
1337
- }
1338
- return options.systemPrompt(defaultPrompt);
1339
- };
1340
1325
 
1341
- const toolNamesFromRegistry = Array.from(toolRegistry.keys());
1342
- const requestedToolNames = options.toolNames?.map(name => name.toLowerCase()) ?? toolNamesFromRegistry;
1343
- const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
1344
- const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode");
1345
- const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") ?? false;
1346
- const defaultInactiveToolNames = new Set(
1347
- registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
1348
- );
1349
- const requestedActiveToolNames = includeExitPlanMode
1350
- ? normalizedRequested
1351
- : normalizedRequested.filter(name => name !== "exit_plan_mode");
1352
- const initialRequestedActiveToolNames = options.toolNames
1353
- ? requestedActiveToolNames
1354
- : requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
1355
- const explicitlyRequestedMCPToolNames = options.toolNames
1356
- ? requestedActiveToolNames.filter(name => name.startsWith("mcp_"))
1357
- : [];
1358
- const discoveryDefaultServers = new Set(
1359
- (settings.get("mcp.discoveryDefaultServers") ?? []).map(serverName => serverName.trim()).filter(Boolean),
1360
- );
1361
- const discoveryDefaultServerToolNames = mcpDiscoveryEnabled
1362
- ? selectDiscoverableMCPToolNamesByServer(
1363
- collectDiscoverableMCPTools(toolRegistry.values()),
1364
- discoveryDefaultServers,
1365
- )
1366
- : [];
1367
- let initialSelectedMCPToolNames: string[] = [];
1368
- let defaultSelectedMCPToolNames: string[] = [];
1369
- let initialToolNames = [...initialRequestedActiveToolNames];
1370
- if (mcpDiscoveryEnabled) {
1371
- const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(name => toolRegistry.has(name));
1372
- defaultSelectedMCPToolNames = [
1373
- ...new Set([...discoveryDefaultServerToolNames, ...explicitlyRequestedMCPToolNames]),
1374
- ];
1375
- initialSelectedMCPToolNames = existingSession.hasPersistedMCPToolSelection
1376
- ? restoredSelectedMCPToolNames
1377
- : [...new Set([...restoredSelectedMCPToolNames, ...defaultSelectedMCPToolNames])];
1378
- initialToolNames = [
1379
- ...new Set([
1380
- ...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp_")),
1381
- ...initialSelectedMCPToolNames,
1382
- ]),
1383
- ];
1384
- }
1326
+ if (options.systemPrompt === undefined) {
1327
+ return defaultPrompt;
1328
+ }
1329
+ if (typeof options.systemPrompt === "string") {
1330
+ return await buildSystemPromptInternal({
1331
+ cwd,
1332
+ skills,
1333
+ contextFiles,
1334
+ tools: promptTools,
1335
+ toolNames,
1336
+ rules: rulebookRules,
1337
+ alwaysApplyRules,
1338
+ skillsSettings: settings.getGroup("skills"),
1339
+ customPrompt: options.systemPrompt,
1340
+ appendSystemPrompt: appendPrompt,
1341
+ repeatToolDescriptions,
1342
+ intentField,
1343
+ mcpDiscoveryMode: hasDiscoverableMCPTools,
1344
+ mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1345
+ eagerTasks,
1346
+ secretsEnabled,
1347
+ });
1348
+ }
1349
+ return options.systemPrompt(defaultPrompt);
1350
+ };
1385
1351
 
1386
- // Custom tools and extension-registered tools are always included regardless of toolNames filter
1387
- const alwaysInclude: string[] = [
1388
- ...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
1389
- ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
1390
- ];
1391
- for (const name of alwaysInclude) {
1392
- if (mcpDiscoveryEnabled && name.startsWith("mcp_")) {
1393
- continue;
1352
+ const toolNamesFromRegistry = Array.from(toolRegistry.keys());
1353
+ const requestedToolNames =
1354
+ (options.toolNames ? [...new Set(options.toolNames.map(name => name.toLowerCase()))] : undefined) ??
1355
+ toolNamesFromRegistry;
1356
+ const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
1357
+ const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode");
1358
+ const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") ?? false;
1359
+ const defaultInactiveToolNames = new Set(
1360
+ registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
1361
+ );
1362
+ const requestedActiveToolNames = includeExitPlanMode
1363
+ ? normalizedRequested
1364
+ : normalizedRequested.filter(name => name !== "exit_plan_mode");
1365
+ const initialRequestedActiveToolNames = options.toolNames
1366
+ ? requestedActiveToolNames
1367
+ : requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
1368
+ const explicitlyRequestedMCPToolNames = options.toolNames
1369
+ ? requestedActiveToolNames.filter(name => name.startsWith("mcp_"))
1370
+ : [];
1371
+ const discoveryDefaultServers = new Set(
1372
+ (settings.get("mcp.discoveryDefaultServers") ?? []).map(serverName => serverName.trim()).filter(Boolean),
1373
+ );
1374
+ const discoveryDefaultServerToolNames = mcpDiscoveryEnabled
1375
+ ? selectDiscoverableMCPToolNamesByServer(
1376
+ collectDiscoverableMCPTools(toolRegistry.values()),
1377
+ discoveryDefaultServers,
1378
+ )
1379
+ : [];
1380
+ let initialSelectedMCPToolNames: string[] = [];
1381
+ let defaultSelectedMCPToolNames: string[] = [];
1382
+ let initialToolNames = [...initialRequestedActiveToolNames];
1383
+ if (mcpDiscoveryEnabled) {
1384
+ const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(name =>
1385
+ toolRegistry.has(name),
1386
+ );
1387
+ defaultSelectedMCPToolNames = [
1388
+ ...new Set([...discoveryDefaultServerToolNames, ...explicitlyRequestedMCPToolNames]),
1389
+ ];
1390
+ initialSelectedMCPToolNames = existingSession.hasPersistedMCPToolSelection
1391
+ ? restoredSelectedMCPToolNames
1392
+ : [...new Set([...restoredSelectedMCPToolNames, ...defaultSelectedMCPToolNames])];
1393
+ initialToolNames = [
1394
+ ...new Set([
1395
+ ...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp_")),
1396
+ ...initialSelectedMCPToolNames,
1397
+ ]),
1398
+ ];
1394
1399
  }
1395
- if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
1396
- initialToolNames.push(name);
1400
+
1401
+ // Custom tools and extension-registered tools are always included regardless of toolNames filter
1402
+ const alwaysInclude: string[] = [
1403
+ ...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
1404
+ ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
1405
+ ];
1406
+ for (const name of alwaysInclude) {
1407
+ if (mcpDiscoveryEnabled && name.startsWith("mcp_")) {
1408
+ continue;
1409
+ }
1410
+ if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
1411
+ initialToolNames.push(name);
1412
+ }
1397
1413
  }
1398
- }
1399
1414
 
1400
- const systemPrompt = await logger.time("buildSystemPrompt", rebuildSystemPrompt, initialToolNames, toolRegistry);
1415
+ const systemPrompt = await logger.time("buildSystemPrompt", rebuildSystemPrompt, initialToolNames, toolRegistry);
1401
1416
 
1402
- const promptTemplates =
1403
- options.promptTemplates ?? (await logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1404
- toolSession.promptTemplates = promptTemplates;
1417
+ const promptTemplates =
1418
+ options.promptTemplates ??
1419
+ (await logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1420
+ toolSession.promptTemplates = promptTemplates;
1405
1421
 
1406
- const slashCommands =
1407
- options.slashCommands ?? (await logger.time("discoverSlashCommands", discoverSlashCommands, cwd));
1422
+ const slashCommands =
1423
+ options.slashCommands ?? (await logger.time("discoverSlashCommands", discoverSlashCommands, cwd));
1408
1424
 
1409
- // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
1410
- const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
1411
- const converted = convertToLlm(messages);
1412
- // Check setting dynamically so mid-session changes take effect
1413
- if (!settings.get("images.blockImages")) {
1414
- return converted;
1415
- }
1416
- // Filter out ImageContent from all messages, replacing with text placeholder
1417
- return converted.map(msg => {
1418
- if (msg.role === "user" || msg.role === "toolResult") {
1419
- const content = msg.content;
1420
- if (Array.isArray(content)) {
1421
- const hasImages = content.some(c => c.type === "image");
1422
- if (hasImages) {
1423
- const filteredContent = content
1424
- .map(c => (c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c))
1425
- .filter((c, i, arr) => {
1426
- // Dedupe consecutive "Image reading is disabled." texts
1427
- if (!(c.type === "text" && c.text === "Image reading is disabled." && i > 0)) return true;
1428
- const prev = arr[i - 1];
1429
- return !(prev.type === "text" && prev.text === "Image reading is disabled.");
1430
- });
1431
- return { ...msg, content: filteredContent };
1425
+ // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
1426
+ const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
1427
+ const converted = convertToLlm(messages);
1428
+ // Check setting dynamically so mid-session changes take effect
1429
+ if (!settings.get("images.blockImages")) {
1430
+ return converted;
1431
+ }
1432
+ // Filter out ImageContent from all messages, replacing with text placeholder
1433
+ return converted.map(msg => {
1434
+ if (msg.role === "user" || msg.role === "toolResult") {
1435
+ const content = msg.content;
1436
+ if (Array.isArray(content)) {
1437
+ const hasImages = content.some(c => c.type === "image");
1438
+ if (hasImages) {
1439
+ const filteredContent = content
1440
+ .map(c =>
1441
+ c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c,
1442
+ )
1443
+ .filter((c, i, arr) => {
1444
+ // Dedupe consecutive "Image reading is disabled." texts
1445
+ if (!(c.type === "text" && c.text === "Image reading is disabled." && i > 0)) return true;
1446
+ const prev = arr[i - 1];
1447
+ return !(prev.type === "text" && prev.text === "Image reading is disabled.");
1448
+ });
1449
+ return { ...msg, content: filteredContent };
1450
+ }
1432
1451
  }
1433
1452
  }
1434
- }
1435
- return msg;
1436
- });
1437
- };
1453
+ return msg;
1454
+ });
1455
+ };
1438
1456
 
1439
- // Final convertToLlm: chain block-images filter with secret obfuscation
1440
- const convertToLlmFinal = (messages: AgentMessage[]): Message[] => {
1441
- const converted = convertToLlmWithBlockImages(messages);
1442
- if (!obfuscator?.hasSecrets()) return converted;
1443
- return obfuscateMessages(obfuscator, converted);
1444
- };
1445
- const transformContext = extensionRunner
1446
- ? async (messages: AgentMessage[], _signal?: AbortSignal) => {
1447
- return await extensionRunner.emitContext(messages);
1448
- }
1449
- : undefined;
1450
- const onPayload = extensionRunner
1451
- ? async (payload: unknown, _model?: Model) => {
1452
- return await extensionRunner.emitBeforeProviderRequest(payload);
1453
- }
1454
- : undefined;
1457
+ // Final convertToLlm: chain block-images filter with secret obfuscation
1458
+ const convertToLlmFinal = (messages: AgentMessage[]): Message[] => {
1459
+ const converted = convertToLlmWithBlockImages(messages);
1460
+ if (!obfuscator?.hasSecrets()) return converted;
1461
+ return obfuscateMessages(obfuscator, converted);
1462
+ };
1463
+ const transformContext = extensionRunner
1464
+ ? async (messages: AgentMessage[], _signal?: AbortSignal) => {
1465
+ return await extensionRunner.emitContext(messages);
1466
+ }
1467
+ : undefined;
1468
+ const onPayload = extensionRunner
1469
+ ? async (payload: unknown, _model?: Model) => {
1470
+ return await extensionRunner.emitBeforeProviderRequest(payload);
1471
+ }
1472
+ : undefined;
1455
1473
 
1456
- const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
1457
- toolContextStore.setUIContext(uiContext, hasUI);
1458
- };
1474
+ const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
1475
+ toolContextStore.setUIContext(uiContext, hasUI);
1476
+ };
1459
1477
 
1460
- const initialTools = initialToolNames
1461
- .map(name => toolRegistry.get(name))
1462
- .filter((tool): tool is AgentTool => tool !== undefined);
1463
-
1464
- const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "auto";
1465
- const preferOpenAICodexWebsockets =
1466
- openaiWebsocketSetting === "on" ? true : openaiWebsocketSetting === "off" ? false : undefined;
1467
- const serviceTierSetting = settings.get("serviceTier");
1468
-
1469
- const initialServiceTier = hasServiceTierEntry
1470
- ? existingSession.serviceTier
1471
- : serviceTierSetting === "none"
1472
- ? undefined
1473
- : serviceTierSetting;
1474
-
1475
- agent = new Agent({
1476
- initialState: {
1477
- systemPrompt,
1478
- model,
1479
- thinkingLevel: toReasoningEffort(thinkingLevel),
1480
- tools: initialTools,
1481
- },
1482
- convertToLlm: convertToLlmFinal,
1483
- onPayload,
1484
- sessionId: providerSessionId,
1485
- transformContext,
1486
- steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
1487
- followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
1488
- interruptMode: settings.get("interruptMode") ?? "immediate",
1489
- thinkingBudgets: settings.getGroup("thinkingBudgets"),
1490
- temperature: settings.get("temperature") >= 0 ? settings.get("temperature") : undefined,
1491
- topP: settings.get("topP") >= 0 ? settings.get("topP") : undefined,
1492
- topK: settings.get("topK") >= 0 ? settings.get("topK") : undefined,
1493
- minP: settings.get("minP") >= 0 ? settings.get("minP") : undefined,
1494
- presencePenalty: settings.get("presencePenalty") >= 0 ? settings.get("presencePenalty") : undefined,
1495
- repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
1496
- serviceTier: initialServiceTier,
1497
- kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
1498
- preferWebsockets: preferOpenAICodexWebsockets,
1499
- getToolContext: tc => toolContextStore.getContext(tc),
1500
- getApiKey: async provider => {
1501
- // Use the provider-facing session id for sticky credential selection so cache keys
1502
- // and provider auth affinity stay aligned across fresh benchmark sessions.
1503
- const key = await modelRegistry.getApiKeyForProvider(provider, providerSessionId);
1504
- if (!key) {
1505
- throw new Error(`No API key found for provider "${provider}"`);
1506
- }
1507
- return key;
1508
- },
1509
- cursorExecHandlers,
1510
- transformToolCallArguments: (args, _toolName) => {
1511
- let result = args;
1512
- const maxTimeout = settings.get("tools.maxTimeout");
1513
- if (maxTimeout > 0 && typeof result.timeout === "number") {
1514
- result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
1515
- }
1516
- if (obfuscator?.hasSecrets()) {
1517
- result = obfuscator.deobfuscateObject(result);
1518
- }
1519
- return result;
1520
- },
1521
- intentTracing: !!intentField,
1522
- getToolChoice: () => session?.nextToolChoice(),
1523
- });
1478
+ const initialTools = initialToolNames
1479
+ .map(name => toolRegistry.get(name))
1480
+ .filter((tool): tool is AgentTool => tool !== undefined);
1481
+
1482
+ const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "off";
1483
+ const preferOpenAICodexWebsockets =
1484
+ openaiWebsocketSetting === "on" ? true : openaiWebsocketSetting === "off" ? false : undefined;
1485
+ const serviceTierSetting = settings.get("serviceTier");
1486
+
1487
+ const initialServiceTier = hasServiceTierEntry
1488
+ ? existingSession.serviceTier
1489
+ : serviceTierSetting === "none"
1490
+ ? undefined
1491
+ : serviceTierSetting;
1492
+
1493
+ agent = new Agent({
1494
+ initialState: {
1495
+ systemPrompt,
1496
+ model,
1497
+ thinkingLevel: toReasoningEffort(thinkingLevel),
1498
+ tools: initialTools,
1499
+ },
1500
+ convertToLlm: convertToLlmFinal,
1501
+ onPayload,
1502
+ sessionId: providerSessionId,
1503
+ transformContext,
1504
+ steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
1505
+ followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
1506
+ interruptMode: settings.get("interruptMode") ?? "immediate",
1507
+ thinkingBudgets: settings.getGroup("thinkingBudgets"),
1508
+ temperature: settings.get("temperature") >= 0 ? settings.get("temperature") : undefined,
1509
+ topP: settings.get("topP") >= 0 ? settings.get("topP") : undefined,
1510
+ topK: settings.get("topK") >= 0 ? settings.get("topK") : undefined,
1511
+ minP: settings.get("minP") >= 0 ? settings.get("minP") : undefined,
1512
+ presencePenalty: settings.get("presencePenalty") >= 0 ? settings.get("presencePenalty") : undefined,
1513
+ repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
1514
+ serviceTier: initialServiceTier,
1515
+ kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
1516
+ preferWebsockets: preferOpenAICodexWebsockets,
1517
+ getToolContext: tc => toolContextStore.getContext(tc),
1518
+ getApiKey: async provider => {
1519
+ // Use the provider-facing session id for sticky credential selection so cache keys
1520
+ // and provider auth affinity stay aligned across fresh benchmark sessions.
1521
+ const key = await modelRegistry.getApiKeyForProvider(provider, providerSessionId);
1522
+ if (!key) {
1523
+ throw new Error(`No API key found for provider "${provider}"`);
1524
+ }
1525
+ return key;
1526
+ },
1527
+ cursorExecHandlers,
1528
+ transformToolCallArguments: (args, _toolName) => {
1529
+ let result = args;
1530
+ const maxTimeout = settings.get("tools.maxTimeout");
1531
+ if (maxTimeout > 0 && typeof result.timeout === "number") {
1532
+ result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
1533
+ }
1534
+ if (obfuscator?.hasSecrets()) {
1535
+ result = obfuscator.deobfuscateObject(result);
1536
+ }
1537
+ return result;
1538
+ },
1539
+ intentTracing: !!intentField,
1540
+ getToolChoice: () => session?.nextToolChoice(),
1541
+ });
1524
1542
 
1525
- cursorEventEmitter = event => agent.emitExternalEvent(event);
1543
+ cursorEventEmitter = event => agent.emitExternalEvent(event);
1526
1544
 
1527
- // Restore messages if session has existing data
1528
- if (hasExistingSession) {
1529
- agent.replaceMessages(existingSession.messages);
1530
- } else {
1531
- // Save initial model and thinking level for new sessions so they can be restored on resume
1532
- if (model) {
1533
- sessionManager.appendModelChange(`${model.provider}/${model.id}`);
1545
+ // Restore messages if session has existing data
1546
+ if (hasExistingSession) {
1547
+ agent.replaceMessages(existingSession.messages);
1548
+ } else {
1549
+ // Save initial model and thinking level for new sessions so they can be restored on resume
1550
+ if (model) {
1551
+ sessionManager.appendModelChange(`${model.provider}/${model.id}`);
1552
+ }
1553
+ sessionManager.appendThinkingLevelChange(thinkingLevel);
1534
1554
  }
1535
- sessionManager.appendThinkingLevelChange(thinkingLevel);
1536
- }
1537
-
1538
- session = new AgentSession({
1539
- agent,
1540
- thinkingLevel,
1541
- sessionManager,
1542
- settings,
1543
- scopedModels: options.scopedModels,
1544
- promptTemplates,
1545
- slashCommands,
1546
- extensionRunner,
1547
- customCommands: customCommandsResult.commands,
1548
- skills,
1549
- skillWarnings,
1550
- skillsSettings: settings.getGroup("skills"),
1551
- modelRegistry,
1552
- toolRegistry,
1553
- transformContext,
1554
- onPayload,
1555
- convertToLlm: convertToLlmFinal,
1556
- rebuildSystemPrompt,
1557
- mcpDiscoveryEnabled,
1558
- initialSelectedMCPToolNames,
1559
- defaultSelectedMCPToolNames,
1560
- persistInitialMCPToolSelection: !hasExistingSession,
1561
- defaultSelectedMCPServerNames: [...discoveryDefaultServers],
1562
- ttsrManager,
1563
- obfuscator,
1564
- asyncJobManager,
1565
- searchDb,
1566
- });
1567
1555
 
1568
- if (model?.api === "openai-codex-responses") {
1569
- const codexModel = model;
1570
- const codexTransport = getOpenAICodexTransportDetails(codexModel, {
1571
- sessionId: providerSessionId,
1572
- baseUrl: codexModel.baseUrl,
1573
- preferWebsockets: preferOpenAICodexWebsockets,
1574
- providerSessionState: session.providerSessionState,
1556
+ session = new AgentSession({
1557
+ agent,
1558
+ thinkingLevel,
1559
+ sessionManager,
1560
+ settings,
1561
+ pythonKernelOwnerId,
1562
+ scopedModels: options.scopedModels,
1563
+ promptTemplates,
1564
+ slashCommands,
1565
+ extensionRunner,
1566
+ customCommands: customCommandsResult.commands,
1567
+ skills,
1568
+ skillWarnings,
1569
+ skillsSettings: settings.getGroup("skills"),
1570
+ modelRegistry,
1571
+ toolRegistry,
1572
+ transformContext,
1573
+ onPayload,
1574
+ convertToLlm: convertToLlmFinal,
1575
+ rebuildSystemPrompt,
1576
+ mcpDiscoveryEnabled,
1577
+ initialSelectedMCPToolNames,
1578
+ defaultSelectedMCPToolNames,
1579
+ persistInitialMCPToolSelection: !hasExistingSession,
1580
+ defaultSelectedMCPServerNames: [...discoveryDefaultServers],
1581
+ ttsrManager,
1582
+ obfuscator,
1583
+ asyncJobManager,
1575
1584
  });
1576
- if (codexTransport.websocketPreferred) {
1577
- void (async () => {
1578
- try {
1579
- const codexPrewarmApiKey = await modelRegistry.getApiKey(codexModel, providerSessionId);
1580
- if (!codexPrewarmApiKey) return;
1581
- await logger.time("prewarmOpenAICodexResponses", prewarmOpenAICodexResponses, codexModel, {
1582
- apiKey: codexPrewarmApiKey,
1583
- sessionId: providerSessionId,
1584
- preferWebsockets: preferOpenAICodexWebsockets,
1585
- providerSessionState: session.providerSessionState,
1586
- });
1587
- } catch (error) {
1588
- const errorMessage = error instanceof Error ? error.message : String(error);
1589
- logger.debug("Codex websocket prewarm failed", {
1590
- error: errorMessage,
1591
- provider: codexModel.provider,
1592
- model: codexModel.id,
1593
- });
1594
- }
1595
- })();
1585
+ hasSession = true;
1586
+
1587
+ if (model?.api === "openai-codex-responses") {
1588
+ const codexModel = model;
1589
+ const codexTransport = getOpenAICodexTransportDetails(codexModel, {
1590
+ sessionId: providerSessionId,
1591
+ baseUrl: codexModel.baseUrl,
1592
+ preferWebsockets: preferOpenAICodexWebsockets,
1593
+ providerSessionState: session.providerSessionState,
1594
+ });
1595
+ if (codexTransport.websocketPreferred) {
1596
+ void (async () => {
1597
+ try {
1598
+ const codexPrewarmApiKey = await modelRegistry.getApiKey(codexModel, providerSessionId);
1599
+ if (!codexPrewarmApiKey) return;
1600
+ await logger.time("prewarmOpenAICodexResponses", prewarmOpenAICodexResponses, codexModel, {
1601
+ apiKey: codexPrewarmApiKey,
1602
+ sessionId: providerSessionId,
1603
+ preferWebsockets: preferOpenAICodexWebsockets,
1604
+ providerSessionState: session.providerSessionState,
1605
+ });
1606
+ } catch (error) {
1607
+ const errorMessage = error instanceof Error ? error.message : String(error);
1608
+ logger.debug("Codex websocket prewarm failed", {
1609
+ error: errorMessage,
1610
+ provider: codexModel.provider,
1611
+ model: codexModel.id,
1612
+ });
1613
+ }
1614
+ })();
1615
+ }
1596
1616
  }
1597
- }
1598
1617
 
1599
- // Start LSP warmup in the background so startup does not block on language server initialization.
1600
- let lspServers: CreateAgentSessionResult["lspServers"];
1601
- if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
1602
- lspServers = discoverStartupLspServers(cwd);
1603
- if (lspServers.length > 0) {
1604
- void (async () => {
1605
- try {
1606
- const result = await logger.time("warmupLspServers", warmupLspServers, cwd);
1607
- const serversByName = new Map(result.servers.map(server => [server.name, server] as const));
1608
- for (const server of lspServers ?? []) {
1609
- const next = serversByName.get(server.name);
1610
- if (!next) continue;
1611
- server.status = next.status;
1612
- server.fileTypes = next.fileTypes;
1613
- server.error = next.error;
1614
- }
1615
- const event: LspStartupEvent = {
1616
- type: "completed",
1617
- servers: result.servers,
1618
- };
1619
- eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
1620
- } catch (error) {
1621
- const errorMessage = error instanceof Error ? error.message : String(error);
1622
- logger.warn("LSP server warmup failed", { cwd, error: errorMessage });
1623
- for (const server of lspServers ?? []) {
1624
- server.status = "error";
1625
- server.error = errorMessage;
1618
+ // Start LSP warmup in the background so startup does not block on language server initialization.
1619
+ let lspServers: CreateAgentSessionResult["lspServers"];
1620
+ if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
1621
+ lspServers = discoverStartupLspServers(cwd);
1622
+ if (lspServers.length > 0) {
1623
+ void (async () => {
1624
+ try {
1625
+ const result = await logger.time("warmupLspServers", warmupLspServers, cwd);
1626
+ const serversByName = new Map(result.servers.map(server => [server.name, server] as const));
1627
+ for (const server of lspServers ?? []) {
1628
+ const next = serversByName.get(server.name);
1629
+ if (!next) continue;
1630
+ server.status = next.status;
1631
+ server.fileTypes = next.fileTypes;
1632
+ server.error = next.error;
1633
+ }
1634
+ const event: LspStartupEvent = {
1635
+ type: "completed",
1636
+ servers: result.servers,
1637
+ };
1638
+ eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
1639
+ } catch (error) {
1640
+ const errorMessage = error instanceof Error ? error.message : String(error);
1641
+ logger.warn("LSP server warmup failed", { cwd, error: errorMessage });
1642
+ for (const server of lspServers ?? []) {
1643
+ server.status = "error";
1644
+ server.error = errorMessage;
1645
+ }
1646
+ const event: LspStartupEvent = {
1647
+ type: "failed",
1648
+ error: errorMessage,
1649
+ };
1650
+ eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
1626
1651
  }
1627
- const event: LspStartupEvent = {
1628
- type: "failed",
1629
- error: errorMessage,
1630
- };
1631
- eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
1632
- }
1633
- })();
1652
+ })();
1653
+ }
1634
1654
  }
1635
- }
1636
1655
 
1637
- logger.time("startMemoryStartupTask", () =>
1638
- startMemoryStartupTask({
1639
- session,
1640
- settings,
1641
- modelRegistry,
1642
- agentDir,
1643
- taskDepth,
1644
- }),
1645
- );
1656
+ logger.time("startMemoryStartupTask", () =>
1657
+ startMemoryStartupTask({
1658
+ session,
1659
+ settings,
1660
+ modelRegistry,
1661
+ agentDir,
1662
+ taskDepth,
1663
+ }),
1664
+ );
1646
1665
 
1647
- // Wire MCP manager callbacks to session for reactive tool updates
1648
- if (mcpManager) {
1649
- mcpManager.setOnToolsChanged(tools => {
1650
- void session.refreshMCPTools(tools);
1651
- });
1652
- // Wire prompt refresh → rebuild MCP prompt slash commands
1653
- mcpManager.setOnPromptsChanged(serverName => {
1654
- const promptCommands = buildMCPPromptCommands(mcpManager);
1655
- session.setMCPPromptCommands(promptCommands);
1656
- logger.debug("MCP prompt commands refreshed", { path: `mcp:${serverName}` });
1657
- });
1658
- const notificationDebounceTimers = new Map<string, Timer>();
1659
- const clearDebounceTimers = () => {
1660
- for (const timer of notificationDebounceTimers.values()) clearTimeout(timer);
1661
- notificationDebounceTimers.clear();
1666
+ // Wire MCP manager callbacks to session for reactive tool updates
1667
+ if (mcpManager) {
1668
+ mcpManager.setOnToolsChanged(tools => {
1669
+ void session.refreshMCPTools(tools);
1670
+ });
1671
+ // Wire prompt refresh → rebuild MCP prompt slash commands
1672
+ mcpManager.setOnPromptsChanged(serverName => {
1673
+ const promptCommands = buildMCPPromptCommands(mcpManager);
1674
+ session.setMCPPromptCommands(promptCommands);
1675
+ logger.debug("MCP prompt commands refreshed", { path: `mcp:${serverName}` });
1676
+ });
1677
+ const notificationDebounceTimers = new Map<string, Timer>();
1678
+ const clearDebounceTimers = () => {
1679
+ for (const timer of notificationDebounceTimers.values()) clearTimeout(timer);
1680
+ notificationDebounceTimers.clear();
1681
+ };
1682
+ postmortem.register("mcp-notification-cleanup", clearDebounceTimers);
1683
+ mcpManager.setOnResourcesChanged((serverName, uri) => {
1684
+ logger.debug("MCP resources changed", { path: `mcp:${serverName}`, uri });
1685
+ if (!settings.get("mcp.notifications")) return;
1686
+ const debounceMs = settings.get("mcp.notificationDebounceMs");
1687
+ const key = `${serverName}:${uri}`;
1688
+ const existing = notificationDebounceTimers.get(key);
1689
+ if (existing) clearTimeout(existing);
1690
+ notificationDebounceTimers.set(
1691
+ key,
1692
+ setTimeout(() => {
1693
+ notificationDebounceTimers.delete(key);
1694
+ // Re-check: user may have disabled notifications during the debounce window
1695
+ if (!settings.get("mcp.notifications")) return;
1696
+ void session.followUp(
1697
+ `[MCP notification] Server "${serverName}" reports resource \`${uri}\` was updated. Use read(path="mcp://${uri}") to inspect if relevant.`,
1698
+ );
1699
+ }, debounceMs),
1700
+ );
1701
+ });
1702
+ }
1703
+
1704
+ logger.time("createAgentSession:return");
1705
+ return {
1706
+ session,
1707
+ extensionsResult,
1708
+ setToolUIContext,
1709
+ mcpManager,
1710
+ modelFallbackMessage,
1711
+ lspServers,
1712
+ eventBus,
1662
1713
  };
1663
- postmortem.register("mcp-notification-cleanup", clearDebounceTimers);
1664
- mcpManager.setOnResourcesChanged((serverName, uri) => {
1665
- logger.debug("MCP resources changed", { path: `mcp:${serverName}`, uri });
1666
- if (!settings.get("mcp.notifications")) return;
1667
- const debounceMs = settings.get("mcp.notificationDebounceMs");
1668
- const key = `${serverName}:${uri}`;
1669
- const existing = notificationDebounceTimers.get(key);
1670
- if (existing) clearTimeout(existing);
1671
- notificationDebounceTimers.set(
1672
- key,
1673
- setTimeout(() => {
1674
- notificationDebounceTimers.delete(key);
1675
- // Re-check: user may have disabled notifications during the debounce window
1676
- if (!settings.get("mcp.notifications")) return;
1677
- void session.followUp(
1678
- `[MCP notification] Server "${serverName}" reports resource \`${uri}\` was updated. Use read(path="mcp://${uri}") to inspect if relevant.`,
1679
- );
1680
- }, debounceMs),
1681
- );
1682
- });
1714
+ } catch (error) {
1715
+ try {
1716
+ if (hasSession) {
1717
+ await session.dispose();
1718
+ } else {
1719
+ await disposeKernelSessionsByOwner(pythonKernelOwnerId);
1720
+ }
1721
+ } catch (cleanupError) {
1722
+ logger.warn("Failed to clean up createAgentSession resources after startup error", {
1723
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
1724
+ });
1725
+ }
1726
+ throw error;
1683
1727
  }
1684
-
1685
- logger.time("createAgentSession:return");
1686
- return {
1687
- session,
1688
- extensionsResult,
1689
- setToolUIContext,
1690
- mcpManager,
1691
- modelFallbackMessage,
1692
- lspServers,
1693
- eventBus,
1694
- };
1695
1728
  }