@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.6
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 +31 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
- package/dist/cli.js +2989 -2978
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +23 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/model-controls.d.ts +3 -0
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +4 -2
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +27 -1
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +91 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +3 -3
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +17 -8
- package/src/session/agent-session-types.ts +3 -0
- package/src/session/agent-session.ts +21 -4
- package/src/session/model-controls.ts +43 -7
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-tools.ts +4 -2
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +21 -2
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/read.ts +2 -2
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
package/src/lsp/client.ts
CHANGED
|
@@ -918,7 +918,7 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
|
|
|
918
918
|
if (isEnoent(err)) return;
|
|
919
919
|
throw err;
|
|
920
920
|
}
|
|
921
|
-
const languageId = detectLanguageId(filePath);
|
|
921
|
+
const languageId = client.config.languageId ?? detectLanguageId(filePath);
|
|
922
922
|
throwIfAborted(signal);
|
|
923
923
|
|
|
924
924
|
await sendNotification(
|
|
@@ -992,7 +992,7 @@ export async function syncContent(
|
|
|
992
992
|
|
|
993
993
|
if (!info) {
|
|
994
994
|
// Open file with provided content instead of reading from disk
|
|
995
|
-
const languageId = detectLanguageId(filePath);
|
|
995
|
+
const languageId = client.config.languageId ?? detectLanguageId(filePath);
|
|
996
996
|
throwIfAborted(signal);
|
|
997
997
|
await sendNotification(
|
|
998
998
|
client,
|
package/src/lsp/config.ts
CHANGED
|
@@ -74,6 +74,8 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
|
|
|
74
74
|
const fileTypes =
|
|
75
75
|
normalizeStringArray(config.fileTypes) ?? normalizeExtensionToFileTypes(config.extensionToLanguage);
|
|
76
76
|
const rootMarkers = normalizeStringArray(config.rootMarkers) ?? (config.extensionToLanguage ? ["."] : null);
|
|
77
|
+
const languageId =
|
|
78
|
+
typeof config.languageId === "string" && config.languageId.length > 0 ? config.languageId : undefined;
|
|
77
79
|
|
|
78
80
|
if (!command || !fileTypes || !rootMarkers) {
|
|
79
81
|
logger.warn("Ignoring invalid LSP server config (missing required fields).", { name });
|
|
@@ -95,6 +97,7 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
|
|
|
95
97
|
args,
|
|
96
98
|
fileTypes,
|
|
97
99
|
rootMarkers,
|
|
100
|
+
languageId,
|
|
98
101
|
...(initOptions ? { initOptions } : {}),
|
|
99
102
|
};
|
|
100
103
|
}
|
|
@@ -423,6 +426,7 @@ function getConfigSources(cwd: string): ConfigSource[] {
|
|
|
423
426
|
* "command": "/path/to/server",
|
|
424
427
|
* "args": ["--stdio"],
|
|
425
428
|
* "fileTypes": [".xyz"],
|
|
429
|
+
* "languageId": "xyz",
|
|
426
430
|
* "rootMarkers": [".xyz-project"]
|
|
427
431
|
* }
|
|
428
432
|
* }
|
package/src/lsp/types.ts
CHANGED
|
@@ -340,6 +340,8 @@ export interface ServerConfig {
|
|
|
340
340
|
command: string;
|
|
341
341
|
args?: string[];
|
|
342
342
|
fileTypes: string[];
|
|
343
|
+
/** LSP language identifier sent in didOpen; inferred from the file path when omitted. */
|
|
344
|
+
languageId?: string;
|
|
343
345
|
rootMarkers: string[];
|
|
344
346
|
initOptions?: Record<string, unknown>;
|
|
345
347
|
settings?: Record<string, unknown>;
|
package/src/mcp/config.ts
CHANGED
|
@@ -97,14 +97,6 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
|
|
|
97
97
|
const filterExa = options?.filterExa ?? true;
|
|
98
98
|
const filterBrowser = options?.filterBrowser ?? false;
|
|
99
99
|
|
|
100
|
-
// Load MCP servers via capability system
|
|
101
|
-
const result = await loadCapability<MCPServer>(mcpCapability.id, { cwd });
|
|
102
|
-
|
|
103
|
-
// Filter out project-level configs if disabled
|
|
104
|
-
const servers = enableProjectConfig
|
|
105
|
-
? result.items
|
|
106
|
-
: result.items.filter(server => server._source.level !== "project");
|
|
107
|
-
|
|
108
100
|
// Load user-level disable/force-enable lists. The denylist always wins; the
|
|
109
101
|
// allowlist overrides a non-writable source config's `enabled: false`.
|
|
110
102
|
const userPath = getMCPConfigPath("user", cwd);
|
|
@@ -112,14 +104,34 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
|
|
|
112
104
|
readDisabledServers(userPath).then(list => new Set(list)),
|
|
113
105
|
readEnabledServers(userPath).then(list => new Set(list)),
|
|
114
106
|
]);
|
|
115
|
-
|
|
107
|
+
|
|
108
|
+
// Scope exclusions drop entries entirely BEFORE deduplication: with project
|
|
109
|
+
// config disabled, a project entry must not shadow anything.
|
|
110
|
+
const includeServer = (server: MCPServer & { _source: SourceMeta }): boolean =>
|
|
111
|
+
enableProjectConfig || server._source.level !== "project";
|
|
112
|
+
|
|
113
|
+
// Disabled servers are suppressed rather than dropped: they still own their
|
|
114
|
+
// name at key-level dedupe (a disabled project `foo` keeps a same-named,
|
|
115
|
+
// lower-priority user `foo` disabled), but never equivalence-shadow a
|
|
116
|
+
// differently-named enabled server — otherwise the disabled alias would be
|
|
117
|
+
// removed downstream and starve the surviving connection.
|
|
118
|
+
const suppressServer = (server: MCPServer & { _source: SourceMeta }): boolean => {
|
|
119
|
+
if (disabledServers.has(server.name)) return true;
|
|
120
|
+
if (server.enabled === false && !forcedEnabled.has(server.name)) return true;
|
|
121
|
+
return false;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await loadCapability<MCPServer>(mcpCapability.id, {
|
|
125
|
+
cwd,
|
|
126
|
+
filter: includeServer,
|
|
127
|
+
suppress: suppressServer,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Convert to legacy format and preserve source metadata.
|
|
116
131
|
let configs: Record<string, MCPServerConfig> = {};
|
|
117
132
|
let sources: Record<string, SourceMeta> = {};
|
|
118
|
-
for (const server of
|
|
119
|
-
|
|
120
|
-
if (disabledServers.has(server.name)) continue;
|
|
121
|
-
if (config.enabled === false && !forcedEnabled.has(server.name)) continue;
|
|
122
|
-
configs[server.name] = config;
|
|
133
|
+
for (const server of result.items) {
|
|
134
|
+
configs[server.name] = convertToLegacyConfig(server);
|
|
123
135
|
sources[server.name] = server._source;
|
|
124
136
|
}
|
|
125
137
|
|
package/src/mcp/manager.ts
CHANGED
|
@@ -1041,13 +1041,7 @@ export class MCPManager {
|
|
|
1041
1041
|
async #loadServerResourcesAndPrompts(name: string, connection: MCPServerConnection): Promise<void> {
|
|
1042
1042
|
if (serverSupportsResources(connection.capabilities)) {
|
|
1043
1043
|
try {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if (this.#notificationsEnabled && connection.capabilities.resources?.subscribe) {
|
|
1047
|
-
const uris = resources.map(r => r.uri);
|
|
1048
|
-
const notificationEpoch = this.#notificationsEpoch;
|
|
1049
|
-
this.#subscribeAndTrack(name, connection, uris, notificationEpoch);
|
|
1050
|
-
}
|
|
1044
|
+
await this.refreshServerResources(name);
|
|
1051
1045
|
} catch (error) {
|
|
1052
1046
|
logger.debug("Failed to load MCP resources", { path: `mcp:${name}`, error });
|
|
1053
1047
|
}
|
|
@@ -1107,8 +1101,20 @@ export class MCPManager {
|
|
|
1107
1101
|
connection.resources = undefined;
|
|
1108
1102
|
connection.resourceTemplates = undefined;
|
|
1109
1103
|
|
|
1110
|
-
// Reload
|
|
1111
|
-
|
|
1104
|
+
// Reload. Template listing failures must not discard a successful
|
|
1105
|
+
// resources/list — let both settle, then continue without templates.
|
|
1106
|
+
const [resourcesResult, templatesResult] = await Promise.allSettled([
|
|
1107
|
+
listResources(connection),
|
|
1108
|
+
listResourceTemplates(connection),
|
|
1109
|
+
]);
|
|
1110
|
+
if (templatesResult.status === "rejected") {
|
|
1111
|
+
logger.debug("Failed to list MCP resource templates", {
|
|
1112
|
+
path: `mcp:${name}`,
|
|
1113
|
+
error: templatesResult.reason,
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
if (resourcesResult.status === "rejected") throw resourcesResult.reason;
|
|
1117
|
+
const resources = resourcesResult.value;
|
|
1112
1118
|
if (this.#notificationsEnabled && connection.capabilities.resources?.subscribe) {
|
|
1113
1119
|
const newUris = new Set(resources.map(r => r.uri));
|
|
1114
1120
|
const oldUris = this.#subscribedResources.get(name);
|
|
@@ -1161,6 +1167,17 @@ export class MCPManager {
|
|
|
1161
1167
|
return promise;
|
|
1162
1168
|
}
|
|
1163
1169
|
|
|
1170
|
+
/**
|
|
1171
|
+
* Wait until a connected server's resource catalog has been loaded.
|
|
1172
|
+
* Coalesces with initial loading and notification-driven refreshes.
|
|
1173
|
+
*/
|
|
1174
|
+
async ensureServerResources(name: string): Promise<void> {
|
|
1175
|
+
const connection = this.#connections.get(name);
|
|
1176
|
+
if (!connection || !serverSupportsResources(connection.capabilities)) return;
|
|
1177
|
+
if (connection.resources !== undefined && connection.resourceTemplates !== undefined) return;
|
|
1178
|
+
await this.refreshServerResources(name);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1164
1181
|
/**
|
|
1165
1182
|
* Refresh prompts from a specific server.
|
|
1166
1183
|
*/
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import { normalizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
|
|
9
|
-
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
11
11
|
import type { SourceMeta } from "../capability/types";
|
|
12
12
|
import type {
|
|
@@ -357,6 +357,57 @@ export function createMCPToolName(serverName: string, toolName: string): string
|
|
|
357
357
|
return `mcp__${sanitizedServerName}_${normalizedToolName}`;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Keeps one MCP tool per minted name and logs collisions between distinct MCP
|
|
362
|
+
* origins. The winner is chosen by a stable origin key (server name + original
|
|
363
|
+
* tool name), NOT array order: MCPManager re-appends a reconnecting server's
|
|
364
|
+
* tools, so insertion order is mutable across reconnects and first-wins would
|
|
365
|
+
* silently flip ownership of the minted name. Non-MCP tools pass through
|
|
366
|
+
* unchanged.
|
|
367
|
+
*/
|
|
368
|
+
export function deduplicateMCPToolsByName<T extends { name: string; mcpServerName?: unknown; mcpToolName?: unknown }>(
|
|
369
|
+
tools: readonly T[],
|
|
370
|
+
): T[] {
|
|
371
|
+
const deduplicated: T[] = [];
|
|
372
|
+
const registered = new Map<string, { tool: T; originKey: string; index: number }>();
|
|
373
|
+
|
|
374
|
+
for (const tool of tools) {
|
|
375
|
+
if (typeof tool.mcpServerName !== "string" || typeof tool.mcpToolName !== "string") {
|
|
376
|
+
deduplicated.push(tool);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const originKey = `${tool.mcpServerName}\u0000${tool.mcpToolName}`;
|
|
381
|
+
const existing = registered.get(tool.name);
|
|
382
|
+
if (!existing) {
|
|
383
|
+
registered.set(tool.name, { tool, originKey, index: deduplicated.length });
|
|
384
|
+
deduplicated.push(tool);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (existing.originKey === originKey) continue;
|
|
389
|
+
|
|
390
|
+
// Deterministic winner regardless of encounter order across reconnects.
|
|
391
|
+
const keepExisting = existing.originKey < originKey;
|
|
392
|
+
const winner = keepExisting ? existing.tool : tool;
|
|
393
|
+
const loser = keepExisting ? tool : existing.tool;
|
|
394
|
+
if (!keepExisting) {
|
|
395
|
+
deduplicated[existing.index] = tool;
|
|
396
|
+
existing.tool = tool;
|
|
397
|
+
existing.originKey = originKey;
|
|
398
|
+
}
|
|
399
|
+
logger.warn("MCP tool name collision; keeping stable winner", {
|
|
400
|
+
name: tool.name,
|
|
401
|
+
keptServer: winner.mcpServerName,
|
|
402
|
+
keptTool: winner.mcpToolName,
|
|
403
|
+
ignoredServer: loser.mcpServerName,
|
|
404
|
+
ignoredTool: loser.mcpToolName,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return deduplicated;
|
|
409
|
+
}
|
|
410
|
+
|
|
360
411
|
/**
|
|
361
412
|
* Parse an MCP tool name back to server and tool components.
|
|
362
413
|
*
|
package/src/memories/index.ts
CHANGED
|
@@ -1361,12 +1361,31 @@ async function appendLearnedLine(filePath: string, line: string): Promise<void>
|
|
|
1361
1361
|
} catch (err) {
|
|
1362
1362
|
if (!isEnoent(err)) throw err;
|
|
1363
1363
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1364
|
+
// Treat the file as an ordered line list so headings, prose, and blank
|
|
1365
|
+
// lines keep their positions relative to the bullets they scope. Managed
|
|
1366
|
+
// operations touch only bullet lines: dedupe removes an existing copy of
|
|
1367
|
+
// the incoming lesson in place, the new lesson enters at the head of the
|
|
1368
|
+
// first bullet run (newest-first, matching the read path and cap docs),
|
|
1369
|
+
// and the cap drops the oldest (bottom-most) bullets. Hand-edited content
|
|
1370
|
+
// outside the list region survives every write byte-for-byte.
|
|
1371
|
+
const lines = existing.split("\n");
|
|
1372
|
+
// A well-formed file ends with "\n"; drop the terminal split artifact so
|
|
1373
|
+
// repeated saves stay idempotent instead of growing a blank line each time.
|
|
1374
|
+
if (lines.at(-1) === "") lines.pop();
|
|
1375
|
+
const isLesson = (l: string) => l.trimStart().startsWith("- ");
|
|
1376
|
+
const out = lines.filter(l => !(isLesson(l) && l.trim() === line));
|
|
1377
|
+
const firstBullet = out.findIndex(isLesson);
|
|
1378
|
+
if (firstBullet === -1) out.push(line);
|
|
1379
|
+
else out.splice(firstBullet, 0, line);
|
|
1380
|
+
let lessonCount = 0;
|
|
1381
|
+
for (const l of out) if (isLesson(l)) lessonCount++;
|
|
1382
|
+
for (let i = out.length - 1; i >= 0 && lessonCount > MAX_LEARNED_LESSONS; i--) {
|
|
1383
|
+
if (isLesson(out[i])) {
|
|
1384
|
+
out.splice(i, 1);
|
|
1385
|
+
lessonCount--;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
await Bun.write(filePath, `${out.join("\n")}\n`);
|
|
1370
1389
|
}
|
|
1371
1390
|
|
|
1372
1391
|
/**
|
|
@@ -433,11 +433,12 @@ const costSegment: StatusLineSegment = {
|
|
|
433
433
|
id: "cost",
|
|
434
434
|
render(ctx) {
|
|
435
435
|
const { cost, premiumRequests } = ctx.usageStats;
|
|
436
|
+
const advisorCost = ctx.session.getAdvisorCost?.() ?? 0;
|
|
436
437
|
const normalizedPremiumRequests = normalizePremiumRequests(premiumRequests);
|
|
437
438
|
const state = ctx.session.state;
|
|
438
439
|
const usingSubscription = state.model ? ctx.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
439
440
|
|
|
440
|
-
if (!cost && !usingSubscription && !normalizedPremiumRequests) {
|
|
441
|
+
if (!cost && !advisorCost && !usingSubscription && !normalizedPremiumRequests) {
|
|
441
442
|
return { content: "", visible: false };
|
|
442
443
|
}
|
|
443
444
|
|
|
@@ -445,6 +446,7 @@ const costSegment: StatusLineSegment = {
|
|
|
445
446
|
if (cost) billingParts.push(`$${cost.toFixed(2)}`);
|
|
446
447
|
if (normalizedPremiumRequests) billingParts.push(`★ ${formatNumber(normalizedPremiumRequests)}`);
|
|
447
448
|
if (usingSubscription) billingParts.push("(sub)");
|
|
449
|
+
if (advisorCost) billingParts.push(`${billingParts.length ? "+ " : ""}$${advisorCost.toFixed(2)} (adv)`);
|
|
448
450
|
|
|
449
451
|
return { content: theme.fg("statusLineCost", billingParts.join(" ")), visible: true };
|
|
450
452
|
},
|
|
@@ -346,7 +346,7 @@ export class CommandController {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
this.ctx.
|
|
349
|
+
this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
static readonly #advisorStatusGlyph: Record<string, string> = {
|
|
@@ -368,7 +368,7 @@ export class CommandController {
|
|
|
368
368
|
async handleAdvisorStatusCommand(): Promise<void> {
|
|
369
369
|
const stats = this.ctx.session.getAdvisorStats();
|
|
370
370
|
if (!stats.configured) {
|
|
371
|
-
this.ctx.
|
|
371
|
+
this.ctx.presentCommandOutput([new Spacer(1), new Text("Advisor is disabled.", 1, 0)]);
|
|
372
372
|
return;
|
|
373
373
|
}
|
|
374
374
|
// Fetch live quota data (cached 5 min by the auth-gateway) so we can show
|
|
@@ -434,7 +434,7 @@ export class CommandController {
|
|
|
434
434
|
info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
435
435
|
if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
|
|
436
436
|
}
|
|
437
|
-
this.ctx.
|
|
437
|
+
this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
|
|
438
438
|
return;
|
|
439
439
|
}
|
|
440
440
|
// Single active advisor — detailed view.
|
|
@@ -480,7 +480,7 @@ export class CommandController {
|
|
|
480
480
|
info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
|
|
481
481
|
}
|
|
482
482
|
if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
|
|
483
|
-
this.ctx.
|
|
483
|
+
this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
async handleJobsCommand(): Promise<void> {
|
|
@@ -497,7 +497,7 @@ export class CommandController {
|
|
|
497
497
|
|
|
498
498
|
if (snapshot.running.length === 0 && snapshot.recent.length === 0) {
|
|
499
499
|
info += `\n${theme.fg("dim", "No async jobs yet.")}\n`;
|
|
500
|
-
this.ctx.
|
|
500
|
+
this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
503
503
|
|
|
@@ -517,7 +517,7 @@ export class CommandController {
|
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
this.ctx.
|
|
520
|
+
this.ctx.presentCommandOutput([new Spacer(1), new Text(info.trimEnd(), 1, 0)]);
|
|
521
521
|
}
|
|
522
522
|
|
|
523
523
|
async handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
|
|
@@ -558,7 +558,7 @@ export class CommandController {
|
|
|
558
558
|
provider => (provider === currentProvider ? activeAccount : undefined),
|
|
559
559
|
usageModelSelectors,
|
|
560
560
|
);
|
|
561
|
-
this.ctx.
|
|
561
|
+
this.ctx.presentCommandOutput([new Spacer(1), new Text(output, 1, 0)]);
|
|
562
562
|
}
|
|
563
563
|
|
|
564
564
|
async handleChangelogCommand(showFull = false): Promise<void> {
|
|
@@ -578,7 +578,7 @@ export class CommandController {
|
|
|
578
578
|
block.addChild(new Spacer(1));
|
|
579
579
|
block.addChild(new Markdown(changelogMarkdown + hint, 1, 1, getMarkdownTheme()));
|
|
580
580
|
block.addChild(new DynamicBorder());
|
|
581
|
-
this.ctx.
|
|
581
|
+
this.ctx.presentCommandOutput(block);
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
handleHotkeysCommand(): void {
|
|
@@ -607,7 +607,7 @@ export class CommandController {
|
|
|
607
607
|
block.addChild(new Spacer(1));
|
|
608
608
|
block.addChild(new Text(output, 1, 0));
|
|
609
609
|
block.addChild(new DynamicBorder());
|
|
610
|
-
this.ctx.
|
|
610
|
+
this.ctx.presentCommandOutput(block);
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
async handleMemoryCommand(text: string): Promise<void> {
|
|
@@ -628,7 +628,7 @@ export class CommandController {
|
|
|
628
628
|
block.addChild(new Spacer(1));
|
|
629
629
|
block.addChild(new Markdown(payload, 1, 1, getMarkdownTheme()));
|
|
630
630
|
block.addChild(new DynamicBorder());
|
|
631
|
-
this.ctx.
|
|
631
|
+
this.ctx.presentCommandOutput(block);
|
|
632
632
|
return;
|
|
633
633
|
}
|
|
634
634
|
|
|
@@ -86,7 +86,7 @@ export class ExtensionUiController {
|
|
|
86
86
|
const uiContext: ExtensionUIContext = {
|
|
87
87
|
timeoutStartsOnPresentation: true,
|
|
88
88
|
select: (title, options, dialogOptions) => this.showCollabAwareSelector(title, options, dialogOptions),
|
|
89
|
-
confirm: (title, message,
|
|
89
|
+
confirm: (title, message, dialogOptions) => this.showHookConfirm(title, message, dialogOptions),
|
|
90
90
|
input: (title, placeholder, dialogOptions) => this.showHookInput(title, placeholder, dialogOptions),
|
|
91
91
|
askDialog: (questions, dialogOptions) => this.showAskDialog(questions, dialogOptions),
|
|
92
92
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
@@ -941,8 +941,8 @@ export class ExtensionUiController {
|
|
|
941
941
|
/**
|
|
942
942
|
* Show a confirmation dialog for hooks.
|
|
943
943
|
*/
|
|
944
|
-
async showHookConfirm(title: string, message: string): Promise<boolean> {
|
|
945
|
-
const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"]);
|
|
944
|
+
async showHookConfirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean> {
|
|
945
|
+
const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"], dialogOptions);
|
|
946
946
|
return result === "Yes";
|
|
947
947
|
}
|
|
948
948
|
|
|
@@ -603,6 +603,57 @@ export function requestRpcEditor(
|
|
|
603
603
|
} as RpcExtensionUIRequest);
|
|
604
604
|
return promise;
|
|
605
605
|
}
|
|
606
|
+
|
|
607
|
+
/** Sends an RPC extension dialog and cancels the remote presentation when its signal aborts. */
|
|
608
|
+
export function requestRpcDialog<T>(
|
|
609
|
+
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
610
|
+
output: RpcOutput,
|
|
611
|
+
opts: ExtensionUIDialogOptions | undefined,
|
|
612
|
+
defaultValue: T,
|
|
613
|
+
request: Record<string, unknown>,
|
|
614
|
+
parseResponse: (response: RpcExtensionUIResponse) => T,
|
|
615
|
+
): Promise<T> {
|
|
616
|
+
if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
|
|
617
|
+
|
|
618
|
+
const id = Snowflake.next() as string;
|
|
619
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
620
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
621
|
+
|
|
622
|
+
const cleanup = () => {
|
|
623
|
+
clearTimeout(timeoutId);
|
|
624
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
625
|
+
pendingRequests.delete(id);
|
|
626
|
+
};
|
|
627
|
+
const onAbort = () => {
|
|
628
|
+
output({
|
|
629
|
+
type: "extension_ui_request",
|
|
630
|
+
id: Snowflake.next() as string,
|
|
631
|
+
method: "cancel",
|
|
632
|
+
targetId: id,
|
|
633
|
+
} as RpcExtensionUIRequest);
|
|
634
|
+
cleanup();
|
|
635
|
+
resolve(defaultValue);
|
|
636
|
+
};
|
|
637
|
+
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
638
|
+
|
|
639
|
+
if (opts?.timeout !== undefined) {
|
|
640
|
+
timeoutId = setTimeout(() => {
|
|
641
|
+
opts.onTimeout?.();
|
|
642
|
+
cleanup();
|
|
643
|
+
resolve(defaultValue);
|
|
644
|
+
}, opts.timeout);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
pendingRequests.set(id, {
|
|
648
|
+
resolve: response => {
|
|
649
|
+
cleanup();
|
|
650
|
+
resolve(parseResponse(response));
|
|
651
|
+
},
|
|
652
|
+
reject,
|
|
653
|
+
});
|
|
654
|
+
output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
|
|
655
|
+
return promise;
|
|
656
|
+
}
|
|
606
657
|
/**
|
|
607
658
|
* Run in RPC mode.
|
|
608
659
|
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
|
@@ -685,56 +736,14 @@ export async function runRpcMode(
|
|
|
685
736
|
private output: (obj: RpcResponse | RpcExtensionUIRequest | object) => void,
|
|
686
737
|
) {}
|
|
687
738
|
|
|
688
|
-
/** Helper for dialog methods with signal/timeout support */
|
|
689
|
-
#createDialogPromise<T>(
|
|
690
|
-
opts: ExtensionUIDialogOptions | undefined,
|
|
691
|
-
defaultValue: T,
|
|
692
|
-
request: Record<string, unknown>,
|
|
693
|
-
parseResponse: (response: RpcExtensionUIResponse) => T,
|
|
694
|
-
): Promise<T> {
|
|
695
|
-
if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
|
|
696
|
-
|
|
697
|
-
const id = Snowflake.next() as string;
|
|
698
|
-
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
699
|
-
let timeoutId: NodeJS.Timeout | undefined;
|
|
700
|
-
|
|
701
|
-
const cleanup = () => {
|
|
702
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
703
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
704
|
-
this.pendingRequests.delete(id);
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
const onAbort = () => {
|
|
708
|
-
cleanup();
|
|
709
|
-
resolve(defaultValue);
|
|
710
|
-
};
|
|
711
|
-
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
712
|
-
|
|
713
|
-
if (opts?.timeout !== undefined) {
|
|
714
|
-
timeoutId = setTimeout(() => {
|
|
715
|
-
opts.onTimeout?.();
|
|
716
|
-
cleanup();
|
|
717
|
-
resolve(defaultValue);
|
|
718
|
-
}, opts.timeout);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
this.pendingRequests.set(id, {
|
|
722
|
-
resolve: (response: RpcExtensionUIResponse) => {
|
|
723
|
-
cleanup();
|
|
724
|
-
resolve(parseResponse(response));
|
|
725
|
-
},
|
|
726
|
-
reject,
|
|
727
|
-
});
|
|
728
|
-
this.output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
|
|
729
|
-
return promise;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
739
|
select(
|
|
733
740
|
title: string,
|
|
734
741
|
options: ExtensionUISelectItem[],
|
|
735
742
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
736
743
|
): Promise<string | undefined> {
|
|
737
|
-
return
|
|
744
|
+
return requestRpcDialog(
|
|
745
|
+
this.pendingRequests,
|
|
746
|
+
this.output,
|
|
738
747
|
dialogOptions,
|
|
739
748
|
undefined,
|
|
740
749
|
{
|
|
@@ -748,7 +757,9 @@ export async function runRpcMode(
|
|
|
748
757
|
}
|
|
749
758
|
|
|
750
759
|
confirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean> {
|
|
751
|
-
return
|
|
760
|
+
return requestRpcDialog(
|
|
761
|
+
this.pendingRequests,
|
|
762
|
+
this.output,
|
|
752
763
|
dialogOptions,
|
|
753
764
|
false,
|
|
754
765
|
{ method: "confirm", title, message, timeout: dialogOptions?.timeout },
|
|
@@ -768,7 +779,9 @@ export async function runRpcMode(
|
|
|
768
779
|
placeholder?: string,
|
|
769
780
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
770
781
|
): Promise<string | undefined> {
|
|
771
|
-
return
|
|
782
|
+
return requestRpcDialog(
|
|
783
|
+
this.pendingRequests,
|
|
784
|
+
this.output,
|
|
772
785
|
dialogOptions,
|
|
773
786
|
undefined,
|
|
774
787
|
{ method: "input", title, placeholder, timeout: dialogOptions?.timeout },
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
<
|
|
1
|
+
<system-notice>
|
|
2
2
|
The user sent this message as an interjection while you were working. It takes
|
|
3
3
|
priority and supersedes earlier instructions wherever they conflict — re-read it
|
|
4
4
|
and make sure your current work reflects their intent.
|
|
5
|
-
|
|
6
|
-
<message>
|
|
5
|
+
</system-notice>
|
|
7
6
|
{{message}}
|
|
8
|
-
</message>
|
|
9
|
-
</user_interjection>
|
|
@@ -25,7 +25,8 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
|
|
|
25
25
|
- `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
|
|
26
26
|
- `agent`: The agent type running this item (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
|
|
27
27
|
- `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
28
|
-
- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
|
|
28
|
+
{{#if effortEnabled}} - `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
|
|
29
|
+
{{/if}}
|
|
29
30
|
- `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
|
|
30
31
|
- `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
|
|
31
32
|
{{#if isolationEnabled}}
|
|
@@ -39,7 +40,8 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
|
|
|
39
40
|
- `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
|
|
40
41
|
- `agent`: The agent type to spawn (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
|
|
41
42
|
- `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
42
|
-
- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
|
|
43
|
+
{{#if effortEnabled}}- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
|
|
44
|
+
{{/if}}
|
|
43
45
|
- `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
|
|
44
46
|
- `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
|
|
45
47
|
{{#if isolationEnabled}}
|
package/src/sdk.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type {
|
|
13
13
|
Context,
|
|
14
14
|
CredentialDisabledEvent,
|
|
15
|
+
Effort,
|
|
15
16
|
Message,
|
|
16
17
|
Model,
|
|
17
18
|
ModelUsageHealth,
|
|
@@ -98,6 +99,7 @@ import type { HindsightSessionState } from "./hindsight/state";
|
|
|
98
99
|
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
|
|
99
100
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
100
101
|
import {
|
|
102
|
+
deduplicateMCPToolsByName,
|
|
101
103
|
discoverAndLoadMCPTools,
|
|
102
104
|
type MCPLoadResult,
|
|
103
105
|
MCPManager,
|
|
@@ -357,6 +359,8 @@ export interface CreateAgentSessionOptions {
|
|
|
357
359
|
modelPatternDefaultFallbackChain?: string[];
|
|
358
360
|
/** Thinking selector. Default: from settings, else unset */
|
|
359
361
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
362
|
+
/** Hard ceiling on the session's thinking effort (e.g. a task spawn's `task.maxEffort`-capped hint); retry-fallback recovery re-clamps to it. */
|
|
363
|
+
thinkingLevelCeiling?: Effort;
|
|
360
364
|
/** Models available for cycling (Ctrl+P in interactive mode) */
|
|
361
365
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
362
366
|
/** Prewalk from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
|
|
@@ -924,13 +928,14 @@ export function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
|
924
928
|
}
|
|
925
929
|
|
|
926
930
|
function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
931
|
+
const uniqueTools = deduplicateMCPToolsByName(tools);
|
|
927
932
|
return api => {
|
|
928
|
-
for (const tool of
|
|
933
|
+
for (const tool of uniqueTools) {
|
|
929
934
|
api.registerTool(customToolToDefinition(tool));
|
|
930
935
|
}
|
|
931
936
|
|
|
932
937
|
const runOnSession = async (event: CustomToolSessionEvent, ctx: ExtensionContext) => {
|
|
933
|
-
for (const tool of
|
|
938
|
+
for (const tool of uniqueTools) {
|
|
934
939
|
if (!tool.onSession) continue;
|
|
935
940
|
try {
|
|
936
941
|
await tool.onSession(event, createCustomToolContext(ctx));
|
|
@@ -1985,10 +1990,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1985
1990
|
// Process provider registrations queued during extension loading.
|
|
1986
1991
|
// This must happen before the runner is created so that models registered by
|
|
1987
1992
|
// extensions are available for model selection on session resume / fallback.
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1993
|
+
if (!restrictToolNames) {
|
|
1994
|
+
const activeExtensionSources = extensionsResult.extensions.map(extension => extension.path);
|
|
1995
|
+
modelRegistry.syncExtensionSources(activeExtensionSources);
|
|
1996
|
+
for (const sourceId of new Set(activeExtensionSources)) {
|
|
1997
|
+
modelRegistry.clearSourceRegistrations(sourceId);
|
|
1998
|
+
}
|
|
1992
1999
|
}
|
|
1993
2000
|
if (extensionsResult.runtime.pendingProviderRegistrations.length > 0) {
|
|
1994
2001
|
for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
|
|
@@ -2537,8 +2544,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2537
2544
|
// Built-in tools get it in `createTools`; extension, SDK-custom, image-gen,
|
|
2538
2545
|
// TTS, and startup (non-deferred) MCP tools all funnel through here, so apply
|
|
2539
2546
|
// it once at this adapter boundary (idempotent — a no-op if already wrapped).
|
|
2540
|
-
const wrappedExtensionTools: Tool[] =
|
|
2541
|
-
wrapToolWithMetaNotice,
|
|
2547
|
+
const wrappedExtensionTools: Tool[] = deduplicateMCPToolsByName(
|
|
2548
|
+
wrapRegisteredTools(allCustomTools, extensionRunner).map(wrapToolWithMetaNotice),
|
|
2542
2549
|
);
|
|
2543
2550
|
|
|
2544
2551
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
@@ -3056,6 +3063,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3056
3063
|
dialect: resolveDialect(settings.get("tools.format"), model),
|
|
3057
3064
|
abortOnFabricatedToolResult: settings.get("tools.abortOnFabricatedResult"),
|
|
3058
3065
|
getToolChoice: () => session?.nextToolChoiceDirective(),
|
|
3066
|
+
onToolChoiceUnavailable: () => session?.toolChoiceQueue.reject("unavailable"),
|
|
3059
3067
|
telemetry: options.telemetry,
|
|
3060
3068
|
appendOnlyContext: model
|
|
3061
3069
|
? shouldEnableAppendOnlyContext(settings.get("provider.appendOnlyContext"), model)
|
|
@@ -3131,6 +3139,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3131
3139
|
agent,
|
|
3132
3140
|
pruneToolDescriptions: inlineToolDescriptors,
|
|
3133
3141
|
thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
|
|
3142
|
+
thinkingLevelCeiling: options.thinkingLevelCeiling,
|
|
3134
3143
|
initialRetryFallback,
|
|
3135
3144
|
prewalk: options.prewalk,
|
|
3136
3145
|
planYolo: options.planYolo,
|