@oh-my-pi/pi-coding-agent 15.11.6 → 15.11.8
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.
- package/CHANGELOG.md +57 -1
- package/dist/cli.js +431 -381
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/collab/crypto.d.ts +12 -0
- package/dist/types/collab/guest.d.ts +21 -0
- package/dist/types/collab/host.d.ts +13 -0
- package/dist/types/collab/protocol.d.ts +100 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +93 -1
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/modes/components/agent-hub.d.ts +13 -0
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/settings-selector.d.ts +8 -1
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/status-line/component.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +13 -9
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
- package/dist/types/modes/types.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +8 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +1 -0
- package/package.json +14 -12
- package/scripts/bench-guard.ts +71 -0
- package/src/cli/args.ts +2 -0
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/crypto.ts +57 -0
- package/src/collab/guest.ts +421 -0
- package/src/collab/host.ts +494 -0
- package/src/collab/protocol.ts +191 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +74 -19
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +119 -1
- package/src/edit/renderer.ts +5 -0
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +11 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/agent-hub.ts +119 -22
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/oauth-selector.ts +67 -7
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/settings-selector.ts +27 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/status-line/component.ts +21 -1
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +13 -0
- package/src/modes/components/status-line/types.ts +10 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +18 -10
- package/src/modes/controllers/input-controller.ts +80 -12
- package/src/modes/controllers/selector-controller.ts +6 -2
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/setup-wizard/index.ts +1 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
- package/src/modes/setup-wizard/scenes/providers.ts +36 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
- package/src/modes/setup-wizard/scenes/theme.ts +28 -1
- package/src/modes/setup-wizard/scenes/types.ts +10 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
- package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
- package/src/modes/types.ts +8 -0
- package/src/modes/utils/context-usage.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/prompts/bench.md +7 -0
- package/src/sdk.ts +240 -36
- package/src/session/agent-session.ts +22 -0
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +20 -22
- package/src/slash-commands/builtin-registry.ts +210 -0
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/read.ts +38 -5
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/write.ts +13 -42
package/src/modes/types.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Component, Container, EditorTheme, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import type { CollabGuestLink } from "../collab/guest";
|
|
6
|
+
import type { CollabHost } from "../collab/host";
|
|
5
7
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
6
8
|
import type { Settings } from "../config/settings";
|
|
7
9
|
import type {
|
|
@@ -19,6 +21,7 @@ import type { HistoryStorage } from "../session/history-storage";
|
|
|
19
21
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
20
22
|
import type { ShakeMode } from "../session/shake-types";
|
|
21
23
|
import type { LspStartupServerInfo } from "../tools";
|
|
24
|
+
import type { EventBus } from "../utils/event-bus";
|
|
22
25
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
23
26
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
24
27
|
import type { CustomEditor } from "./components/custom-editor";
|
|
@@ -29,6 +32,7 @@ import type { HookSelectorComponent, HookSelectorOptions } from "./components/ho
|
|
|
29
32
|
import type { StatusLineComponent } from "./components/status-line";
|
|
30
33
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
31
34
|
import type { TranscriptContainer } from "./components/transcript-container";
|
|
35
|
+
import type { EventController } from "./controllers/event-controller";
|
|
32
36
|
import type { LoopLimitRuntime } from "./loop-limit";
|
|
33
37
|
import type { OAuthManualInputManager } from "./oauth-manual-input";
|
|
34
38
|
import type { Theme } from "./theme/theme";
|
|
@@ -101,6 +105,10 @@ export interface InteractiveModeContext {
|
|
|
101
105
|
mcpManager?: MCPManager;
|
|
102
106
|
lspServers?: LspStartupServerInfo[];
|
|
103
107
|
titleSystemPrompt?: string;
|
|
108
|
+
collabHost?: CollabHost;
|
|
109
|
+
collabGuest?: CollabGuestLink;
|
|
110
|
+
eventController: EventController;
|
|
111
|
+
eventBus?: EventBus;
|
|
104
112
|
|
|
105
113
|
// State
|
|
106
114
|
isInitialized: boolean;
|
|
@@ -183,7 +183,7 @@ export function computeContextBreakdown(
|
|
|
183
183
|
const renderToolResults = session.settings.get("snapcompact.toolResults");
|
|
184
184
|
if (renderSystemPrompt !== "none" || renderToolResults) {
|
|
185
185
|
snapcompactSavings = estimateInlineSavings({
|
|
186
|
-
options: { renderSystemPrompt, renderToolResults },
|
|
186
|
+
options: { renderSystemPrompt, renderToolResults, shape: session.settings.get("snapcompact.shape") },
|
|
187
187
|
model,
|
|
188
188
|
systemPrompt: session.systemPrompt ?? [],
|
|
189
189
|
messages: session.messages ?? [],
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
|
|
4
5
|
import { settings } from "../../config/settings";
|
|
5
6
|
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
6
7
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
7
8
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
8
9
|
import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
|
|
10
|
+
import { CollabPromptMessageComponent } from "../../modes/components/collab-prompt-message";
|
|
9
11
|
import { CompactionSummaryMessageComponent } from "../../modes/components/compaction-summary-message";
|
|
10
12
|
import { CustomMessageComponent } from "../../modes/components/custom-message";
|
|
11
13
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
@@ -185,6 +187,11 @@ export class UiHelpers {
|
|
|
185
187
|
this.ctx.chatContainer.addChild(component);
|
|
186
188
|
break;
|
|
187
189
|
}
|
|
190
|
+
if (message.customType === COLLAB_PROMPT_MESSAGE_TYPE) {
|
|
191
|
+
const component = new CollabPromptMessageComponent(message as CustomMessage<CollabPromptDetails>);
|
|
192
|
+
this.ctx.chatContainer.addChild(component);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
188
195
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
189
196
|
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
190
197
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Write a continuous, plain-prose technical explanation of how a relational database executes a SQL query: lexing and parsing, semantic analysis, logical plan construction, cost-based optimization, physical operator selection, and row-by-row execution through the iterator model.
|
|
2
|
+
|
|
3
|
+
Form:
|
|
4
|
+
- Plain paragraphs only: no headings, no lists, no code fences, no preamble.
|
|
5
|
+
- Do not wrap up early or summarize; keep writing until you are cut off.
|
|
6
|
+
|
|
7
|
+
Output only the explanation.
|
package/src/sdk.ts
CHANGED
|
@@ -89,7 +89,14 @@ import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal }
|
|
|
89
89
|
import type { HindsightSessionState } from "./hindsight/state";
|
|
90
90
|
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
|
|
91
91
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
92
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
discoverAndLoadMCPTools,
|
|
94
|
+
type MCPLoadResult,
|
|
95
|
+
MCPManager,
|
|
96
|
+
MCPToolCache,
|
|
97
|
+
type MCPToolsLoadResult,
|
|
98
|
+
parseMCPToolName,
|
|
99
|
+
} from "./mcp";
|
|
93
100
|
import { createSessionMemoryRuntimeContext, resolveMemoryBackend } from "./memory-backend";
|
|
94
101
|
import type { MnemopiSessionState } from "./mnemopi/state";
|
|
95
102
|
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
|
|
@@ -147,6 +154,7 @@ import {
|
|
|
147
154
|
type DiscoverableTool,
|
|
148
155
|
filterBySource,
|
|
149
156
|
formatDiscoverableToolServerSummary,
|
|
157
|
+
isMCPToolName,
|
|
150
158
|
selectDiscoverableToolNamesByServer,
|
|
151
159
|
summarizeDiscoverableTools,
|
|
152
160
|
} from "./tool-discovery/tool-index";
|
|
@@ -302,6 +310,76 @@ function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): Agen
|
|
|
302
310
|
};
|
|
303
311
|
}
|
|
304
312
|
|
|
313
|
+
type DeferredMCPActivation = {
|
|
314
|
+
mcpDiscoveryEnabled: boolean;
|
|
315
|
+
explicitlyRequestedMCPToolNames: string[];
|
|
316
|
+
activateAllMCPTools: boolean;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function formatMCPConnectingMessage(serverNames: string[]): string {
|
|
320
|
+
return `Connecting to MCP servers: ${serverNames.join(", ")}…`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function createPendingMCPTool(name: string): Tool {
|
|
324
|
+
const parsed = parseMCPToolName(name);
|
|
325
|
+
const serverName = parsed?.serverName;
|
|
326
|
+
const mcpToolName = parsed?.toolName ?? name;
|
|
327
|
+
const label = serverName ? `${serverName}/${mcpToolName}` : name;
|
|
328
|
+
const message = serverName
|
|
329
|
+
? `MCP server "${serverName}" is still connecting; tool "${name}" is not yet available. Retry after the MCP connection completes.`
|
|
330
|
+
: `MCP discovery is still in progress; tool "${name}" is not yet available. Retry after MCP connection completes.`;
|
|
331
|
+
const tool: Tool & { mcpServerName?: string; mcpToolName?: string } = {
|
|
332
|
+
name,
|
|
333
|
+
label,
|
|
334
|
+
description: `Pending MCP tool. ${message}`,
|
|
335
|
+
parameters: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {},
|
|
338
|
+
additionalProperties: true,
|
|
339
|
+
},
|
|
340
|
+
approval: "write",
|
|
341
|
+
intent: "omit",
|
|
342
|
+
mcpServerName: serverName,
|
|
343
|
+
mcpToolName,
|
|
344
|
+
async execute() {
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text", text: message }],
|
|
347
|
+
details: { serverName, mcpToolName, isError: true },
|
|
348
|
+
isError: true,
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
return tool;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function collectPendingMCPToolNames(
|
|
356
|
+
explicitToolNames: readonly string[] | undefined,
|
|
357
|
+
restoredSelectedToolNames: readonly string[],
|
|
358
|
+
): string[] {
|
|
359
|
+
const names = new Set<string>();
|
|
360
|
+
for (const name of explicitToolNames ?? []) {
|
|
361
|
+
const normalized = name.toLowerCase();
|
|
362
|
+
if (isMCPToolName(normalized)) names.add(normalized);
|
|
363
|
+
}
|
|
364
|
+
for (const name of restoredSelectedToolNames) {
|
|
365
|
+
const normalized = name.toLowerCase();
|
|
366
|
+
if (isMCPToolName(normalized)) names.add(normalized);
|
|
367
|
+
}
|
|
368
|
+
return [...names];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function logMCPLoadErrors(errors: MCPLoadResult["errors"]): void {
|
|
372
|
+
for (const [serverName, error] of errors) {
|
|
373
|
+
logger.error("MCP tool load failed", { path: `mcp:${serverName}`, error });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function applyMCPEnvironment(result: { exaApiKeys: string[] }): void {
|
|
378
|
+
if (result.exaApiKeys.length > 0 && !$env.EXA_API_KEY) {
|
|
379
|
+
Bun.env.EXA_API_KEY = result.exaApiKeys[0];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
305
383
|
// Types
|
|
306
384
|
export interface CreateAgentSessionOptions {
|
|
307
385
|
/** Working directory for project-local discovery. Default: getProjectDir() */
|
|
@@ -1518,46 +1596,131 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1518
1596
|
let mcpManager: MCPManager | undefined = options.mcpManager;
|
|
1519
1597
|
toolSession.mcpManager = mcpManager;
|
|
1520
1598
|
const enableMCP = options.enableMCP ?? true;
|
|
1599
|
+
const deferMCPDiscoveryForUI = enableMCP && !mcpManager && options.hasUI === true;
|
|
1521
1600
|
const customTools: CustomTool[] = [];
|
|
1601
|
+
let startDeferredMCPDiscovery:
|
|
1602
|
+
| ((liveSession: AgentSession, activation: DeferredMCPActivation) => void)
|
|
1603
|
+
| undefined;
|
|
1604
|
+
const onMCPConnecting = (serverNames: string[]) => {
|
|
1605
|
+
if (!options.hasUI || serverNames.length === 0) return;
|
|
1606
|
+
process.stderr.write(`${chalk.gray(formatMCPConnectingMessage(serverNames))}\n`);
|
|
1607
|
+
};
|
|
1608
|
+
const mcpDiscoverOptions = {
|
|
1609
|
+
onConnecting: onMCPConnecting,
|
|
1610
|
+
enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
|
|
1611
|
+
// Always filter Exa - we have native integration
|
|
1612
|
+
filterExa: true,
|
|
1613
|
+
// Filter browser MCP servers when builtin browser tool is active
|
|
1614
|
+
filterBrowser: settings.get("browser.enabled") ?? false,
|
|
1615
|
+
};
|
|
1522
1616
|
if (enableMCP && !mcpManager) {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
// Filter browser MCP servers when builtin browser tool is active
|
|
1533
|
-
filterBrowser: settings.get("browser.enabled") ?? false,
|
|
1534
|
-
cacheStorage: settings.getStorage(),
|
|
1535
|
-
authStorage,
|
|
1536
|
-
});
|
|
1537
|
-
mcpManager = mcpResult.manager;
|
|
1538
|
-
toolSession.mcpManager = mcpManager;
|
|
1617
|
+
if (deferMCPDiscoveryForUI) {
|
|
1618
|
+
const cacheStorage = settings.getStorage();
|
|
1619
|
+
mcpManager = new MCPManager(cwd, cacheStorage ? new MCPToolCache(cacheStorage) : null);
|
|
1620
|
+
mcpManager.setAuthStorage(authStorage);
|
|
1621
|
+
toolSession.mcpManager = mcpManager;
|
|
1622
|
+
|
|
1623
|
+
if (settings.get("mcp.notifications")) {
|
|
1624
|
+
mcpManager.setNotificationsEnabled(true);
|
|
1625
|
+
}
|
|
1539
1626
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1627
|
+
const deferredMCPManager = mcpManager;
|
|
1628
|
+
startDeferredMCPDiscovery = (liveSession, activation) => {
|
|
1629
|
+
void (async () => {
|
|
1630
|
+
try {
|
|
1631
|
+
const mcpResult = await logger.time("discoverAndLoadMCPTools", () =>
|
|
1632
|
+
deferredMCPManager.discoverAndConnect(mcpDiscoverOptions),
|
|
1633
|
+
);
|
|
1634
|
+
// The session can be torn down while servers are still connecting.
|
|
1635
|
+
// Don't resurrect tools on a disposed session, and don't leak the
|
|
1636
|
+
// transports/subprocesses the connect just spawned.
|
|
1637
|
+
if (liveSession.isDisposed) {
|
|
1638
|
+
await deferredMCPManager.disconnectAll();
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
applyMCPEnvironment(mcpResult);
|
|
1642
|
+
logMCPLoadErrors(mcpResult.errors);
|
|
1643
|
+
// `tools.discoveryMode: "auto"` was resolved against a registry that
|
|
1644
|
+
// held only built-ins plus persisted placeholder names. Recompute with
|
|
1645
|
+
// the real MCP tool count: a large toolset must flip discovery on
|
|
1646
|
+
// BEFORE the refresh, or activateAll would dump every MCP tool into
|
|
1647
|
+
// the active set with no search_tool_bm25 registered.
|
|
1648
|
+
let discoveryEnabled = activation.mcpDiscoveryEnabled;
|
|
1649
|
+
let activateAll = activation.activateAllMCPTools;
|
|
1650
|
+
if (!discoveryEnabled) {
|
|
1651
|
+
const nonMCPToolNames = [...toolRegistry.keys()].filter(name => !isMCPToolName(name));
|
|
1652
|
+
const projectedMode = resolveEffectiveToolDiscoveryMode(
|
|
1653
|
+
settings,
|
|
1654
|
+
countToolsForAutoDiscovery([...nonMCPToolNames, ...mcpResult.tools.map(tool => tool.name)]),
|
|
1655
|
+
);
|
|
1656
|
+
if (projectedMode !== "off") {
|
|
1657
|
+
effectiveDiscoveryMode = projectedMode;
|
|
1658
|
+
mcpDiscoveryEnabled = true;
|
|
1659
|
+
discoveryEnabled = true;
|
|
1660
|
+
activateAll = false;
|
|
1661
|
+
liveSession.enableMCPDiscovery();
|
|
1662
|
+
if (!toolRegistry.has("search_tool_bm25")) {
|
|
1663
|
+
const searchTool: Tool = new SearchToolBm25Tool(toolSession);
|
|
1664
|
+
toolRegistry.set(
|
|
1665
|
+
searchTool.name,
|
|
1666
|
+
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
await liveSession.setActiveToolsByName([
|
|
1670
|
+
...liveSession.getActiveToolNames(),
|
|
1671
|
+
"search_tool_bm25",
|
|
1672
|
+
]);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
await liveSession.refreshMCPTools(mcpResult.tools, { activateAll });
|
|
1676
|
+
if (activation.explicitlyRequestedMCPToolNames.length > 0) {
|
|
1677
|
+
if (discoveryEnabled && !activation.mcpDiscoveryEnabled) {
|
|
1678
|
+
// Discovery flipped on mid-flight: route the explicit request
|
|
1679
|
+
// through discovery-aware activation so selection persists.
|
|
1680
|
+
await liveSession.activateDiscoveredMCPTools(activation.explicitlyRequestedMCPToolNames);
|
|
1681
|
+
} else if (!discoveryEnabled) {
|
|
1682
|
+
await liveSession.setActiveToolsByName([
|
|
1683
|
+
...liveSession.getActiveToolNames(),
|
|
1684
|
+
...activation.explicitlyRequestedMCPToolNames,
|
|
1685
|
+
]);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
logger.error("MCP tool load failed", {
|
|
1690
|
+
path: ".mcp.json",
|
|
1691
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
})();
|
|
1695
|
+
};
|
|
1696
|
+
} else {
|
|
1697
|
+
const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
|
|
1698
|
+
...mcpDiscoverOptions,
|
|
1699
|
+
cacheStorage: settings.getStorage(),
|
|
1700
|
+
authStorage,
|
|
1701
|
+
});
|
|
1702
|
+
mcpManager = mcpResult.manager;
|
|
1703
|
+
toolSession.mcpManager = mcpManager;
|
|
1547
1704
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1705
|
+
if (settings.get("mcp.notifications")) {
|
|
1706
|
+
mcpManager.setNotificationsEnabled(true);
|
|
1707
|
+
}
|
|
1708
|
+
applyMCPEnvironment(mcpResult);
|
|
1552
1709
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1710
|
+
// Log MCP errors
|
|
1711
|
+
for (const { path, error } of mcpResult.errors) {
|
|
1712
|
+
logger.error("MCP tool load failed", { path, error });
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (mcpResult.tools.length > 0) {
|
|
1716
|
+
// MCP tools are LoadedCustomTool, extract the tool property
|
|
1717
|
+
customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
|
|
1718
|
+
}
|
|
1556
1719
|
}
|
|
1557
1720
|
}
|
|
1558
1721
|
// Only top-level sessions own the global MCPManager. Subagents already
|
|
1559
1722
|
// receive the parent's manager via `options.mcpManager`, and reassigning
|
|
1560
|
-
// the singleton to the same value is a no-op
|
|
1723
|
+
// the singleton to the same value is a no-op — keep the gate explicit
|
|
1561
1724
|
// to mirror the AsyncJobManager ownership rule.
|
|
1562
1725
|
if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
|
|
1563
1726
|
|
|
@@ -1852,6 +2015,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1852
2015
|
for (const tool of wrappedExtensionTools) {
|
|
1853
2016
|
toolRegistry.set(tool.name, tool);
|
|
1854
2017
|
}
|
|
2018
|
+
if (deferMCPDiscoveryForUI && mcpManager) {
|
|
2019
|
+
for (const name of collectPendingMCPToolNames(options.toolNames, existingSession.selectedMCPToolNames)) {
|
|
2020
|
+
if (!toolRegistry.has(name)) {
|
|
2021
|
+
toolRegistry.set(name, createPendingMCPTool(name));
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1855
2026
|
// Wrap every tool with `ExtensionToolWrapper` so the per-tool approval gate runs on every
|
|
1856
2027
|
// call site, regardless of whether any user extensions are loaded. See the runner-construction
|
|
1857
2028
|
// comment above for the safety invariant this enforces.
|
|
@@ -1879,7 +2050,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1879
2050
|
}
|
|
1880
2051
|
}
|
|
1881
2052
|
|
|
1882
|
-
|
|
2053
|
+
// `let`: the deferred MCP discovery closure upgrades these when the real
|
|
2054
|
+
// MCP tool count pushes `auto` past its threshold; `rebuildSystemPrompt`
|
|
2055
|
+
// below reads the live bindings.
|
|
2056
|
+
let effectiveDiscoveryMode = resolveEffectiveToolDiscoveryMode(
|
|
1883
2057
|
settings,
|
|
1884
2058
|
countToolsForAutoDiscovery(toolRegistry.keys()),
|
|
1885
2059
|
);
|
|
@@ -1890,7 +2064,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1890
2064
|
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
1891
2065
|
);
|
|
1892
2066
|
}
|
|
1893
|
-
|
|
2067
|
+
let mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1894
2068
|
|
|
1895
2069
|
const reloadSshTool = async (): Promise<AgentTool | null> => {
|
|
1896
2070
|
if (!requestedToolNameSet.has("ssh")) return null;
|
|
@@ -1942,7 +2116,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1942
2116
|
const memoryBackend = await resolveMemoryBackend(settings);
|
|
1943
2117
|
const memoryInstructions = await memoryBackend.buildDeveloperInstructions(agentDir, settings, session);
|
|
1944
2118
|
|
|
1945
|
-
// Build combined append prompt: memory instructions + MCP server instructions
|
|
2119
|
+
// Build combined append prompt: memory instructions + MCP server instructions.
|
|
2120
|
+
// For UI sessions MCP discovery is deferred, so `getServerInstructions()` is
|
|
2121
|
+
// empty until the background connect completes; the rebuild that
|
|
2122
|
+
// `refreshMCPTools` triggers post-discovery then picks up the now-connected
|
|
2123
|
+
// servers' instructions, so they join the prompt for the rest of the session.
|
|
1946
2124
|
const serverInstructions = mcpManager?.getServerInstructions();
|
|
1947
2125
|
let appendPrompt: string | undefined = memoryInstructions ?? undefined;
|
|
1948
2126
|
if (serverInstructions && serverInstructions.size > 0) {
|
|
@@ -2168,6 +2346,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2168
2346
|
? new SnapcompactInlineTransformer({
|
|
2169
2347
|
renderSystemPrompt: snapcompactSystemPromptMode,
|
|
2170
2348
|
renderToolResults: settings.get("snapcompact.toolResults"),
|
|
2349
|
+
shape: settings.get("snapcompact.shape"),
|
|
2171
2350
|
})
|
|
2172
2351
|
: undefined;
|
|
2173
2352
|
const transformProviderContext =
|
|
@@ -2493,7 +2672,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2493
2672
|
// Skip when reusing a parent's manager — the parent owns the callbacks.
|
|
2494
2673
|
if (mcpManager && !options.mcpManager) {
|
|
2495
2674
|
mcpManager.setOnToolsChanged(tools => {
|
|
2496
|
-
void
|
|
2675
|
+
void (async () => {
|
|
2676
|
+
try {
|
|
2677
|
+
await session.refreshMCPTools(
|
|
2678
|
+
tools,
|
|
2679
|
+
deferMCPDiscoveryForUI && !mcpDiscoveryEnabled && options.toolNames === undefined
|
|
2680
|
+
? { activateAll: true }
|
|
2681
|
+
: undefined,
|
|
2682
|
+
);
|
|
2683
|
+
if (deferMCPDiscoveryForUI && !mcpDiscoveryEnabled && explicitlyRequestedMCPToolNames.length > 0) {
|
|
2684
|
+
await session.setActiveToolsByName([
|
|
2685
|
+
...session.getActiveToolNames(),
|
|
2686
|
+
...explicitlyRequestedMCPToolNames,
|
|
2687
|
+
]);
|
|
2688
|
+
}
|
|
2689
|
+
} catch (error) {
|
|
2690
|
+
logger.warn("MCP tool refresh failed", {
|
|
2691
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
})();
|
|
2497
2695
|
});
|
|
2498
2696
|
// Wire prompt refresh → rebuild MCP prompt slash commands
|
|
2499
2697
|
mcpManager.setOnPromptsChanged(serverName => {
|
|
@@ -2526,6 +2724,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2526
2724
|
});
|
|
2527
2725
|
}
|
|
2528
2726
|
|
|
2727
|
+
startDeferredMCPDiscovery?.(session, {
|
|
2728
|
+
mcpDiscoveryEnabled,
|
|
2729
|
+
explicitlyRequestedMCPToolNames,
|
|
2730
|
+
activateAllMCPTools: !mcpDiscoveryEnabled && options.toolNames === undefined,
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2529
2733
|
return {
|
|
2530
2734
|
session,
|
|
2531
2735
|
extensionsResult,
|
|
@@ -3140,6 +3140,12 @@ export class AgentSession {
|
|
|
3140
3140
|
state.resetConversationTracking();
|
|
3141
3141
|
}
|
|
3142
3142
|
|
|
3143
|
+
/** True once dispose() has begun; deferred background work (e.g. the deferred
|
|
3144
|
+
* MCP discovery task in sdk.ts) must not touch the session past this point. */
|
|
3145
|
+
get isDisposed(): boolean {
|
|
3146
|
+
return this.#isDisposed;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3143
3149
|
/**
|
|
3144
3150
|
* Synchronously mark the session as disposing so new work is rejected
|
|
3145
3151
|
* immediately: Python/eval starts throw, queued asides are dropped, and the
|
|
@@ -3473,6 +3479,17 @@ export class AgentSession {
|
|
|
3473
3479
|
return this.#mcpDiscoveryEnabled;
|
|
3474
3480
|
}
|
|
3475
3481
|
|
|
3482
|
+
/**
|
|
3483
|
+
* Flip MCP discovery on after deferred discovery learns the real tool count.
|
|
3484
|
+
* UI sessions resolve `tools.discoveryMode: "auto"` before MCP servers
|
|
3485
|
+
* connect, so a large MCP toolset discovered later must be able to upgrade
|
|
3486
|
+
* the session from the force-activate path to the discovery path. One-way:
|
|
3487
|
+
* discovery is never downgraded mid-session.
|
|
3488
|
+
*/
|
|
3489
|
+
enableMCPDiscovery(): void {
|
|
3490
|
+
this.#mcpDiscoveryEnabled = true;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3476
3493
|
getSelectedMCPToolNames(): string[] {
|
|
3477
3494
|
if (!this.#mcpDiscoveryEnabled) {
|
|
3478
3495
|
return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
|
|
@@ -6388,6 +6405,10 @@ export class AgentSession {
|
|
|
6388
6405
|
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
6389
6406
|
convertToLlm,
|
|
6390
6407
|
model: this.model,
|
|
6408
|
+
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
6409
|
+
// Providers with hard image caps (OpenRouter: 8) silently drop
|
|
6410
|
+
// frames past the cap — keep the archive within budget.
|
|
6411
|
+
maxFrames: snapcompact.providerFrameBudget(this.model.provider),
|
|
6391
6412
|
});
|
|
6392
6413
|
summary = snapcompactResult.summary;
|
|
6393
6414
|
shortSummary = snapcompactResult.shortSummary;
|
|
@@ -7921,6 +7942,7 @@ export class AgentSession {
|
|
|
7921
7942
|
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
7922
7943
|
convertToLlm,
|
|
7923
7944
|
model: this.model,
|
|
7945
|
+
maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
|
|
7924
7946
|
});
|
|
7925
7947
|
summary = snapcompactResult.summary;
|
|
7926
7948
|
shortSummary = snapcompactResult.shortSummary;
|
|
@@ -1994,6 +1994,12 @@ export class SessionManager {
|
|
|
1994
1994
|
#byId: Map<string, SessionEntry> = new Map();
|
|
1995
1995
|
#labelsById: Map<string, string> = new Map();
|
|
1996
1996
|
#leafId: string | null = null;
|
|
1997
|
+
/**
|
|
1998
|
+
* Collab replication tap: invoked for every appended entry with the
|
|
1999
|
+
* in-memory (pre-blob-externalization) entry, so inline images survive.
|
|
2000
|
+
* Failures are swallowed — a broadcast error must never break persistence.
|
|
2001
|
+
*/
|
|
2002
|
+
onEntryAppended?: (entry: SessionEntry) => void;
|
|
1997
2003
|
#usageStatistics = {
|
|
1998
2004
|
input: 0,
|
|
1999
2005
|
output: 0,
|
|
@@ -2927,6 +2933,44 @@ export class SessionManager {
|
|
|
2927
2933
|
this.#usageStatistics.cost += usage.cost.total;
|
|
2928
2934
|
}
|
|
2929
2935
|
}
|
|
2936
|
+
if (this.onEntryAppended) {
|
|
2937
|
+
try {
|
|
2938
|
+
this.onEntryAppended(entry);
|
|
2939
|
+
} catch (err) {
|
|
2940
|
+
logger.warn("collab entry hook failed", { error: String(err) });
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
/**
|
|
2946
|
+
* Append a foreign (host-authored) entry verbatim, preserving its
|
|
2947
|
+
* `id`/`parentId` — no id minting. Used by collab guests to mirror the
|
|
2948
|
+
* host session into the local replica file.
|
|
2949
|
+
*/
|
|
2950
|
+
ingestReplicatedEntry(entry: SessionEntry): void {
|
|
2951
|
+
this.#appendEntry(entry);
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
/**
|
|
2955
|
+
* Snapshot the session for collab replication: the live header plus a deep
|
|
2956
|
+
* copy of every entry (the host mutates entries in place on
|
|
2957
|
+
* truncation/rewrite paths, so guests must not share references).
|
|
2958
|
+
*/
|
|
2959
|
+
snapshotForReplication(): { header: SessionHeader; entries: SessionEntry[] } {
|
|
2960
|
+
const live = this.getHeader();
|
|
2961
|
+
const header: SessionHeader = live
|
|
2962
|
+
? structuredClone(live)
|
|
2963
|
+
: {
|
|
2964
|
+
type: "session",
|
|
2965
|
+
version: CURRENT_SESSION_VERSION,
|
|
2966
|
+
id: this.#sessionId,
|
|
2967
|
+
title: this.#sessionName,
|
|
2968
|
+
titleSource: this.#titleSource,
|
|
2969
|
+
timestamp: new Date().toISOString(),
|
|
2970
|
+
cwd: this.cwd,
|
|
2971
|
+
};
|
|
2972
|
+
const entries = structuredClone(this.#fileEntries.filter(e => e.type !== "session")) as SessionEntry[];
|
|
2973
|
+
return { header, entries };
|
|
2930
2974
|
}
|
|
2931
2975
|
|
|
2932
2976
|
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
@@ -28,22 +28,15 @@ export type SnapcompactSystemPromptMode = "none" | "agents-md" | "all";
|
|
|
28
28
|
export interface SnapcompactInlineOptions {
|
|
29
29
|
renderSystemPrompt: SnapcompactSystemPromptMode;
|
|
30
30
|
renderToolResults: boolean;
|
|
31
|
+
/** Frame variant override; `"auto"`/omitted picks the provider's eval winner. */
|
|
32
|
+
shape?: snapcompact.ShapeVariantName | "auto";
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const INLINE_IMAGE_BUDGET_BY_PROVIDER: Record<string, number> = {
|
|
39
|
-
anthropic: 90,
|
|
40
|
-
"amazon-bedrock": 90,
|
|
41
|
-
openai: 200,
|
|
42
|
-
google: 200,
|
|
43
|
-
"google-vertex": 200,
|
|
44
|
-
"google-gemini-cli": 200,
|
|
45
|
-
};
|
|
46
|
-
const DEFAULT_INLINE_IMAGE_BUDGET = 5;
|
|
35
|
+
// Per-provider image-count budgets live in @oh-my-pi/snapcompact
|
|
36
|
+
// (`providerImageBudget`): snapcompact frames are 1568px (<2000px) so
|
|
37
|
+
// dimension/size limits never bind; only COUNT does. Once the budget is
|
|
38
|
+
// spent (e.g. OpenRouter's hard 8-image cap, already consumed by archive
|
|
39
|
+
// frames), tool results ship verbatim as text.
|
|
47
40
|
const MAX_SYSTEM_PROMPT_FRAMES = 6;
|
|
48
41
|
/** Tool results under this many tokens are never rasterized — the swap can't
|
|
49
42
|
* save enough to justify trading crisp text for an image. */
|
|
@@ -130,14 +123,16 @@ function selectSystemPromptImageTarget(
|
|
|
130
123
|
|
|
131
124
|
/** Tool-result swap candidate, in context order. */
|
|
132
125
|
export interface InlineToolResultCandidate {
|
|
133
|
-
/**
|
|
126
|
+
/** Stable identifier for rendering cache key and applying the swap. */
|
|
134
127
|
id: string;
|
|
135
|
-
/** Token count of the joined text blocks (0
|
|
128
|
+
/** Token count of the joined text blocks (0 for empty or image-carrying). */
|
|
136
129
|
textTokens: number;
|
|
137
|
-
/** Frames needed to render the text (0 = empty
|
|
130
|
+
/** Frames needed to render the text (0 = empty, below floor, image-carrying, or error). */
|
|
138
131
|
frames: number;
|
|
139
132
|
/** Already carries an image (screenshot etc.) — never re-imaged. */
|
|
140
133
|
hasImage: boolean;
|
|
134
|
+
/** Error tool results must stay text-only for provider API validation. */
|
|
135
|
+
isError?: boolean;
|
|
141
136
|
}
|
|
142
137
|
|
|
143
138
|
export interface InlineSystemPromptCandidate {
|
|
@@ -180,6 +175,7 @@ export function planInlineSwaps(input: InlinePlanInput): InlineSwapPlan {
|
|
|
180
175
|
for (let k = 0; k < input.toolResults.length - 1 && budget > 0; k++) {
|
|
181
176
|
const candidate = input.toolResults[k];
|
|
182
177
|
if (candidate.hasImage) continue;
|
|
178
|
+
if (candidate.isError) continue;
|
|
183
179
|
if (candidate.textTokens < MIN_TOOL_RESULT_TOKENS) continue;
|
|
184
180
|
if (candidate.frames === 0 || candidate.frames > budget) continue;
|
|
185
181
|
if (!passesSavingsGate(candidate.frames, input.shape, candidate.textTokens)) continue;
|
|
@@ -218,6 +214,7 @@ export interface InlineMessageView {
|
|
|
218
214
|
role: string;
|
|
219
215
|
toolCallId?: string;
|
|
220
216
|
content?: unknown;
|
|
217
|
+
isError?: boolean;
|
|
221
218
|
}
|
|
222
219
|
|
|
223
220
|
export interface SnapcompactSavingsEstimate {
|
|
@@ -273,7 +270,7 @@ export function estimateInlineSavings(input: {
|
|
|
273
270
|
return { visionCapable: false, savedTokens: 0 };
|
|
274
271
|
}
|
|
275
272
|
|
|
276
|
-
const shape = snapcompact.resolveShape(model.
|
|
273
|
+
const shape = snapcompact.resolveShape(model, options.shape);
|
|
277
274
|
let existingImages = 0;
|
|
278
275
|
for (const message of input.messages) {
|
|
279
276
|
if (!Array.isArray(message.content)) continue;
|
|
@@ -281,12 +278,13 @@ export function estimateInlineSavings(input: {
|
|
|
281
278
|
if (block.type === "image") existingImages++;
|
|
282
279
|
}
|
|
283
280
|
}
|
|
284
|
-
const budget = (
|
|
281
|
+
const budget = snapcompact.providerImageBudget(model.provider) - existingImages;
|
|
285
282
|
|
|
286
283
|
const candidates: InlineToolResultCandidate[] = [];
|
|
287
284
|
if (options.renderToolResults) {
|
|
288
285
|
for (const message of input.messages) {
|
|
289
286
|
if (message.role !== "toolResult" || typeof message.toolCallId !== "string") continue;
|
|
287
|
+
if (message.isError) continue;
|
|
290
288
|
const blocks: BlockViews = Array.isArray(message.content) ? (message.content as BlockViews) : [];
|
|
291
289
|
const hasImage = blocks.some(block => block.type === "image");
|
|
292
290
|
const text = hasImage
|
|
@@ -407,9 +405,8 @@ export class SnapcompactInlineTransformer {
|
|
|
407
405
|
// rendering would lose the content entirely.
|
|
408
406
|
if (!model.input.includes("image")) return context;
|
|
409
407
|
|
|
410
|
-
const shape = snapcompact.resolveShape(model.
|
|
411
|
-
const budget =
|
|
412
|
-
(INLINE_IMAGE_BUDGET_BY_PROVIDER[model.provider] ?? DEFAULT_INLINE_IMAGE_BUDGET) - countContextImages(context);
|
|
408
|
+
const shape = snapcompact.resolveShape(model, this.options.shape);
|
|
409
|
+
const budget = snapcompact.providerImageBudget(model.provider) - countContextImages(context);
|
|
413
410
|
if (budget <= 0) return context;
|
|
414
411
|
|
|
415
412
|
const messages = [...context.messages];
|
|
@@ -426,6 +423,7 @@ export class SnapcompactInlineTransformer {
|
|
|
426
423
|
liveToolCallIds.add(message.toolCallId);
|
|
427
424
|
// Don't re-image results that already carry images (screenshots etc.).
|
|
428
425
|
const hasImage = message.content.some(block => block.type === "image");
|
|
426
|
+
if (message.isError) continue;
|
|
429
427
|
const text = hasImage
|
|
430
428
|
? ""
|
|
431
429
|
: message.content
|