@oh-my-pi/pi-coding-agent 15.11.7 → 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 +30 -2
- package/dist/cli.js +363 -356
- package/dist/types/cli/args.d.ts +2 -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/join.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +21 -1
- 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/segment-track.d.ts +11 -6
- 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/interactive-mode.d.ts +7 -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 +6 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/package.json +14 -12
- package/scripts/bench-guard.ts +71 -0
- package/src/cli/args.ts +2 -0
- package/src/cli-commands.ts +1 -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/join.ts +39 -0
- package/src/config/model-registry.ts +22 -14
- package/src/config/settings-schema.ts +27 -1
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/main.ts +11 -2
- 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/segment-track.ts +44 -7
- 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/controllers/input-controller.ts +72 -6
- package/src/modes/controllers/selector-controller.ts +2 -0
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +12 -4
- package/src/modes/types.ts +8 -0
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/sdk.ts +239 -36
- package/src/session/agent-session.ts +17 -0
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +9 -3
- package/src/slash-commands/builtin-registry.ts +210 -0
- package/src/tools/read.ts +38 -5
- package/src/tools/write.ts +13 -42
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) {
|
|
@@ -2494,7 +2672,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2494
2672
|
// Skip when reusing a parent's manager — the parent owns the callbacks.
|
|
2495
2673
|
if (mcpManager && !options.mcpManager) {
|
|
2496
2674
|
mcpManager.setOnToolsChanged(tools => {
|
|
2497
|
-
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
|
+
})();
|
|
2498
2695
|
});
|
|
2499
2696
|
// Wire prompt refresh → rebuild MCP prompt slash commands
|
|
2500
2697
|
mcpManager.setOnPromptsChanged(serverName => {
|
|
@@ -2527,6 +2724,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2527
2724
|
});
|
|
2528
2725
|
}
|
|
2529
2726
|
|
|
2727
|
+
startDeferredMCPDiscovery?.(session, {
|
|
2728
|
+
mcpDiscoveryEnabled,
|
|
2729
|
+
explicitlyRequestedMCPToolNames,
|
|
2730
|
+
activateAllMCPTools: !mcpDiscoveryEnabled && options.toolNames === undefined,
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2530
2733
|
return {
|
|
2531
2734
|
session,
|
|
2532
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));
|
|
@@ -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.
|
|
@@ -123,14 +123,16 @@ function selectSystemPromptImageTarget(
|
|
|
123
123
|
|
|
124
124
|
/** Tool-result swap candidate, in context order. */
|
|
125
125
|
export interface InlineToolResultCandidate {
|
|
126
|
-
/**
|
|
126
|
+
/** Stable identifier for rendering cache key and applying the swap. */
|
|
127
127
|
id: string;
|
|
128
|
-
/** Token count of the joined text blocks (0
|
|
128
|
+
/** Token count of the joined text blocks (0 for empty or image-carrying). */
|
|
129
129
|
textTokens: number;
|
|
130
|
-
/** Frames needed to render the text (0 = empty
|
|
130
|
+
/** Frames needed to render the text (0 = empty, below floor, image-carrying, or error). */
|
|
131
131
|
frames: number;
|
|
132
132
|
/** Already carries an image (screenshot etc.) — never re-imaged. */
|
|
133
133
|
hasImage: boolean;
|
|
134
|
+
/** Error tool results must stay text-only for provider API validation. */
|
|
135
|
+
isError?: boolean;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
export interface InlineSystemPromptCandidate {
|
|
@@ -173,6 +175,7 @@ export function planInlineSwaps(input: InlinePlanInput): InlineSwapPlan {
|
|
|
173
175
|
for (let k = 0; k < input.toolResults.length - 1 && budget > 0; k++) {
|
|
174
176
|
const candidate = input.toolResults[k];
|
|
175
177
|
if (candidate.hasImage) continue;
|
|
178
|
+
if (candidate.isError) continue;
|
|
176
179
|
if (candidate.textTokens < MIN_TOOL_RESULT_TOKENS) continue;
|
|
177
180
|
if (candidate.frames === 0 || candidate.frames > budget) continue;
|
|
178
181
|
if (!passesSavingsGate(candidate.frames, input.shape, candidate.textTokens)) continue;
|
|
@@ -211,6 +214,7 @@ export interface InlineMessageView {
|
|
|
211
214
|
role: string;
|
|
212
215
|
toolCallId?: string;
|
|
213
216
|
content?: unknown;
|
|
217
|
+
isError?: boolean;
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
export interface SnapcompactSavingsEstimate {
|
|
@@ -280,6 +284,7 @@ export function estimateInlineSavings(input: {
|
|
|
280
284
|
if (options.renderToolResults) {
|
|
281
285
|
for (const message of input.messages) {
|
|
282
286
|
if (message.role !== "toolResult" || typeof message.toolCallId !== "string") continue;
|
|
287
|
+
if (message.isError) continue;
|
|
283
288
|
const blocks: BlockViews = Array.isArray(message.content) ? (message.content as BlockViews) : [];
|
|
284
289
|
const hasImage = blocks.some(block => block.type === "image");
|
|
285
290
|
const text = hasImage
|
|
@@ -418,6 +423,7 @@ export class SnapcompactInlineTransformer {
|
|
|
418
423
|
liveToolCallIds.add(message.toolCallId);
|
|
419
424
|
// Don't re-image results that already carry images (screenshots etc.).
|
|
420
425
|
const hasImage = message.content.some(block => block.type === "image");
|
|
426
|
+
if (message.isError) continue;
|
|
421
427
|
const text = hasImage
|
|
422
428
|
? ""
|
|
423
429
|
: message.content
|