@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +24 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +5 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +321 -184
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
|
@@ -239,9 +239,12 @@ function overflowFrame(frame: object): object {
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
function encodeRpcFrameFromJson(
|
|
243
|
+
frame: object,
|
|
244
|
+
json: string,
|
|
245
|
+
streamedMessageCount: number,
|
|
246
|
+
streamedMessages?: readonly unknown[],
|
|
247
|
+
): string {
|
|
245
248
|
if (serializedFrameBytes(json) <= MAX_RPC_FRAME_BYTES) return `${json}\n`;
|
|
246
249
|
if (isRecord(frame) && frame.type === "response") {
|
|
247
250
|
return `${JSON.stringify(overflowFrame(frame))}\n`;
|
|
@@ -259,6 +262,11 @@ export function encodeRpcFrame(frame: object, streamedMessageCount = 0, streamed
|
|
|
259
262
|
return `${JSON.stringify(overflowFrame(compacted))}\n`;
|
|
260
263
|
}
|
|
261
264
|
|
|
265
|
+
/** Serialize a complete JSONL frame while enforcing the transport byte ceiling. */
|
|
266
|
+
export function encodeRpcFrame(frame: object, streamedMessageCount = 0, streamedMessages?: readonly unknown[]): string {
|
|
267
|
+
return encodeRpcFrameFromJson(frame, JSON.stringify(frame), streamedMessageCount, streamedMessages);
|
|
268
|
+
}
|
|
269
|
+
|
|
262
270
|
/** Stateful encoder that tracks which messages a client has already received. */
|
|
263
271
|
export class RpcFrameEncoder {
|
|
264
272
|
#streamedMessages: unknown[] = [];
|
|
@@ -292,14 +300,14 @@ export class RpcFrameEncoder {
|
|
|
292
300
|
frames = [singleFrame];
|
|
293
301
|
}
|
|
294
302
|
} else {
|
|
295
|
-
singleFrame =
|
|
303
|
+
singleFrame = encodeRpcFrameFromJson(frame, json, this.#streamedMessages.length, this.#streamedMessages);
|
|
296
304
|
frames = [singleFrame];
|
|
297
305
|
}
|
|
298
306
|
if (!isRecord(frame)) return frames;
|
|
299
307
|
if (frame.type === "message_end") {
|
|
300
308
|
const snapshot =
|
|
301
309
|
this.#protocolVersion === 2 && Object.hasOwn(frame, "message")
|
|
302
|
-
? { message: jsonSnapshot(frame.message) }
|
|
310
|
+
? (encodedMessageSnapshot(json) ?? { message: jsonSnapshot(frame.message) })
|
|
303
311
|
: singleFrame !== undefined
|
|
304
312
|
? encodedMessageSnapshot(singleFrame)
|
|
305
313
|
: undefined;
|
|
@@ -148,18 +148,17 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
148
148
|
}
|
|
149
149
|
const mermaid = markdownMermaidRendering
|
|
150
150
|
? (() => {
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// mapping; `text`/`muted`/`border`/`borderMuted`/`accent` exist in every theme.
|
|
151
|
+
// Diagram geometry is content, so keep every structural stroke on the
|
|
152
|
+
// theme's readable muted foreground instead of subtle UI chrome borders.
|
|
154
153
|
const mermaidColorMode =
|
|
155
154
|
theme.getColorMode() === "truecolor" ? ("truecolor" as const) : ("ansi256" as const);
|
|
156
155
|
const mermaidTheme = {
|
|
157
156
|
fg: theme.getColorHex("text"),
|
|
158
|
-
border: theme.getColorHex("
|
|
157
|
+
border: theme.getColorHex("muted"),
|
|
159
158
|
line: theme.getColorHex("muted"),
|
|
160
159
|
arrow: theme.getColorHex("accent"),
|
|
161
160
|
corner: theme.getColorHex("muted"),
|
|
162
|
-
junction: theme.getColorHex("
|
|
161
|
+
junction: theme.getColorHex("muted"),
|
|
163
162
|
};
|
|
164
163
|
return { mermaidColorMode, mermaidTheme };
|
|
165
164
|
})()
|
package/src/modes/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { KeybindingsManager } from "../config/keybindings";
|
|
|
8
8
|
import type { Settings } from "../config/settings";
|
|
9
9
|
import type {
|
|
10
10
|
AutocompleteProviderFactory,
|
|
11
|
+
ExtensionCustomOptions,
|
|
11
12
|
ExtensionUIContext,
|
|
12
13
|
ExtensionUIDialogOptions,
|
|
13
14
|
ExtensionUISelectItem,
|
|
@@ -113,6 +114,7 @@ export interface InteractiveModeContext {
|
|
|
113
114
|
omfgContainer: Container;
|
|
114
115
|
errorBannerContainer: Container;
|
|
115
116
|
modelCycleContainer: Container;
|
|
117
|
+
deferredCommandContainer: Container;
|
|
116
118
|
editor: CustomEditor;
|
|
117
119
|
editorContainer: Container;
|
|
118
120
|
hookWidgetContainerAbove: Container;
|
|
@@ -436,12 +438,16 @@ export interface InteractiveModeContext {
|
|
|
436
438
|
toggleToolOutputExpansion(): void;
|
|
437
439
|
setToolsExpanded(expanded: boolean): void;
|
|
438
440
|
toggleThinkingBlockVisibility(): void;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
handlePlanModeCommand(
|
|
442
|
+
initialPrompt?: string,
|
|
443
|
+
input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
|
|
444
|
+
): Promise<boolean>;
|
|
445
|
+
handleVibeModeCommand(
|
|
446
|
+
initialPrompt?: string,
|
|
447
|
+
input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
|
|
448
|
+
): Promise<boolean>;
|
|
449
|
+
handleGoalModeCommand(rest?: string, input?: Pick<SubmittedUserInput, "images" | "imageLinks">): Promise<boolean>;
|
|
450
|
+
handleGuidedGoalCommand(rest?: string, input?: Pick<SubmittedUserInput, "images" | "imageLinks">): Promise<boolean>;
|
|
445
451
|
handleLoopCommand(args?: string): Promise<string | undefined>;
|
|
446
452
|
setLoopPrompt(prompt: string): void;
|
|
447
453
|
disableLoopMode(): void;
|
|
@@ -487,7 +493,7 @@ export interface InteractiveModeContext {
|
|
|
487
493
|
keybindings: KeybindingsManager,
|
|
488
494
|
done: (result: T) => void,
|
|
489
495
|
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
|
|
490
|
-
options?:
|
|
496
|
+
options?: ExtensionCustomOptions,
|
|
491
497
|
): Promise<T>;
|
|
492
498
|
showExtensionError(extensionPath: string, error: string): void;
|
|
493
499
|
showToolError(toolName: string, error: string): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AppKeybinding, type KeybindingsManager, keyHintPlatform, modifierLabel } from "../../config/keybindings";
|
|
2
2
|
|
|
3
3
|
export interface HotkeysMarkdownBindings {
|
|
4
4
|
keybindings: Pick<KeybindingsManager, "getDisplayString">;
|
|
@@ -9,21 +9,25 @@ function appKey(bindings: HotkeysMarkdownBindings, action: AppKeybinding): strin
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
|
|
12
|
+
const platform = keyHintPlatform();
|
|
13
|
+
const isMac = platform === "darwin";
|
|
14
|
+
const alt = modifierLabel("alt", platform);
|
|
15
|
+
const cmd = modifierLabel("super", platform);
|
|
12
16
|
return [
|
|
13
17
|
"**Navigation**",
|
|
14
18
|
"| Key | Action |",
|
|
15
19
|
"|-----|--------|",
|
|
16
20
|
"| `Arrow keys` | Move cursor / browse history (Up when empty) |",
|
|
17
|
-
|
|
18
|
-
"| `Ctrl+A` / `Home`
|
|
19
|
-
"| `Ctrl+E` / `End`
|
|
21
|
+
`| \`${alt}+Left/Right\` | Move by word |`,
|
|
22
|
+
isMac ? `| \`Ctrl+A\` / \`Home\` / \`${cmd}+Left\` | Start of line |` : "| `Ctrl+A` / `Home` | Start of line |",
|
|
23
|
+
isMac ? `| \`Ctrl+E\` / \`End\` / \`${cmd}+Right\` | End of line |` : "| `Ctrl+E` / `End` | End of line |",
|
|
20
24
|
"",
|
|
21
25
|
"**Editing**",
|
|
22
26
|
"| Key | Action |",
|
|
23
27
|
"|-----|--------|",
|
|
24
28
|
"| `Enter` | Send message |",
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
`| \`Shift+Enter\` / \`${alt}+Enter\` | New line |`,
|
|
30
|
+
`| \`Ctrl+W\` / \`${alt}+Backspace\` | Delete word backwards |`,
|
|
27
31
|
"| `Ctrl+U` | Delete to start of line |",
|
|
28
32
|
"| `Ctrl+K` | Delete to end of line |",
|
|
29
33
|
`| \`${appKey(bindings, "app.clipboard.copyLine")}\` | Copy current line |`,
|
|
@@ -120,7 +120,7 @@ You MUST use the specialized tool over its shell equivalent:
|
|
|
120
120
|
{{#has tools "edit"}}- Surgical edits → `{{toolRefs.edit}}`.{{/has}}
|
|
121
121
|
{{#has tools "write"}}- Create or overwrite → `{{toolRefs.write}}`.{{/has}}
|
|
122
122
|
{{#has tools "lsp"}}- When a language server is available, MUST use `{{toolRefs.lsp}}` for definition, type_definition, implementation, references, and hover; for refactors, imports, and fixes, list code actions then apply one. NEVER use search or manual edits for code intelligence.{{/has}}
|
|
123
|
-
{{#has tools "grep"}}- Regex search or locating targets → `{{toolRefs.grep}}`, not `grep`, `rg`, or `awk`.{{/has}}
|
|
123
|
+
{{#has tools "grep"}}- Regex search or locating targets → `{{toolRefs.grep}}`, not shell `grep`, `rg`, or `awk`.{{/has}}
|
|
124
124
|
{{#has tools "glob"}}- Mapping structure or globbing → `{{toolRefs.glob}}`, not `ls **/*.ext` or `fd`.{{/has}}
|
|
125
125
|
{{#has tools "bash"}}- `{{toolRefs.bash}}`: real binaries and short fact pipelines only. Commands shadowing the specialized tools above are blocked.{{/has}}
|
|
126
126
|
{{#has tools "bash"}}- Litmus: one external-CLI call or short pipeline returning a count, frequency, set difference, or checksum → bash. Merely moves, pages, or trims bytes a tool can fetch → use the tool.{{/has}}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
3
4
|
import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../advisor/transcript-recorder";
|
|
4
5
|
import { resolveExplicitModelRole } from "../config/model-resolver";
|
|
6
|
+
import { assistantTurnProducedOutput } from "../session/messages";
|
|
5
7
|
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "../session/session-entries";
|
|
6
8
|
import { visitEntriesFromFileStream } from "../session/session-loader";
|
|
7
9
|
import { loadBundledAgents } from "../task/agents";
|
|
@@ -90,6 +92,12 @@ interface AssistantMetrics {
|
|
|
90
92
|
cost: number;
|
|
91
93
|
contextTokens?: number;
|
|
92
94
|
resolvedModel?: string;
|
|
95
|
+
/**
|
|
96
|
+
* True when this turn produced output, making its model the run's. Uses the
|
|
97
|
+
* same predicate as the live session, so replaying a transcript reaches the
|
|
98
|
+
* same verdict the session reached while running it.
|
|
99
|
+
*/
|
|
100
|
+
served: boolean;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
function assistantMetrics(message: Record<string, unknown>): AssistantMetrics {
|
|
@@ -104,6 +112,10 @@ function assistantMetrics(message: Record<string, unknown>): AssistantMetrics {
|
|
|
104
112
|
cost: finiteNumber(cost?.total),
|
|
105
113
|
contextTokens: finiteNumber(usage.totalTokens) || undefined,
|
|
106
114
|
resolvedModel: provider && model ? `${provider}/${model}` : undefined,
|
|
115
|
+
served: assistantTurnProducedOutput({
|
|
116
|
+
stopReason: message.stopReason,
|
|
117
|
+
content,
|
|
118
|
+
} as Pick<AssistantMessage, "stopReason" | "content">),
|
|
107
119
|
};
|
|
108
120
|
}
|
|
109
121
|
|
|
@@ -159,27 +171,42 @@ async function readPersistedAgentHistory(
|
|
|
159
171
|
),
|
|
160
172
|
durationKind: "span",
|
|
161
173
|
};
|
|
174
|
+
// Attribution walks leaf → root and stops at the newest turn that actually
|
|
175
|
+
// produced output: that model did this run's work. A `model_change` newer
|
|
176
|
+
// than it was never served (a fallback the session died on), so crediting the
|
|
177
|
+
// run to it would report work the previous model did.
|
|
162
178
|
let resolvedModel: string | undefined;
|
|
163
179
|
let resolvedModelIsFallback: boolean | undefined;
|
|
164
180
|
let modelRole: string | undefined;
|
|
165
181
|
let contextTokens: number | undefined;
|
|
166
|
-
let
|
|
182
|
+
let servedModel: string | undefined;
|
|
183
|
+
let latestModelChange: { model: string; resolvedModelIsFallback: boolean } | undefined;
|
|
167
184
|
const visited = new Set<string>();
|
|
168
185
|
for (let id = leafId; id && !visited.has(id); id = parents.get(id)) {
|
|
169
186
|
visited.add(id);
|
|
170
187
|
const modelChange = modelChangeById.get(id);
|
|
171
|
-
if (modelChange
|
|
172
|
-
|
|
173
|
-
resolvedModel = modelChange.model;
|
|
174
|
-
resolvedModelIsFallback = modelChange.resolvedModelIsFallback;
|
|
188
|
+
if (modelChange) {
|
|
189
|
+
latestModelChange ??= modelChange;
|
|
175
190
|
if (modelChange.role && modelChange.role !== EPHEMERAL_MODEL_CHANGE_ROLE) {
|
|
176
|
-
modelRole
|
|
191
|
+
modelRole ??= modelChange.role;
|
|
192
|
+
}
|
|
193
|
+
// The transition that installed the serving model: it carries the
|
|
194
|
+
// fallback flag the raw message lacks. Every writer records the selector
|
|
195
|
+
// through `formatModelStringWithRouting`, which appends an `@upstream`
|
|
196
|
+
// gateway route the message's bare `provider/model` never has.
|
|
197
|
+
if (
|
|
198
|
+
servedModel !== undefined &&
|
|
199
|
+
resolvedModel === undefined &&
|
|
200
|
+
(modelChange.model === servedModel || modelChange.model.startsWith(`${servedModel}@`))
|
|
201
|
+
) {
|
|
202
|
+
resolvedModel = modelChange.model;
|
|
203
|
+
resolvedModelIsFallback = modelChange.resolvedModelIsFallback;
|
|
177
204
|
}
|
|
178
205
|
}
|
|
179
206
|
const assistant = assistantById.get(id);
|
|
180
207
|
if (!assistant) continue;
|
|
181
|
-
if (
|
|
182
|
-
|
|
208
|
+
if (servedModel === undefined && assistant.served && assistant.resolvedModel) {
|
|
209
|
+
servedModel = assistant.resolvedModel;
|
|
183
210
|
}
|
|
184
211
|
metrics.requests++;
|
|
185
212
|
metrics.tokens += assistant.tokens;
|
|
@@ -187,6 +214,14 @@ async function readPersistedAgentHistory(
|
|
|
187
214
|
metrics.cost += assistant.cost;
|
|
188
215
|
contextTokens ??= assistant.contextTokens;
|
|
189
216
|
}
|
|
217
|
+
// No transition described the serving model (pre-`model_change` transcript, or
|
|
218
|
+
// the spawn record was pruned) — the message's own model still beats a
|
|
219
|
+
// transition that never ran. Nothing served at all leaves only the last
|
|
220
|
+
// transition to report.
|
|
221
|
+
if (resolvedModel === undefined) {
|
|
222
|
+
resolvedModel = servedModel ?? latestModelChange?.model;
|
|
223
|
+
resolvedModelIsFallback = servedModel !== undefined ? false : latestModelChange?.resolvedModelIsFallback;
|
|
224
|
+
}
|
|
190
225
|
if (contextTokens !== undefined) metrics.contextTokens = contextTokens;
|
|
191
226
|
return {
|
|
192
227
|
...(metrics.requests > 0 ? { metrics } : {}),
|
package/src/sdk.ts
CHANGED
|
@@ -83,6 +83,7 @@ import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./ex
|
|
|
83
83
|
import {
|
|
84
84
|
discoverAndLoadExtensions,
|
|
85
85
|
discoverExtensionPaths,
|
|
86
|
+
EXTENSION_HANDLER_TIMEOUT_MS,
|
|
86
87
|
type ExtensionContext,
|
|
87
88
|
type ExtensionFactory,
|
|
88
89
|
ExtensionRunner,
|
|
@@ -91,6 +92,7 @@ import {
|
|
|
91
92
|
type LoadExtensionsResult,
|
|
92
93
|
loadExtensionFromFactory,
|
|
93
94
|
loadExtensions,
|
|
95
|
+
type RegisteredTool,
|
|
94
96
|
type ToolDefinition,
|
|
95
97
|
wrapRegisteredTools,
|
|
96
98
|
} from "./extensibility/extensions";
|
|
@@ -108,6 +110,7 @@ import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-e
|
|
|
108
110
|
import {
|
|
109
111
|
deduplicateMCPToolsByName,
|
|
110
112
|
discoverAndLoadMCPTools,
|
|
113
|
+
getMCPToolOriginKey,
|
|
111
114
|
type MCPLoadResult,
|
|
112
115
|
MCPManager,
|
|
113
116
|
MCPToolCache,
|
|
@@ -1824,6 +1827,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
1824
1827
|
toolSession.enableMCP = enableMCP;
|
|
1825
1828
|
const deferMCPDiscoveryForUI = enableMCP && !mcpManager && options.hasUI === true;
|
|
1826
1829
|
const customTools: CustomTool[] = [];
|
|
1830
|
+
const initialMcpManagerTools: CustomTool[] = [];
|
|
1827
1831
|
let startDeferredMCPDiscovery: ((liveSession: AgentSession) => void) | undefined;
|
|
1828
1832
|
const startupQuiet = settings.get("startup.quiet");
|
|
1829
1833
|
const onMCPStatus = (event: McpConnectionStatusEvent) => {
|
|
@@ -1895,10 +1899,11 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
1895
1899
|
logger.error("MCP tool load failed", { path, error });
|
|
1896
1900
|
}
|
|
1897
1901
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
+
// MCP tools are LoadedCustomTool, extract the tool property while
|
|
1903
|
+
// retaining their origins for initial registry ownership.
|
|
1904
|
+
const loadedMcpTools = mcpResult.tools.map(loaded => loaded.tool);
|
|
1905
|
+
customTools.push(...loadedMcpTools);
|
|
1906
|
+
initialMcpManagerTools.push(...loadedMcpTools);
|
|
1902
1907
|
}
|
|
1903
1908
|
}
|
|
1904
1909
|
// Only top-level sessions own the global MCPManager. Subagents already
|
|
@@ -2574,6 +2579,11 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2574
2579
|
autoApprove: options.autoApprove ?? false,
|
|
2575
2580
|
});
|
|
2576
2581
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
2582
|
+
const setSessionActiveToolNames = (names: Iterable<string>): void => {
|
|
2583
|
+
const snapshot = Array.from(names);
|
|
2584
|
+
setActiveToolNames(snapshot);
|
|
2585
|
+
toolContextStore.setToolNames(snapshot);
|
|
2586
|
+
};
|
|
2577
2587
|
// Native built-in implementations backing same-tool `ctx.invokeTool`, so a tool that
|
|
2578
2588
|
// re-registers a built-in (e.g. wrapping `write`) can delegate to the original — reaching the
|
|
2579
2589
|
// unwrapped native execute, which inherits the caller's already-granted approval rather than
|
|
@@ -2584,10 +2594,12 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2584
2594
|
const nativeToolsByName = new Map<string, Tool>(toolSession.xdev?.tools ?? undefined);
|
|
2585
2595
|
|
|
2586
2596
|
const registeredTools = restrictToolNames ? [] : extensionRunner.getAllRegisteredTools();
|
|
2597
|
+
const initialRegisteredTools = new WeakSet(registeredTools);
|
|
2587
2598
|
const sdkCustomTools =
|
|
2588
2599
|
restrictToolNames && options.allowRestrictedCustomTools !== true
|
|
2589
2600
|
? []
|
|
2590
2601
|
: (options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? []);
|
|
2602
|
+
const sdkCustomToolNames = new Set(sdkCustomTools.map(tool => tool.name));
|
|
2591
2603
|
const allCustomTools = [
|
|
2592
2604
|
...registeredTools,
|
|
2593
2605
|
...sdkCustomTools.map(tool => {
|
|
@@ -2602,6 +2614,16 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2602
2614
|
const wrappedExtensionTools: Tool[] = deduplicateMCPToolsByName(
|
|
2603
2615
|
wrapRegisteredTools(allCustomTools, extensionRunner).map(wrapToolWithMetaNotice),
|
|
2604
2616
|
);
|
|
2617
|
+
const initialMcpManagerToolNames = new Set<string>();
|
|
2618
|
+
for (const tool of wrappedExtensionTools) {
|
|
2619
|
+
const originKey = getMCPToolOriginKey(tool);
|
|
2620
|
+
const matchesManagerOrigin =
|
|
2621
|
+
originKey !== undefined &&
|
|
2622
|
+
initialMcpManagerTools.some(
|
|
2623
|
+
managerTool => managerTool.name === tool.name && getMCPToolOriginKey(managerTool) === originKey,
|
|
2624
|
+
);
|
|
2625
|
+
if (matchesManagerOrigin) initialMcpManagerToolNames.add(tool.name);
|
|
2626
|
+
}
|
|
2605
2627
|
|
|
2606
2628
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
2607
2629
|
const builtInRegistryToolNames = toolSession.xdev?.builtInNames ?? new Set(toolRegistry.keys());
|
|
@@ -2635,6 +2657,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2635
2657
|
for (const name of collectPendingMCPToolNames(options.toolNames)) {
|
|
2636
2658
|
if (!toolRegistry.has(name)) {
|
|
2637
2659
|
toolRegistry.set(name, createPendingMCPTool(name));
|
|
2660
|
+
initialMcpManagerToolNames.add(name);
|
|
2638
2661
|
}
|
|
2639
2662
|
}
|
|
2640
2663
|
}
|
|
@@ -2785,7 +2808,6 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2785
2808
|
toolNames: string[],
|
|
2786
2809
|
tools: Map<string, AgentTool>,
|
|
2787
2810
|
): Promise<BuildSystemPromptResult> => {
|
|
2788
|
-
toolContextStore.setToolNames(toolNames);
|
|
2789
2811
|
const promptCwd = sessionManager.getCwd();
|
|
2790
2812
|
const activeRepoContext = hasSession
|
|
2791
2813
|
? await logger.time("resolveActiveRepoContext", resolveRepoContext, promptCwd)
|
|
@@ -3038,7 +3060,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3038
3060
|
if (mountedNames.length > 0 && !initialToolNames.includes("write")) initialToolNames.push("write");
|
|
3039
3061
|
}
|
|
3040
3062
|
|
|
3041
|
-
|
|
3063
|
+
setSessionActiveToolNames(initialToolNames);
|
|
3042
3064
|
const { systemPrompt } = await logger.time(
|
|
3043
3065
|
"buildSystemPrompt",
|
|
3044
3066
|
rebuildSystemPrompt,
|
|
@@ -3379,6 +3401,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3379
3401
|
? () => createVibeTools(toolSession)
|
|
3380
3402
|
: undefined,
|
|
3381
3403
|
builtInToolNames: builtInRegistryToolNames,
|
|
3404
|
+
mcpManagerToolNames: initialMcpManagerToolNames,
|
|
3382
3405
|
transformContext,
|
|
3383
3406
|
transformProviderContext,
|
|
3384
3407
|
onPayload,
|
|
@@ -3391,7 +3414,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3391
3414
|
getXdevToolEntries: () => (toolSession.xdev ? xdevEntries(toolSession.xdev) : []),
|
|
3392
3415
|
xdev: toolSession.xdev,
|
|
3393
3416
|
presentationPinnedToolNames: explicitlyRequestedToolNameSet,
|
|
3394
|
-
setActiveToolNames,
|
|
3417
|
+
setActiveToolNames: setSessionActiveToolNames,
|
|
3395
3418
|
ensureWriteRegistered,
|
|
3396
3419
|
getMcpServerInstructions: mcpManager
|
|
3397
3420
|
? () => {
|
|
@@ -3433,12 +3456,120 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3433
3456
|
titleSystemPrompt: options.titleSystemPrompt,
|
|
3434
3457
|
});
|
|
3435
3458
|
hasSession = true;
|
|
3459
|
+
// Extension factories normally register tools before session construction,
|
|
3460
|
+
// but Pi-compatible extensions may discover them asynchronously from a
|
|
3461
|
+
// session_start handler. Install those late registrations into the live
|
|
3462
|
+
// registry and serialize activation so no update can overwrite a sibling.
|
|
3463
|
+
const scheduledToolRegistrations = new WeakMap<RegisteredTool, Promise<void>>();
|
|
3464
|
+
const scheduleToolRegistration = (registered: RegisteredTool, signal?: AbortSignal): Promise<void> => {
|
|
3465
|
+
const scheduled = scheduledToolRegistrations.get(registered);
|
|
3466
|
+
if (scheduled) return scheduled;
|
|
3467
|
+
const activationSignal = signal ?? AbortSignal.timeout(EXTENSION_HANDLER_TIMEOUT_MS);
|
|
3468
|
+
|
|
3469
|
+
const [wrapped] = wrapRegisteredTools([registered], extensionRunner);
|
|
3470
|
+
if (!wrapped) return Promise.resolve();
|
|
3471
|
+
const name = registered.definition.name;
|
|
3472
|
+
const liveTool = new ExtensionToolWrapper(wrapToolWithMetaNotice(wrapped), extensionRunner);
|
|
3473
|
+
// Capture ordinary extension precedence while the listener observes this exact registration.
|
|
3474
|
+
// A later same-name registration may replace the extension map before serialized activation runs.
|
|
3475
|
+
const isEffectiveRegistrant = extensionRunner.getRegisteredTool(name) === registered;
|
|
3476
|
+
const activation = session.runToolRegistryMutation(async () => {
|
|
3477
|
+
activationSignal.throwIfAborted();
|
|
3478
|
+
const existingTool = toolRegistry.get(name);
|
|
3479
|
+
const previousExtensionMcpTool = session.getExtensionMCPTool(name);
|
|
3480
|
+
const wasMcpManagerTool = session.hasMCPManagerTool(name);
|
|
3481
|
+
if (existingTool) {
|
|
3482
|
+
// RPC host tools and SDK custom tools retain their startup precedence when an
|
|
3483
|
+
// extension registers the same name later.
|
|
3484
|
+
if (session.hasRpcHostTool(name) || sdkCustomToolNames.has(name)) return;
|
|
3485
|
+
// Put the replacement first so same-origin MCP re-registration keeps it. Distinct MCP origins still
|
|
3486
|
+
// use the stable winner; ordinary tool collisions retain the extension runner's last-wins precedence.
|
|
3487
|
+
const competingTools = deduplicateMCPToolsByName([liveTool, existingTool]);
|
|
3488
|
+
if (competingTools.length === 1) {
|
|
3489
|
+
if (competingTools[0] !== liveTool) return;
|
|
3490
|
+
} else if (!isEffectiveRegistrant) {
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
} else if (!isEffectiveRegistrant) {
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
const enabled = session.getEnabledToolNames();
|
|
3498
|
+
const alreadyEnabled = enabled.includes(name);
|
|
3499
|
+
const explicitlyRequested = explicitlyRequestedToolNameSet?.has(name) === true;
|
|
3500
|
+
const mounted = session.getMountedXdevToolNames();
|
|
3501
|
+
const wasBuiltIn = builtInRegistryToolNames.has(name);
|
|
3502
|
+
toolRegistry.set(name, liveTool);
|
|
3503
|
+
builtInRegistryToolNames.delete(name);
|
|
3504
|
+
session.setToolBuiltIn(name, false);
|
|
3505
|
+
session.setExtensionMCPTool(name, liveTool);
|
|
3506
|
+
try {
|
|
3507
|
+
if (registered.definition.defaultInactive && !explicitlyRequested) {
|
|
3508
|
+
if (!alreadyEnabled) return;
|
|
3509
|
+
await session.setActiveToolPresentation(
|
|
3510
|
+
enabled.filter(enabledName => enabledName !== name),
|
|
3511
|
+
mounted.filter(mountedName => mountedName !== name),
|
|
3512
|
+
existingTool !== undefined,
|
|
3513
|
+
activationSignal,
|
|
3514
|
+
);
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
// Re-registration refreshes the implementation, but it must not reverse an
|
|
3518
|
+
// explicit setActiveTools() decision that disabled the previous definition.
|
|
3519
|
+
if (existingTool && !alreadyEnabled) return;
|
|
3520
|
+
const shouldMount =
|
|
3521
|
+
!explicitlyRequested &&
|
|
3522
|
+
toolSession.xdev !== undefined &&
|
|
3523
|
+
builtInRegistryToolNames.has("read") &&
|
|
3524
|
+
builtInRegistryToolNames.has("write") &&
|
|
3525
|
+
enabled.includes("read") &&
|
|
3526
|
+
enabled.includes("write") &&
|
|
3527
|
+
isMountableUnderXdev(liveTool);
|
|
3528
|
+
const nextMounted = shouldMount
|
|
3529
|
+
? mounted.includes(name)
|
|
3530
|
+
? mounted
|
|
3531
|
+
: [...mounted, name]
|
|
3532
|
+
: mounted.filter(mountedName => mountedName !== name);
|
|
3533
|
+
await session.setActiveToolPresentation(
|
|
3534
|
+
alreadyEnabled ? enabled : [...enabled, name],
|
|
3535
|
+
nextMounted,
|
|
3536
|
+
existingTool !== undefined,
|
|
3537
|
+
activationSignal,
|
|
3538
|
+
);
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
if (existingTool) {
|
|
3541
|
+
toolRegistry.set(name, existingTool);
|
|
3542
|
+
} else {
|
|
3543
|
+
toolRegistry.delete(name);
|
|
3544
|
+
}
|
|
3545
|
+
if (wasBuiltIn) builtInRegistryToolNames.add(name);
|
|
3546
|
+
session.setToolBuiltIn(name, wasBuiltIn);
|
|
3547
|
+
session.setExtensionMCPTool(name, previousExtensionMcpTool);
|
|
3548
|
+
session.setMCPManagerTool(name, wasMcpManagerTool);
|
|
3549
|
+
throw error;
|
|
3550
|
+
}
|
|
3551
|
+
}, activationSignal);
|
|
3552
|
+
scheduledToolRegistrations.set(registered, activation);
|
|
3553
|
+
return activation;
|
|
3554
|
+
};
|
|
3555
|
+
if (!restrictToolNames) {
|
|
3556
|
+
const unsubscribeToolRegistrations = extensionRunner.onToolRegistered(scheduleToolRegistration);
|
|
3557
|
+
disposeCallbacks.add(unsubscribeToolRegistrations);
|
|
3558
|
+
|
|
3559
|
+
// Close the construction race: a background registration can land after
|
|
3560
|
+
// the initial snapshot but before the live listener above is attached.
|
|
3561
|
+
for (const registered of extensionRunner.getAllRegisteredTools()) {
|
|
3562
|
+
if (!initialRegisteredTools.has(registered)) {
|
|
3563
|
+
await scheduleToolRegistration(registered);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3436
3567
|
session.yieldQueue.register<McpNotificationEntry>("mcp-notification", {
|
|
3437
3568
|
build: buildMcpNotificationBatchMessage,
|
|
3438
3569
|
});
|
|
3439
3570
|
session.yieldQueue.register<DeferredDiagnosticsEntry>(LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE, {
|
|
3440
|
-
isStale: entry => entry.isStale(),
|
|
3441
3571
|
build: buildLateDiagnosticsBatchMessage,
|
|
3572
|
+
isStale: entry => entry.isStale(),
|
|
3442
3573
|
});
|
|
3443
3574
|
|
|
3444
3575
|
// Attach the live session to the pre-registered ref so peers can route IRC
|
|
@@ -174,6 +174,8 @@ export interface AgentSessionConfig {
|
|
|
174
174
|
createVibeTools?: () => AgentTool[];
|
|
175
175
|
/** Names whose current registry entry is the built-in implementation. */
|
|
176
176
|
builtInToolNames?: Iterable<string>;
|
|
177
|
+
/** MCP names whose initial registry entries came from the manager snapshot. */
|
|
178
|
+
mcpManagerToolNames?: Iterable<string>;
|
|
177
179
|
/** Updates tool-session predicates from the live active tool set. */
|
|
178
180
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
179
181
|
/** Registers the write transport when runtime xdev mounts first need it. */
|