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