@oh-my-pi/pi-coding-agent 14.7.1 → 14.7.2
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 +15 -0
- package/package.json +7 -7
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +108 -22
- package/src/config/settings-schema.ts +36 -1
- package/src/discovery/helpers.ts +4 -3
- package/src/edit/index.ts +1 -0
- package/src/eval/py/gateway-coordinator.ts +2 -3
- package/src/eval/py/runtime.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +2 -0
- package/src/mcp/discoverable-tool-metadata.ts +24 -202
- package/src/modes/components/extensions/extension-dashboard.ts +26 -2
- package/src/modes/components/extensions/state-manager.ts +41 -0
- package/src/modes/controllers/selector-controller.ts +3 -0
- package/src/modes/interactive-mode.ts +26 -1
- package/src/prompts/tools/search-tool-bm25.md +14 -14
- package/src/prompts/tools/todo-write.md +1 -0
- package/src/sdk.ts +69 -8
- package/src/session/agent-session.ts +177 -1
- package/src/slash-commands/builtin-registry.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/tool-discovery/tool-index.ts +377 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/ast-edit.ts +2 -0
- package/src/tools/ast-grep.ts +2 -0
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +2 -0
- package/src/tools/calculator.ts +2 -0
- package/src/tools/checkpoint.ts +4 -0
- package/src/tools/debug.ts +2 -0
- package/src/tools/eval.ts +2 -0
- package/src/tools/find.ts +2 -0
- package/src/tools/gh.ts +2 -0
- package/src/tools/hindsight-recall.ts +2 -0
- package/src/tools/hindsight-reflect.ts +2 -0
- package/src/tools/hindsight-retain.ts +2 -0
- package/src/tools/index.ts +74 -14
- package/src/tools/inspect-image.ts +2 -0
- package/src/tools/irc.ts +2 -1
- package/src/tools/job.ts +2 -1
- package/src/tools/notebook.ts +2 -0
- package/src/tools/read.ts +1 -0
- package/src/tools/recipe/index.ts +2 -0
- package/src/tools/render-mermaid.ts +2 -0
- package/src/tools/search-tool-bm25.ts +128 -42
- package/src/tools/search.ts +2 -0
- package/src/tools/ssh.ts +2 -0
- package/src/tools/todo-write.ts +2 -1
- package/src/tools/write.ts +2 -0
- package/src/web/search/index.ts +2 -0
- package/src/web/search/providers/searxng.ts +8 -0
|
@@ -509,6 +509,47 @@ export function filterByProvider(extensions: Extension[], providerId: string): E
|
|
|
509
509
|
return extensions.filter(ext => ext.source.provider === providerId);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
function isShadowedExtension(ext: Extension): boolean {
|
|
513
|
+
if (ext.shadowedBy) return true;
|
|
514
|
+
return Boolean((ext.raw as { _shadowed?: boolean } | null | undefined)?._shadowed);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Apply setting-backed item disable overrides to an existing dashboard state.
|
|
519
|
+
* This gives the UI immediate feedback while the full capability refresh runs.
|
|
520
|
+
*/
|
|
521
|
+
export function applyDisabledExtensionsToState(state: DashboardState, disabledIds: string[]): DashboardState {
|
|
522
|
+
const disabled = new Set(disabledIds);
|
|
523
|
+
const updateExtension = (ext: Extension): Extension => {
|
|
524
|
+
if (disabled.has(ext.id)) {
|
|
525
|
+
if (ext.state === "disabled" && ext.disabledReason === "item-disabled") return ext;
|
|
526
|
+
return { ...ext, state: "disabled", disabledReason: "item-disabled" };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (ext.state !== "disabled" || ext.disabledReason !== "item-disabled") return ext;
|
|
530
|
+
if (!isProviderEnabled(ext.source.provider)) {
|
|
531
|
+
return { ...ext, state: "disabled", disabledReason: "provider-disabled" };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (isShadowedExtension(ext)) {
|
|
535
|
+
const shadowed: Extension = { ...ext, state: "shadowed", disabledReason: "shadowed" };
|
|
536
|
+
return shadowed;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const enabled: Extension = { ...ext, state: "active" };
|
|
540
|
+
delete enabled.disabledReason;
|
|
541
|
+
return enabled;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
...state,
|
|
546
|
+
extensions: state.extensions.map(updateExtension),
|
|
547
|
+
tabFiltered: state.tabFiltered.map(updateExtension),
|
|
548
|
+
searchFiltered: state.searchFiltered.map(updateExtension),
|
|
549
|
+
selected: state.selected ? updateExtension(state.selected) : null,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
512
553
|
/**
|
|
513
554
|
* Create initial dashboard state.
|
|
514
555
|
*/
|
|
@@ -663,10 +663,23 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
663
663
|
}
|
|
664
664
|
|
|
665
665
|
finishPendingSubmission(input: SubmittedUserInput): void {
|
|
666
|
-
|
|
666
|
+
const wasPendingSubmission = this.#pendingSubmittedInput === input;
|
|
667
|
+
const pendingSubmissionDispose = this.#pendingSubmissionDispose;
|
|
668
|
+
if (wasPendingSubmission) {
|
|
667
669
|
this.#pendingSubmittedInput = undefined;
|
|
668
670
|
this.#pendingSubmissionDispose = undefined;
|
|
669
671
|
}
|
|
672
|
+
|
|
673
|
+
if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
|
|
674
|
+
this.optimisticUserMessageSignature = undefined;
|
|
675
|
+
pendingSubmissionDispose?.();
|
|
676
|
+
this.#pendingWorkingMessage = undefined;
|
|
677
|
+
if (this.loadingAnimation) {
|
|
678
|
+
this.loadingAnimation.stop();
|
|
679
|
+
this.loadingAnimation = undefined;
|
|
680
|
+
this.statusContainer.clear();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
670
683
|
}
|
|
671
684
|
|
|
672
685
|
#computeEditorMaxHeight(): number {
|
|
@@ -857,6 +870,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
857
870
|
/** Restore mode state from session entries on resume (e.g. plan mode). */
|
|
858
871
|
async #restoreModeFromSession(): Promise<void> {
|
|
859
872
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
873
|
+
if (!this.session.settings.get("plan.enabled")) {
|
|
874
|
+
// Clear stale plan/plan_paused mode so re-enabling the setting
|
|
875
|
+
// later doesn't unexpectedly restore an old plan session.
|
|
876
|
+
if (sessionContext.mode === "plan" || sessionContext.mode === "plan_paused") {
|
|
877
|
+
this.sessionManager.appendModeChange("none");
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
860
881
|
if (sessionContext.mode === "plan") {
|
|
861
882
|
const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
|
|
862
883
|
await this.#enterPlanMode({ planFilePath });
|
|
@@ -1103,6 +1124,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1103
1124
|
await this.#exitPlanMode({ paused: true });
|
|
1104
1125
|
return;
|
|
1105
1126
|
}
|
|
1127
|
+
if (!this.session.settings.get("plan.enabled")) {
|
|
1128
|
+
this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1106
1131
|
await this.#enterPlanMode();
|
|
1107
1132
|
if (initialPrompt && this.onInputCallback) {
|
|
1108
1133
|
this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
Search hidden
|
|
1
|
+
Search hidden tool metadata to discover and activate tools.
|
|
2
2
|
|
|
3
|
-
Use this tool
|
|
3
|
+
Use this tool when you need a capability that is not currently available in your active tool set. It searches all discoverable tools — including MCP tools and built-in tools that are hidden to save tokens.
|
|
4
4
|
|
|
5
5
|
{{#if hasDiscoverableMCPServers}}Discoverable MCP servers in this session: {{#list discoverableMCPServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
|
|
6
|
-
{{#if discoverableMCPToolCount}}Total discoverable
|
|
6
|
+
{{#if discoverableMCPToolCount}}Total discoverable tools available: {{discoverableMCPToolCount}}.{{/if}}
|
|
7
7
|
Input:
|
|
8
8
|
- `query` — required natural-language or keyword query
|
|
9
9
|
- `limit` — optional maximum number of tools to return and activate (default `8`)
|
|
10
10
|
|
|
11
11
|
Behavior:
|
|
12
|
-
- Searches hidden
|
|
13
|
-
- Matches against
|
|
14
|
-
- Activates the top matching
|
|
15
|
-
- Repeated searches add to the active
|
|
16
|
-
- Newly activated
|
|
12
|
+
- Searches hidden tool metadata using BM25-style relevance ranking
|
|
13
|
+
- Matches against tool name, label, server name, description/summary, and input schema keys
|
|
14
|
+
- Activates the top matching tools for the rest of the current session
|
|
15
|
+
- Repeated searches add to the active tool set; they do not remove earlier selections
|
|
16
|
+
- Newly activated tools become available before the next model call in the same overall turn
|
|
17
17
|
|
|
18
18
|
Notes:
|
|
19
19
|
- If you are unsure, start with `limit` between 5 and 10 to see a broader set of tools.
|
|
20
|
-
- `query` is matched against
|
|
20
|
+
- `query` is matched against tool metadata fields:
|
|
21
21
|
- `name`
|
|
22
22
|
- `label`
|
|
23
|
-
- `server_name`
|
|
24
|
-
- `mcp_tool_name`
|
|
25
|
-
- `description`
|
|
23
|
+
- `server_name` (MCP tools)
|
|
24
|
+
- `mcp_tool_name` (MCP tools)
|
|
25
|
+
- `description` / `summary`
|
|
26
26
|
- input schema property keys (`schema_keys`)
|
|
27
27
|
|
|
28
|
-
This is not repository search, file search, or code search. Use it only for
|
|
28
|
+
This is not repository search, file search, or code search. Use it only for tool discovery.
|
|
29
29
|
|
|
30
30
|
Returns JSON with:
|
|
31
31
|
- `query`
|
|
32
|
-
- `activated_tools` —
|
|
32
|
+
- `activated_tools` — tools activated by this search call
|
|
33
33
|
- `match_count` — number of ranked matches returned by the search
|
|
34
34
|
- `total_tools`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Manages a phased task list. Pass `ops`: a flat array of operations.
|
|
2
2
|
The next pending task is auto-promoted to `in_progress` after each completion.
|
|
3
|
+
Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, and `note`. `pending` is a task status, not an `op`; leave not-yet-started tasks implicit in `init`/`append` lists.
|
|
3
4
|
|
|
4
5
|
## Operations
|
|
5
6
|
|
package/src/sdk.ts
CHANGED
|
@@ -81,7 +81,6 @@ import {
|
|
|
81
81
|
collectDiscoverableMCPTools,
|
|
82
82
|
formatDiscoverableMCPToolServerSummary,
|
|
83
83
|
selectDiscoverableMCPToolNamesByServer,
|
|
84
|
-
summarizeDiscoverableMCPTools,
|
|
85
84
|
} from "./mcp/discoverable-tool-metadata";
|
|
86
85
|
import { getMemoryRoot } from "./memories";
|
|
87
86
|
import { resolveMemoryBackend } from "./memory-backend";
|
|
@@ -110,9 +109,15 @@ import {
|
|
|
110
109
|
} from "./system-prompt";
|
|
111
110
|
import { AgentOutputManager } from "./task/output-manager";
|
|
112
111
|
import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
|
|
112
|
+
import {
|
|
113
|
+
collectDiscoverableTools,
|
|
114
|
+
type DiscoverableTool,
|
|
115
|
+
summarizeDiscoverableTools,
|
|
116
|
+
} from "./tool-discovery/tool-index";
|
|
113
117
|
import {
|
|
114
118
|
BashTool,
|
|
115
119
|
BUILTIN_TOOLS,
|
|
120
|
+
computeEssentialBuiltinNames,
|
|
116
121
|
createTools,
|
|
117
122
|
discoverStartupLspServers,
|
|
118
123
|
EditTool,
|
|
@@ -995,6 +1000,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
995
1000
|
getDiscoverableMCPSearchIndex: () => session.getDiscoverableMCPSearchIndex(),
|
|
996
1001
|
getSelectedMCPToolNames: () => session.getSelectedMCPToolNames(),
|
|
997
1002
|
activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
|
|
1003
|
+
// Generic tool discovery (unified — covers built-in + MCP + extension)
|
|
1004
|
+
isToolDiscoveryEnabled: () => session.isToolDiscoveryEnabled(),
|
|
1005
|
+
getDiscoverableTools: filter => session.getDiscoverableTools(filter),
|
|
1006
|
+
getDiscoverableToolSearchIndex: () => session.getDiscoverableToolSearchIndex(),
|
|
1007
|
+
getSelectedDiscoveredToolNames: () => session.getSelectedDiscoveredToolNames(),
|
|
1008
|
+
activateDiscoveredTools: toolNames => session.activateDiscoveredTools(toolNames),
|
|
998
1009
|
getCheckpointState: () => session.getCheckpointState(),
|
|
999
1010
|
setCheckpointState: state => session.setCheckpointState(state ?? undefined),
|
|
1000
1011
|
getToolChoiceQueue: () => session.toolChoiceQueue,
|
|
@@ -1344,11 +1355,33 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1344
1355
|
): Promise<BuildSystemPromptResult> => {
|
|
1345
1356
|
toolContextStore.setToolNames(toolNames);
|
|
1346
1357
|
const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
|
|
1347
|
-
const
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1358
|
+
const activeToolNames = new Set(toolNames);
|
|
1359
|
+
const discoverableBuiltinTools: DiscoverableTool[] =
|
|
1360
|
+
effectiveDiscoveryMode === "all"
|
|
1361
|
+
? collectDiscoverableTools(
|
|
1362
|
+
Array.from(tools.values()).filter(
|
|
1363
|
+
tool => tool.loadMode === "discoverable" && !activeToolNames.has(tool.name),
|
|
1364
|
+
),
|
|
1365
|
+
{ source: "builtin" },
|
|
1366
|
+
)
|
|
1367
|
+
: [];
|
|
1368
|
+
const discoverableToolsForDesc: DiscoverableTool[] = [
|
|
1369
|
+
...discoverableBuiltinTools,
|
|
1370
|
+
...discoverableMCPTools.map(t => ({
|
|
1371
|
+
name: t.name,
|
|
1372
|
+
label: t.label,
|
|
1373
|
+
summary: t.description,
|
|
1374
|
+
source: "mcp" as const,
|
|
1375
|
+
serverName: t.serverName,
|
|
1376
|
+
mcpToolName: t.mcpToolName,
|
|
1377
|
+
schemaKeys: t.schemaKeys,
|
|
1378
|
+
})),
|
|
1379
|
+
];
|
|
1380
|
+
const discoverableToolSummary = summarizeDiscoverableTools(discoverableToolsForDesc);
|
|
1381
|
+
const hasDiscoverableTools =
|
|
1382
|
+
mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableToolsForDesc.length > 0;
|
|
1350
1383
|
const promptTools = buildSystemPromptToolMetadata(tools, {
|
|
1351
|
-
search_tool_bm25: { description: renderSearchToolBm25Description(
|
|
1384
|
+
search_tool_bm25: { description: renderSearchToolBm25Description(discoverableToolsForDesc) },
|
|
1352
1385
|
});
|
|
1353
1386
|
const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
|
|
1354
1387
|
agentDir,
|
|
@@ -1386,8 +1419,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1386
1419
|
appendSystemPrompt: appendPrompt,
|
|
1387
1420
|
repeatToolDescriptions,
|
|
1388
1421
|
intentField,
|
|
1389
|
-
mcpDiscoveryMode:
|
|
1390
|
-
mcpDiscoveryServerSummaries:
|
|
1422
|
+
mcpDiscoveryMode: hasDiscoverableTools,
|
|
1423
|
+
mcpDiscoveryServerSummaries: discoverableToolSummary.servers.map(formatDiscoverableMCPToolServerSummary),
|
|
1391
1424
|
eagerTasks,
|
|
1392
1425
|
secretsEnabled,
|
|
1393
1426
|
agentsMdSearch: agentsMdSearchPromise,
|
|
@@ -1411,7 +1444,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1411
1444
|
toolNamesFromRegistry;
|
|
1412
1445
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1413
1446
|
const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode");
|
|
1414
|
-
|
|
1447
|
+
// Effective discovery mode: tools.discoveryMode takes precedence; mcp.discoveryMode is back-compat alias.
|
|
1448
|
+
const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
|
|
1449
|
+
const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
|
|
1450
|
+
toolsDiscoveryModeSetting !== "off"
|
|
1451
|
+
? (toolsDiscoveryModeSetting as "off" | "mcp-only" | "all")
|
|
1452
|
+
: settings.get("mcp.discoveryMode")
|
|
1453
|
+
? "mcp-only"
|
|
1454
|
+
: "off";
|
|
1455
|
+
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1415
1456
|
const defaultInactiveToolNames = new Set(
|
|
1416
1457
|
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
|
|
1417
1458
|
);
|
|
@@ -1468,6 +1509,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1468
1509
|
}
|
|
1469
1510
|
}
|
|
1470
1511
|
|
|
1512
|
+
// When tools.discoveryMode === "all", hide non-essential built-in discoverable tools
|
|
1513
|
+
// from the initial set unless they were explicitly requested or restored from persistence.
|
|
1514
|
+
// The model finds them via search_tool_bm25 and activates them on demand.
|
|
1515
|
+
if (effectiveDiscoveryMode === "all") {
|
|
1516
|
+
const essentialBuiltinNames = new Set(computeEssentialBuiltinNames(settings));
|
|
1517
|
+
const explicitlyRequestedToolNames = new Set(options.toolNames?.map(name => name.toLowerCase()) ?? []);
|
|
1518
|
+
// Back-compat: persisted activations live under selectedMCPToolNames today (built-in
|
|
1519
|
+
// activation persistence is a follow-up). MCP names won't collide with built-in names.
|
|
1520
|
+
const restoredDiscoveredNames = new Set(existingSession.selectedMCPToolNames);
|
|
1521
|
+
initialToolNames = initialToolNames.filter(name => {
|
|
1522
|
+
const tool = toolRegistry.get(name);
|
|
1523
|
+
if (!tool?.loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
|
|
1524
|
+
if (tool.loadMode === "essential") return true;
|
|
1525
|
+
if (essentialBuiltinNames.has(name)) return true;
|
|
1526
|
+
if (explicitlyRequestedToolNames.has(name)) return true;
|
|
1527
|
+
if (restoredDiscoveredNames.has(name)) return true;
|
|
1528
|
+
return false;
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1471
1532
|
const { systemPrompt } = await logger.time(
|
|
1472
1533
|
"buildSystemPrompt",
|
|
1473
1534
|
rebuildSystemPrompt,
|
|
@@ -129,6 +129,12 @@ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { t
|
|
|
129
129
|
import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
130
130
|
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
131
131
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
132
|
+
import {
|
|
133
|
+
buildDiscoverableToolSearchIndex,
|
|
134
|
+
collectDiscoverableTools,
|
|
135
|
+
type DiscoverableTool,
|
|
136
|
+
type DiscoverableToolSearchIndex,
|
|
137
|
+
} from "../tool-discovery/tool-index";
|
|
132
138
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
133
139
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
134
140
|
import { outputMeta } from "../tools/output-meta";
|
|
@@ -536,6 +542,9 @@ export class AgentSession {
|
|
|
536
542
|
#discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
|
|
537
543
|
#discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
|
|
538
544
|
#selectedMCPToolNames = new Set<string>();
|
|
545
|
+
// Generic tool discovery (covers built-in + MCP + extension when tools.discoveryMode === "all")
|
|
546
|
+
#discoverableToolSearchIndex: DiscoverableToolSearchIndex | null = null;
|
|
547
|
+
#selectedDiscoveredToolNames = new Set<string>();
|
|
539
548
|
#rpcHostToolNames = new Set<string>();
|
|
540
549
|
#defaultSelectedMCPServerNames = new Set<string>();
|
|
541
550
|
#defaultSelectedMCPToolNames = new Set<string>();
|
|
@@ -2101,7 +2110,15 @@ export class AgentSession {
|
|
|
2101
2110
|
|
|
2102
2111
|
#setDiscoverableMCPTools(discoverableMCPTools: Map<string, DiscoverableMCPTool>): void {
|
|
2103
2112
|
this.#discoverableMCPTools = discoverableMCPTools;
|
|
2113
|
+
this.#invalidateDiscoveryCaches();
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/** Single point for invalidating cached discovery indices. Call after any change that can
|
|
2117
|
+
* affect which tools should be discoverable: registry mutations (refreshMCPTools,
|
|
2118
|
+
* refreshRpcHostTools) or active-tool mutations (#applyActiveToolsByName). */
|
|
2119
|
+
#invalidateDiscoveryCaches(): void {
|
|
2104
2120
|
this.#discoverableMCPSearchIndex = null;
|
|
2121
|
+
this.#discoverableToolSearchIndex = null;
|
|
2105
2122
|
}
|
|
2106
2123
|
|
|
2107
2124
|
#filterSelectableMCPToolNames(toolNames: Iterable<string>): string[] {
|
|
@@ -2204,10 +2221,21 @@ export class AgentSession {
|
|
|
2204
2221
|
return this.#mcpDiscoveryEnabled;
|
|
2205
2222
|
}
|
|
2206
2223
|
|
|
2224
|
+
/** @deprecated Use {@link getDiscoverableTools} with `{ source: "mcp" }` instead.
|
|
2225
|
+
* Preserves the legacy `description`-bearing MCP shape for back-compat callers. */
|
|
2207
2226
|
getDiscoverableMCPTools(): DiscoverableMCPTool[] {
|
|
2208
|
-
return Array.from(this.#discoverableMCPTools.values())
|
|
2227
|
+
return Array.from(this.#discoverableMCPTools.values()).map(t => ({
|
|
2228
|
+
name: t.name,
|
|
2229
|
+
label: t.label,
|
|
2230
|
+
description: t.description,
|
|
2231
|
+
serverName: t.serverName,
|
|
2232
|
+
mcpToolName: t.mcpToolName,
|
|
2233
|
+
schemaKeys: t.schemaKeys,
|
|
2234
|
+
}));
|
|
2209
2235
|
}
|
|
2210
2236
|
|
|
2237
|
+
/** @deprecated Use {@link getDiscoverableToolSearchIndex} instead.
|
|
2238
|
+
* Returns the legacy MCP search index whose documents expose `tool.description`. */
|
|
2211
2239
|
getDiscoverableMCPSearchIndex(): DiscoverableMCPSearchIndex {
|
|
2212
2240
|
if (!this.#discoverableMCPSearchIndex) {
|
|
2213
2241
|
this.#discoverableMCPSearchIndex = buildDiscoverableMCPSearchIndex(this.#discoverableMCPTools.values());
|
|
@@ -2243,6 +2271,113 @@ export class AgentSession {
|
|
|
2243
2271
|
return [...new Set(activated)];
|
|
2244
2272
|
}
|
|
2245
2273
|
|
|
2274
|
+
// ── Generic tool discovery (covers built-in + MCP + extension) ────────────
|
|
2275
|
+
|
|
2276
|
+
/** Resolve effective discovery mode: tools.discoveryMode wins; mcp.discoveryMode is back-compat alias. */
|
|
2277
|
+
#resolveEffectiveDiscoveryMode(): "off" | "mcp-only" | "all" {
|
|
2278
|
+
const toolsMode = this.settings.get("tools.discoveryMode");
|
|
2279
|
+
if (toolsMode !== "off") return toolsMode as "off" | "mcp-only" | "all";
|
|
2280
|
+
if (this.settings.get("mcp.discoveryMode")) return "mcp-only";
|
|
2281
|
+
return "off";
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
isToolDiscoveryEnabled(): boolean {
|
|
2285
|
+
return this.#resolveEffectiveDiscoveryMode() !== "off";
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
getDiscoverableTools(filter?: { source?: DiscoverableTool["source"] }): DiscoverableTool[] {
|
|
2289
|
+
// For "all" mode we combine built-in registry entries + MCP tools.
|
|
2290
|
+
// For "mcp-only" mode we only return MCP tools.
|
|
2291
|
+
const mode = this.#resolveEffectiveDiscoveryMode();
|
|
2292
|
+
const activeNames = new Set(this.getActiveToolNames());
|
|
2293
|
+
const mcpTools: DiscoverableTool[] = Array.from(this.#discoverableMCPTools.values())
|
|
2294
|
+
.filter(t => !activeNames.has(t.name))
|
|
2295
|
+
.map(t => ({
|
|
2296
|
+
name: t.name,
|
|
2297
|
+
label: t.label,
|
|
2298
|
+
summary: t.description,
|
|
2299
|
+
source: "mcp" as const,
|
|
2300
|
+
serverName: t.serverName,
|
|
2301
|
+
mcpToolName: t.mcpToolName,
|
|
2302
|
+
schemaKeys: t.schemaKeys,
|
|
2303
|
+
}));
|
|
2304
|
+
const builtinTools: DiscoverableTool[] = mode === "all" ? this.#collectDiscoverableBuiltinTools() : [];
|
|
2305
|
+
const allTools = [...builtinTools, ...mcpTools];
|
|
2306
|
+
return filter?.source ? allTools.filter(t => t.source === filter.source) : allTools;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
/** Collect built-in tools the model can discover via search_tool_bm25. Restricted to tool
|
|
2310
|
+
* definitions whose `loadMode === "discoverable"`. This keeps hidden/internal tools
|
|
2311
|
+
* (resolve, yield, exit_plan_mode, report_finding, report_tool_issue) out of the index
|
|
2312
|
+
* and avoids mislabeling extension/custom default-inactive tools as built-ins. */
|
|
2313
|
+
#collectDiscoverableBuiltinTools(): DiscoverableTool[] {
|
|
2314
|
+
const activeNames = new Set(this.getActiveToolNames());
|
|
2315
|
+
const result: DiscoverableTool[] = [];
|
|
2316
|
+
for (const tool of this.#toolRegistry.values()) {
|
|
2317
|
+
if (tool.loadMode !== "discoverable") continue;
|
|
2318
|
+
if (activeNames.has(tool.name)) continue;
|
|
2319
|
+
const collected = collectDiscoverableTools([tool], { source: "builtin" });
|
|
2320
|
+
result.push(...collected);
|
|
2321
|
+
}
|
|
2322
|
+
return result;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
getDiscoverableToolSearchIndex(): DiscoverableToolSearchIndex {
|
|
2326
|
+
if (!this.#discoverableToolSearchIndex) {
|
|
2327
|
+
this.#discoverableToolSearchIndex = buildDiscoverableToolSearchIndex(this.getDiscoverableTools());
|
|
2328
|
+
}
|
|
2329
|
+
return this.#discoverableToolSearchIndex;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/** Invalidate the generic search index cache (call after tool set changes).
|
|
2333
|
+
* Delegates to {@link #invalidateDiscoveryCaches} so all discovery-related caches stay in sync. */
|
|
2334
|
+
#invalidateDiscoverableToolSearchIndex(): void {
|
|
2335
|
+
this.#invalidateDiscoveryCaches();
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
getSelectedDiscoveredToolNames(): string[] {
|
|
2339
|
+
// Union of MCP-selected and generic non-MCP selected. Non-MCP selections are only
|
|
2340
|
+
// selected while they are still active; otherwise BM25 must be able to rediscover them.
|
|
2341
|
+
const activeNames = new Set(this.getActiveToolNames());
|
|
2342
|
+
const mcpSelected = this.getSelectedMCPToolNames();
|
|
2343
|
+
const nonMcpSelected = Array.from(this.#selectedDiscoveredToolNames).filter(
|
|
2344
|
+
name => activeNames.has(name) && this.#toolRegistry.has(name) && !isMCPToolName(name),
|
|
2345
|
+
);
|
|
2346
|
+
return [...new Set([...mcpSelected, ...nonMcpSelected])];
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
async activateDiscoveredTools(toolNames: string[]): Promise<string[]> {
|
|
2350
|
+
const mcpNames = toolNames.filter(isMCPToolName);
|
|
2351
|
+
const nonMcpNames = toolNames.filter(name => !isMCPToolName(name));
|
|
2352
|
+
const activated: string[] = [];
|
|
2353
|
+
|
|
2354
|
+
// Activate MCP tools via existing path
|
|
2355
|
+
if (mcpNames.length > 0) {
|
|
2356
|
+
const activatedMcp = await this.activateDiscoveredMCPTools(mcpNames);
|
|
2357
|
+
activated.push(...activatedMcp);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// Activate non-MCP tools (built-ins that are in the registry but not currently active)
|
|
2361
|
+
if (nonMcpNames.length > 0) {
|
|
2362
|
+
const currentActiveNames = new Set(this.getActiveToolNames());
|
|
2363
|
+
const newlyAdded: string[] = [];
|
|
2364
|
+
for (const name of nonMcpNames) {
|
|
2365
|
+
if (this.#toolRegistry.has(name) && !currentActiveNames.has(name)) {
|
|
2366
|
+
newlyAdded.push(name);
|
|
2367
|
+
this.#selectedDiscoveredToolNames.add(name);
|
|
2368
|
+
activated.push(name);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (newlyAdded.length > 0) {
|
|
2372
|
+
const nextActive = [...this.getActiveToolNames(), ...newlyAdded];
|
|
2373
|
+
await this.setActiveToolsByName(nextActive);
|
|
2374
|
+
this.#invalidateDiscoverableToolSearchIndex();
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
return [...new Set(activated)];
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2246
2381
|
async #applyActiveToolsByName(
|
|
2247
2382
|
toolNames: string[],
|
|
2248
2383
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
@@ -2273,8 +2408,18 @@ export class AgentSession {
|
|
|
2273
2408
|
),
|
|
2274
2409
|
);
|
|
2275
2410
|
}
|
|
2411
|
+
const activeNameSet = new Set(validToolNames);
|
|
2412
|
+
for (const name of Array.from(this.#selectedDiscoveredToolNames)) {
|
|
2413
|
+
if (!activeNameSet.has(name) || isMCPToolName(name) || !this.#toolRegistry.has(name)) {
|
|
2414
|
+
this.#selectedDiscoveredToolNames.delete(name);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2276
2417
|
this.agent.setTools(tools);
|
|
2277
2418
|
|
|
2419
|
+
// Active tool set changed → discoverable tool list (which excludes already-active tools)
|
|
2420
|
+
// is now stale. Invalidate before any prompt-template hook reads the discovery list.
|
|
2421
|
+
this.#invalidateDiscoveryCaches();
|
|
2422
|
+
|
|
2278
2423
|
// Rebuild base system prompt with new tool set, but only when the tool set
|
|
2279
2424
|
// actually changed. MCP servers can reconnect at arbitrary times and call
|
|
2280
2425
|
// `refreshMCPTools` -> `#applyActiveToolsByName` even though the resulting
|
|
@@ -2509,6 +2654,11 @@ export class AgentSession {
|
|
|
2509
2654
|
this.#rpcHostToolNames.add(finalTool.name);
|
|
2510
2655
|
}
|
|
2511
2656
|
|
|
2657
|
+
// Registry contents changed — invalidate discovery caches so the next BM25 lookup sees
|
|
2658
|
+
// the new RPC-host tool set. (#applyActiveToolsByName below also invalidates, but doing
|
|
2659
|
+
// it here too keeps the contract local to "registry mutated".)
|
|
2660
|
+
this.#invalidateDiscoveryCaches();
|
|
2661
|
+
|
|
2512
2662
|
const activeNonRpcToolNames = previousActiveToolNames.filter(name => !previousRpcHostToolNames.has(name));
|
|
2513
2663
|
const preservedRpcToolNames = previousActiveToolNames.filter(
|
|
2514
2664
|
name => previousRpcHostToolNames.has(name) && this.#rpcHostToolNames.has(name),
|
|
@@ -5983,6 +6133,32 @@ export class AgentSession {
|
|
|
5983
6133
|
setAutoRetryEnabled(enabled: boolean): void {
|
|
5984
6134
|
this.settings.set("retry.enabled", enabled);
|
|
5985
6135
|
}
|
|
6136
|
+
/**
|
|
6137
|
+
* Manually retry the last failed assistant turn.
|
|
6138
|
+
* Removes the error message from agent state and re-attempts with a fresh retry budget.
|
|
6139
|
+
* @returns true if retry was initiated, false if no failed turn to retry or agent is busy
|
|
6140
|
+
*/
|
|
6141
|
+
async retry(): Promise<boolean> {
|
|
6142
|
+
if (this.isStreaming || this.isCompacting || this.isRetrying) return false;
|
|
6143
|
+
|
|
6144
|
+
const messages = this.agent.state.messages;
|
|
6145
|
+
const lastMsg = messages[messages.length - 1];
|
|
6146
|
+
if (lastMsg?.role !== "assistant") return false;
|
|
6147
|
+
|
|
6148
|
+
const assistantMsg = lastMsg as AssistantMessage;
|
|
6149
|
+
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "aborted") return false;
|
|
6150
|
+
|
|
6151
|
+
// Remove the failed/aborted assistant message (same as auto-retry does before re-attempting)
|
|
6152
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
6153
|
+
|
|
6154
|
+
// Reset retry budget for a fresh attempt
|
|
6155
|
+
this.#retryAttempt = 0;
|
|
6156
|
+
|
|
6157
|
+
// Re-attempt the turn
|
|
6158
|
+
this.#scheduleAgentContinue({ delayMs: 1 });
|
|
6159
|
+
|
|
6160
|
+
return true;
|
|
6161
|
+
}
|
|
5986
6162
|
|
|
5987
6163
|
// =========================================================================
|
|
5988
6164
|
// Bash Execution
|
|
@@ -572,6 +572,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
572
572
|
await runtime.ctx.handleBtwCommand(question);
|
|
573
573
|
},
|
|
574
574
|
},
|
|
575
|
+
{
|
|
576
|
+
name: "retry",
|
|
577
|
+
description: "Retry the last failed agent turn",
|
|
578
|
+
handle: async (_command, runtime) => {
|
|
579
|
+
const didRetry = await runtime.ctx.session.retry();
|
|
580
|
+
if (!didRetry) {
|
|
581
|
+
runtime.ctx.showStatus("Nothing to retry");
|
|
582
|
+
}
|
|
583
|
+
runtime.ctx.editor.setText("");
|
|
584
|
+
},
|
|
585
|
+
},
|
|
575
586
|
{
|
|
576
587
|
name: "background",
|
|
577
588
|
aliases: ["bg"],
|
package/src/task/index.ts
CHANGED
|
@@ -195,7 +195,9 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
|
|
|
195
195
|
export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
196
196
|
readonly name = "task";
|
|
197
197
|
readonly label = "Task";
|
|
198
|
+
readonly summary = "Spawn a subagent to complete a parallel task";
|
|
198
199
|
readonly strict = true;
|
|
200
|
+
readonly loadMode = "discoverable";
|
|
199
201
|
readonly renderResult = renderResult;
|
|
200
202
|
readonly #discoveredAgents: AgentDefinition[];
|
|
201
203
|
readonly #blockedAgent: string | undefined;
|