@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.7
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 +23 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-5k4dq4g6.md} +23 -0
- package/dist/cli.js +3149 -3092
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/model-controls.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +12 -12
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/shared-events.ts +18 -1
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +123 -7
- package/src/session/model-controls.ts +5 -5
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
|
@@ -21,8 +21,9 @@ import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
|
21
21
|
import { computerExposureMode } from "../tools/computer/exposure";
|
|
22
22
|
import { wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
23
23
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
24
|
-
import { isMountableUnderXdev, type
|
|
24
|
+
import { isMountableUnderXdev, listXdevTools, type XdevState, xdevDocsFor, xdevEntries } from "../tools/xdev";
|
|
25
25
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
26
|
+
import { type InspectImageMode, isInspectImageToolActive } from "../utils/inspect-image-mode";
|
|
26
27
|
import { formatLocalCalendarDate } from "../utils/local-date";
|
|
27
28
|
import {
|
|
28
29
|
extractPermissionLocations,
|
|
@@ -56,6 +57,9 @@ export interface SessionToolsHost {
|
|
|
56
57
|
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
57
58
|
notifyCommandMetadataChanged(): void;
|
|
58
59
|
localProtocolOptions(): LocalProtocolOptions;
|
|
60
|
+
/** Session-scoped `/vision` override; undefined means "follow the persisted setting". */
|
|
61
|
+
getInspectImageModeOverride(): InspectImageMode | undefined;
|
|
62
|
+
setInspectImageModeOverride(mode: InspectImageMode | undefined): void;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
interface SessionToolsOptions {
|
|
@@ -63,14 +67,15 @@ interface SessionToolsOptions {
|
|
|
63
67
|
toolRegistry?: Map<string, AgentTool>;
|
|
64
68
|
createVibeTools?: () => AgentTool[];
|
|
65
69
|
createComputerTool?: () => Promise<AgentTool | null>;
|
|
70
|
+
/** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link SessionTools.setInspectImageMode}). */
|
|
71
|
+
createInspectImageTool?: () => Promise<AgentTool | null>;
|
|
66
72
|
builtInToolNames?: Iterable<string>;
|
|
67
73
|
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
68
74
|
ensureWriteRegistered?: () => Promise<boolean>;
|
|
69
75
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
70
76
|
getLocalCalendarDate?: () => string;
|
|
71
77
|
getMcpServerInstructions?: () => Map<string, string> | undefined;
|
|
72
|
-
|
|
73
|
-
initialMountedXdevToolNames?: string[];
|
|
78
|
+
xdev?: XdevState;
|
|
74
79
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
75
80
|
baseSystemPrompt: string[];
|
|
76
81
|
skills?: Skill[];
|
|
@@ -161,11 +166,11 @@ export class SessionTools {
|
|
|
161
166
|
#toolRegistry: Map<string, AgentTool>;
|
|
162
167
|
#createVibeTools: (() => AgentTool[]) | undefined;
|
|
163
168
|
#createComputerTool: SessionToolsOptions["createComputerTool"];
|
|
169
|
+
#createInspectImageTool: SessionToolsOptions["createInspectImageTool"];
|
|
164
170
|
#installedVibeToolNames = new Set<string>();
|
|
165
171
|
#builtInToolNames: Set<string>;
|
|
166
172
|
#rpcHostToolNames = new Set<string>();
|
|
167
|
-
#
|
|
168
|
-
#mountedXdevToolNames: Set<string>;
|
|
173
|
+
#xdev: XdevState | undefined;
|
|
169
174
|
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
170
175
|
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
171
176
|
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
@@ -190,14 +195,18 @@ export class SessionTools {
|
|
|
190
195
|
this.#toolRegistry = options.toolRegistry ?? new Map();
|
|
191
196
|
this.#createVibeTools = options.createVibeTools;
|
|
192
197
|
this.#createComputerTool = options.createComputerTool;
|
|
198
|
+
this.#createInspectImageTool = options.createInspectImageTool;
|
|
193
199
|
this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
|
|
194
200
|
this.#presentationPinnedToolNames = options.presentationPinnedToolNames;
|
|
195
201
|
this.#ensureWriteRegistered = options.ensureWriteRegistered;
|
|
196
202
|
this.#rebuildSystemPrompt = options.rebuildSystemPrompt;
|
|
197
203
|
this.#getLocalCalendarDate = options.getLocalCalendarDate ?? formatLocalCalendarDate;
|
|
198
204
|
this.#getMcpServerInstructions = options.getMcpServerInstructions;
|
|
199
|
-
this.#
|
|
200
|
-
this.#
|
|
205
|
+
this.#xdev = options.xdev;
|
|
206
|
+
if (this.#xdev && this.#xdev.tools !== this.#toolRegistry) {
|
|
207
|
+
throw new Error("xd:// state must reference the canonical session tool map");
|
|
208
|
+
}
|
|
209
|
+
if (this.#xdev) this.#xdev.decorateExecution = tool => this.#wrapToolForAcpPermission(tool);
|
|
201
210
|
this.#setActiveToolNames = options.setActiveToolNames;
|
|
202
211
|
this.#baseSystemPrompt = options.baseSystemPrompt;
|
|
203
212
|
this.#skills = options.skills ?? [];
|
|
@@ -242,7 +251,7 @@ export class SessionTools {
|
|
|
242
251
|
this.#acpPermissionDecisions.clear();
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
/**
|
|
254
|
+
/** Drops cached ACP decisions and re-wraps active tools after the client changes. */
|
|
246
255
|
refreshAcpPermissionGates(): void {
|
|
247
256
|
this.#acpPermissionDecisions.clear();
|
|
248
257
|
const activeTools = this.getActiveToolNames()
|
|
@@ -250,11 +259,6 @@ export class SessionTools {
|
|
|
250
259
|
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
251
260
|
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
252
261
|
this.#host.agent.setTools(activeTools);
|
|
253
|
-
const mountedTools = [...this.#mountedXdevToolNames]
|
|
254
|
-
.map(name => this.#toolRegistry.get(name))
|
|
255
|
-
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
256
|
-
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
257
|
-
this.#xdevRegistry?.reconcile(mountedTools);
|
|
258
262
|
}
|
|
259
263
|
|
|
260
264
|
#getActiveNonMCPToolNames(): string[] {
|
|
@@ -268,13 +272,14 @@ export class SessionTools {
|
|
|
268
272
|
|
|
269
273
|
/** Enabled top-level and discoverable tool names. */
|
|
270
274
|
getEnabledToolNames(): string[] {
|
|
271
|
-
|
|
272
|
-
return
|
|
275
|
+
const mountedNames = this.#xdev?.mountedNames;
|
|
276
|
+
if (!mountedNames || mountedNames.size === 0) return this.getActiveToolNames();
|
|
277
|
+
return [...this.getActiveToolNames(), ...mountedNames];
|
|
273
278
|
}
|
|
274
279
|
|
|
275
|
-
/** Names
|
|
280
|
+
/** Names currently presented as `xd://` devices. */
|
|
276
281
|
getMountedXdevToolNames(): string[] {
|
|
277
|
-
return [...this.#
|
|
282
|
+
return [...(this.#xdev?.mountedNames ?? [])];
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
/** Whether the edit tool is registered. */
|
|
@@ -403,6 +408,10 @@ export class SessionTools {
|
|
|
403
408
|
} else if (computerExpected) {
|
|
404
409
|
this.#logComputerState("Computer tool retained after model change", true);
|
|
405
410
|
}
|
|
411
|
+
|
|
412
|
+
// inspect_image auto mode keys off model image capability, so a model
|
|
413
|
+
// switch can flip the tool either way.
|
|
414
|
+
await this.reconcileInspectImageAfterModelChange();
|
|
406
415
|
}
|
|
407
416
|
|
|
408
417
|
/** Enabled MCP tools in their current presentation partition. */
|
|
@@ -543,10 +552,7 @@ export class SessionTools {
|
|
|
543
552
|
this.#presentationPinnedToolNames?.has(name) === true || this.#runtimeSelectedToolNames?.has(name) === true;
|
|
544
553
|
const mountCandidates = selectedTools.filter(
|
|
545
554
|
({ name, tool }) =>
|
|
546
|
-
this.#
|
|
547
|
-
xdevReadAvailable &&
|
|
548
|
-
!isPresentationPinned(name) &&
|
|
549
|
-
isMountableUnderXdev(tool),
|
|
555
|
+
this.#xdev !== undefined && xdevReadAvailable && !isPresentationPinned(name) && isMountableUnderXdev(tool),
|
|
550
556
|
);
|
|
551
557
|
|
|
552
558
|
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
@@ -557,19 +563,15 @@ export class SessionTools {
|
|
|
557
563
|
const mountNames = builtInWriteAvailable ? new Set(mountCandidates.map(({ name }) => name)) : new Set<string>();
|
|
558
564
|
const tools: AgentTool[] = [];
|
|
559
565
|
const validToolNames: string[] = [];
|
|
560
|
-
const mountedTools: AgentTool[] = [];
|
|
561
566
|
for (const { name, tool } of selectedTools) {
|
|
562
|
-
if (mountNames.has(name))
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
566
|
-
validToolNames.push(name);
|
|
567
|
-
}
|
|
567
|
+
if (mountNames.has(name)) continue;
|
|
568
|
+
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
569
|
+
validToolNames.push(name);
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
const pinnedWrite = isPresentationPinned("write");
|
|
571
573
|
const activeDeferrableTool = tools.some(tool => tool.deferrable === true);
|
|
572
|
-
const transportNeeded =
|
|
574
|
+
const transportNeeded = mountNames.size > 0 || activeDeferrableTool || this.#host.planModeEnabled();
|
|
573
575
|
if (transportNeeded && !builtInWriteAvailable) {
|
|
574
576
|
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
575
577
|
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
@@ -590,14 +592,9 @@ export class SessionTools {
|
|
|
590
592
|
if (writeToolIndex >= 0) tools.splice(writeToolIndex, 1);
|
|
591
593
|
}
|
|
592
594
|
|
|
593
|
-
const previousMounted = this.#
|
|
594
|
-
const previousMountedTools = [...previousMounted].flatMap(name => {
|
|
595
|
-
const tool = this.#xdevRegistry?.get(name);
|
|
596
|
-
return tool ? [tool] : [];
|
|
597
|
-
});
|
|
595
|
+
const previousMounted = new Set(this.#xdev?.mountedNames ?? []);
|
|
598
596
|
const previousActiveToolNames = this.getActiveToolNames();
|
|
599
|
-
this.#
|
|
600
|
-
this.#xdevRegistry?.reconcile(mountedTools);
|
|
597
|
+
this.#setMountedNames(mountNames);
|
|
601
598
|
this.#setActiveToolNames?.(validToolNames);
|
|
602
599
|
|
|
603
600
|
let rebuiltSystemPrompt: string[] | undefined;
|
|
@@ -612,15 +609,13 @@ export class SessionTools {
|
|
|
612
609
|
}
|
|
613
610
|
}
|
|
614
611
|
} catch (error) {
|
|
615
|
-
this.#
|
|
616
|
-
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
612
|
+
this.#setMountedNames(previousMounted);
|
|
617
613
|
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
618
614
|
throw error;
|
|
619
615
|
}
|
|
620
616
|
|
|
621
617
|
if (this.#host.isDisposed()) {
|
|
622
|
-
this.#
|
|
623
|
-
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
618
|
+
this.#setMountedNames(previousMounted);
|
|
624
619
|
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
625
620
|
return;
|
|
626
621
|
}
|
|
@@ -637,6 +632,13 @@ export class SessionTools {
|
|
|
637
632
|
}
|
|
638
633
|
}
|
|
639
634
|
|
|
635
|
+
#setMountedNames(names: Iterable<string>): void {
|
|
636
|
+
const mountedNames = this.#xdev?.mountedNames;
|
|
637
|
+
if (!mountedNames) return;
|
|
638
|
+
mountedNames.clear();
|
|
639
|
+
for (const name of names) mountedNames.add(name);
|
|
640
|
+
}
|
|
641
|
+
|
|
640
642
|
/**
|
|
641
643
|
* Record a mid-session `xd://` mount delta for the model. Non-MCP mount
|
|
642
644
|
* churn remains notice-only, leaving the system prompt and provider cache
|
|
@@ -649,9 +651,8 @@ export class SessionTools {
|
|
|
649
651
|
* Full docs join the system prompt opportunistically on a rebuild.
|
|
650
652
|
*/
|
|
651
653
|
#notifyXdevMountDelta(previousMounted: ReadonlySet<string>): void {
|
|
652
|
-
const
|
|
653
|
-
if (!
|
|
654
|
-
const current = this.#mountedXdevToolNames;
|
|
654
|
+
const current = this.#xdev?.mountedNames;
|
|
655
|
+
if (!current) return;
|
|
655
656
|
const addedNames = [...current].filter(name => !previousMounted.has(name));
|
|
656
657
|
const removedNames = [...previousMounted].filter(name => !current.has(name));
|
|
657
658
|
if (addedNames.length === 0 && removedNames.length === 0) return;
|
|
@@ -678,14 +679,17 @@ export class SessionTools {
|
|
|
678
679
|
const pending = this.#pendingXdevMountDelta;
|
|
679
680
|
if (!pending) return undefined;
|
|
680
681
|
this.#pendingXdevMountDelta = undefined;
|
|
681
|
-
const summaries = new Map(this.#
|
|
682
|
+
const summaries = new Map(this.#xdev ? xdevEntries(this.#xdev).map(entry => [entry.name, entry.summary]) : []);
|
|
682
683
|
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
683
684
|
const removed = [...pending.removed].map(name => ({ name }));
|
|
684
|
-
const docs = this.#
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
685
|
+
const docs = this.#xdev
|
|
686
|
+
? xdevDocsFor(
|
|
687
|
+
this.#xdev,
|
|
688
|
+
pending.added,
|
|
689
|
+
this.#host.settings.get("tools.xdevDocs"),
|
|
690
|
+
this.#host.settings.get("tools.xdevInlineDevices"),
|
|
691
|
+
)
|
|
692
|
+
: "";
|
|
689
693
|
return {
|
|
690
694
|
role: "custom",
|
|
691
695
|
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
@@ -727,7 +731,7 @@ export class SessionTools {
|
|
|
727
731
|
// selection change should not demote `write` unless it is already active.
|
|
728
732
|
await this.#applyToolPresentation(
|
|
729
733
|
normalized,
|
|
730
|
-
this.#
|
|
734
|
+
this.#xdev?.mountedNames ?? new Set(),
|
|
731
735
|
this.getActiveToolNames().includes("write"),
|
|
732
736
|
);
|
|
733
737
|
}
|
|
@@ -736,7 +740,7 @@ export class SessionTools {
|
|
|
736
740
|
* Restore an enabled tool set with its exact top-level versus `xd://` partition.
|
|
737
741
|
*
|
|
738
742
|
* Both inputs are required because {@link setActiveToolsByName} only receives the
|
|
739
|
-
* enabled name list and classifies mounts from the current
|
|
743
|
+
* enabled name list and classifies mounts from the current presentation set.
|
|
740
744
|
* Rollback/restore callers must pass the snapshotted mounted subset so names that
|
|
741
745
|
* were top-level stay pinned (`#runtimeSelectedToolNames`) and names that were under
|
|
742
746
|
* `xd://` remain mount-eligible, even when the live mount set has drifted.
|
|
@@ -849,6 +853,108 @@ export class SessionTools {
|
|
|
849
853
|
return true;
|
|
850
854
|
}
|
|
851
855
|
|
|
856
|
+
/** Current effective inspect_image state for `/vision status`. */
|
|
857
|
+
inspectImageState(): { mode: InspectImageMode; active: boolean; model: string | undefined } {
|
|
858
|
+
const model = this.#host.model();
|
|
859
|
+
return {
|
|
860
|
+
mode: this.#host.getInspectImageModeOverride() ?? this.#host.settings.get("inspect_image.mode"),
|
|
861
|
+
active: this.getEnabledToolNames().includes("inspect_image"),
|
|
862
|
+
model: model ? formatModelString(model) : undefined,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Brings the active tool set in line with the effective inspect_image state
|
|
868
|
+
* (mode setting, `/vision` override, active-model image capability).
|
|
869
|
+
* Mirrors {@link setComputerToolEnabled}: enabling builds the tool through
|
|
870
|
+
* the config factory on first use and reuses the registry entry afterwards.
|
|
871
|
+
* Idempotent — safe to call from every model/settings change path.
|
|
872
|
+
*
|
|
873
|
+
* @returns false when the tool should be active but this session cannot
|
|
874
|
+
* build it (e.g. restricted child sessions have no factory).
|
|
875
|
+
*/
|
|
876
|
+
async reconcileInspectImageTool(): Promise<boolean> {
|
|
877
|
+
const expected = isInspectImageToolActive({
|
|
878
|
+
settings: this.#host.settings,
|
|
879
|
+
getActiveModel: () => this.#host.model(),
|
|
880
|
+
getInspectImageModeOverride: () => this.#host.getInspectImageModeOverride(),
|
|
881
|
+
});
|
|
882
|
+
// Keep the read tool's advertised description in sync BEFORE any prompt
|
|
883
|
+
// rebuild below, passing the post-change availability so the prompt never
|
|
884
|
+
// lags a flip in either direction. Per-read lazy sync is the backstop.
|
|
885
|
+
const syncReadDescription = (available: boolean): void => {
|
|
886
|
+
const readTool = this.#toolRegistry.get("read") as
|
|
887
|
+
| { syncInspectImageState?: (available?: boolean) => boolean }
|
|
888
|
+
| undefined;
|
|
889
|
+
readTool?.syncInspectImageState?.(available);
|
|
890
|
+
};
|
|
891
|
+
const active = this.getEnabledToolNames();
|
|
892
|
+
const isActive = active.includes("inspect_image");
|
|
893
|
+
if (expected === isActive) {
|
|
894
|
+
syncReadDescription(isActive);
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
if (!expected) {
|
|
898
|
+
syncReadDescription(false);
|
|
899
|
+
await this.applyActiveToolsByName(active.filter(name => name !== "inspect_image"));
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
if (!this.#toolRegistry.has("inspect_image")) {
|
|
903
|
+
const tool = await this.#createInspectImageTool?.();
|
|
904
|
+
if (tool?.name !== "inspect_image") {
|
|
905
|
+
logger.warn("inspect_image tool could not be created", {
|
|
906
|
+
model: this.#host.model()?.id,
|
|
907
|
+
});
|
|
908
|
+
syncReadDescription(false);
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
const wrapped = this.#wrapRuntimeTool(tool);
|
|
912
|
+
this.#toolRegistry.set(wrapped.name, wrapped);
|
|
913
|
+
this.#builtInToolNames.add(wrapped.name);
|
|
914
|
+
}
|
|
915
|
+
syncReadDescription(true);
|
|
916
|
+
await this.applyActiveToolsByName([...active, "inspect_image"]);
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Reconciles inspect_image after a model change and surfaces a notice when
|
|
922
|
+
* the visible tool set actually flipped. Called from every model-change
|
|
923
|
+
* path — including retry-fallback switches that bypass
|
|
924
|
+
* {@link syncAfterModelChange}.
|
|
925
|
+
*/
|
|
926
|
+
async reconcileInspectImageAfterModelChange(): Promise<void> {
|
|
927
|
+
const before = this.getEnabledToolNames().includes("inspect_image");
|
|
928
|
+
const reconciled = await this.reconcileInspectImageTool();
|
|
929
|
+
const after = this.getEnabledToolNames().includes("inspect_image");
|
|
930
|
+
if (!reconciled || before === after) return;
|
|
931
|
+
const model = this.#host.model();
|
|
932
|
+
const modelName = model ? formatModelString(model) : "the current model";
|
|
933
|
+
this.#host.emitNotice(
|
|
934
|
+
"info",
|
|
935
|
+
after
|
|
936
|
+
? `inspect_image is now available: ${modelName} has no native image input.`
|
|
937
|
+
: `inspect_image is now hidden: ${modelName} supports image input natively. Override with /vision on.`,
|
|
938
|
+
"vision",
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Session-scoped `/vision` override. `auto` clears the override so the
|
|
944
|
+
* persisted `inspect_image.mode` setting (itself possibly `auto`) decides;
|
|
945
|
+
* `on`/`off` force the tool for this session only. Takes effect before the
|
|
946
|
+
* next model call.
|
|
947
|
+
*
|
|
948
|
+
* @returns false when `on` was requested but the tool cannot be built here.
|
|
949
|
+
*/
|
|
950
|
+
async setInspectImageMode(mode: InspectImageMode): Promise<boolean> {
|
|
951
|
+
this.#host.setInspectImageModeOverride(mode === "auto" ? undefined : mode);
|
|
952
|
+
const applied = await this.reconcileInspectImageTool();
|
|
953
|
+
const { active, model } = this.inspectImageState();
|
|
954
|
+
logger.debug("inspect_image mode changed", { mode, active, model });
|
|
955
|
+
return applied;
|
|
956
|
+
}
|
|
957
|
+
|
|
852
958
|
/** Rebuilds the stable base prompt for the current tools and model. */
|
|
853
959
|
async refreshBaseSystemPrompt(): Promise<void> {
|
|
854
960
|
if (this.#host.isDisposed() || !this.#rebuildSystemPrompt) return;
|
|
@@ -964,7 +1070,7 @@ export class SessionTools {
|
|
|
964
1070
|
`${tool.name}=${tool.label ?? ""}|${tool.description ?? ""}|${tool.customWireName ?? ""}`;
|
|
965
1071
|
const descriptionSegment = tools.map(describeTool).join("\u0002");
|
|
966
1072
|
const mountedMCPProjection = projectMountedMCPXdevGuidance(
|
|
967
|
-
collectMountedMCPToolRoutes(this.#
|
|
1073
|
+
collectMountedMCPToolRoutes(this.#xdev ? listXdevTools(this.#xdev) : []),
|
|
968
1074
|
);
|
|
969
1075
|
const mountedMCPRouteSegment =
|
|
970
1076
|
JSON.stringify({
|
|
@@ -53,12 +53,17 @@ export interface OutputSummary {
|
|
|
53
53
|
export interface OutputSinkOptions {
|
|
54
54
|
artifactPath?: string;
|
|
55
55
|
artifactId?: string;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Total inline body budget (bytes). Default DEFAULT_MAX_BYTES. The head
|
|
58
|
+
* window and rolling tail window share this budget, so a composed
|
|
59
|
+
* `dump()` body never exceeds it (plus the elision marker).
|
|
60
|
+
*/
|
|
57
61
|
spillThreshold?: number;
|
|
58
62
|
/**
|
|
59
63
|
* When > 0, the sink keeps the first `headBytes` of output in addition to
|
|
60
64
|
* the rolling tail window. Output between the two windows is elided
|
|
61
|
-
* (middle elision).
|
|
65
|
+
* (middle elision). Clamped to `spillThreshold / 2` so the tail keeps at
|
|
66
|
+
* least half the inline budget. Default 0 = tail-only behavior.
|
|
62
67
|
*/
|
|
63
68
|
headBytes?: number;
|
|
64
69
|
/**
|
|
@@ -799,7 +804,7 @@ export class OutputSink {
|
|
|
799
804
|
this.#artifactPath = artifactPath;
|
|
800
805
|
this.#artifactId = artifactId;
|
|
801
806
|
this.#spillThreshold = spillThreshold;
|
|
802
|
-
this.#headLimit = Math.max(0, headBytes);
|
|
807
|
+
this.#headLimit = Math.max(0, Math.min(headBytes, Math.floor(spillThreshold / 2)));
|
|
803
808
|
this.#maxColumns = Math.max(0, maxColumns);
|
|
804
809
|
this.#onChunk = onChunk;
|
|
805
810
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
@@ -969,16 +974,23 @@ export class OutputSink {
|
|
|
969
974
|
return parts.join("");
|
|
970
975
|
}
|
|
971
976
|
|
|
977
|
+
// The rolling tail budget is whatever the head window has not consumed of
|
|
978
|
+
// the inline budget: `spillThreshold - #headBytes`. While the head window
|
|
979
|
+
// fills it shrinks toward `spillThreshold - headLimit`; after `replace()`
|
|
980
|
+
// (head cleared) it grows back to the full threshold. This keeps
|
|
981
|
+
// `head + tail <= spillThreshold`, so the composed dump body fits the
|
|
982
|
+
// inline byte cap by construction.
|
|
983
|
+
|
|
972
984
|
#willOverflow(dataBytes: number): boolean {
|
|
973
985
|
// Triggers file mirroring as soon as the next chunk would push us over
|
|
974
|
-
// the tail budget
|
|
975
|
-
return this.#bufferBytes + dataBytes > this.#spillThreshold;
|
|
986
|
+
// the tail budget — i.e. the first byte that could be lost from memory.
|
|
987
|
+
return this.#bufferBytes + dataBytes > this.#spillThreshold - this.#headBytes;
|
|
976
988
|
}
|
|
977
989
|
|
|
978
990
|
#pushTail(chunk: string, dataBytes: number): void {
|
|
979
991
|
if (dataBytes === 0) return;
|
|
980
992
|
|
|
981
|
-
const threshold = this.#spillThreshold;
|
|
993
|
+
const threshold = Math.max(0, this.#spillThreshold - this.#headBytes);
|
|
982
994
|
const willOverflow = this.#bufferBytes + dataBytes > threshold;
|
|
983
995
|
|
|
984
996
|
if (!willOverflow) {
|
|
@@ -118,7 +118,7 @@ export interface TurnRecoveryHost {
|
|
|
118
118
|
waitForSessionMessagePersistence(message: AssistantMessage): Promise<void>;
|
|
119
119
|
appendSessionMessage(message: AssistantMessage): void;
|
|
120
120
|
sessionMessageAlreadyPersisted(message: AssistantMessage): boolean;
|
|
121
|
-
setModelWithProviderSessionReset(model: Model): void
|
|
121
|
+
setModelWithProviderSessionReset(model: Model): Promise<void>;
|
|
122
122
|
resetCurrentResponsesProviderSession(reason: string): void;
|
|
123
123
|
maybeAutoRedeemCodexReset(): Promise<boolean>;
|
|
124
124
|
runAutoCompaction(
|
|
@@ -1028,7 +1028,7 @@ export class TurnRecovery {
|
|
|
1028
1028
|
? requestedThinkingLevel
|
|
1029
1029
|
: clampThinkingLevelToCeiling(candidate, requestedThinkingLevel, this.#host.thinkingLevelCeiling());
|
|
1030
1030
|
const candidateSelector = formatModelStringWithRouting(candidate);
|
|
1031
|
-
this.#host.setModelWithProviderSessionReset(candidate);
|
|
1031
|
+
await this.#host.setModelWithProviderSessionReset(candidate);
|
|
1032
1032
|
this.#host.sessionManager.appendModelChange(candidateSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
1033
1033
|
this.#host.settings.getStorage()?.recordModelUsage(candidateSelector);
|
|
1034
1034
|
this.#host.setThinkingLevel(nextThinkingLevel);
|
|
@@ -1147,7 +1147,7 @@ export class TurnRecovery {
|
|
|
1147
1147
|
const apiKey = await this.#host.modelRegistry.getApiKey(baseModel, this.#host.sessionId());
|
|
1148
1148
|
if (!apiKey) return false;
|
|
1149
1149
|
const baseSelector = formatModelStringWithRouting(baseModel);
|
|
1150
|
-
this.#host.setModelWithProviderSessionReset(baseModel);
|
|
1150
|
+
await this.#host.setModelWithProviderSessionReset(baseModel);
|
|
1151
1151
|
this.#host.sessionManager.appendModelChange(baseSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
1152
1152
|
this.#host.settings.getStorage()?.recordModelUsage(baseSelector);
|
|
1153
1153
|
await this.#host.emitSessionEvent({
|
|
@@ -1201,7 +1201,7 @@ export class TurnRecovery {
|
|
|
1201
1201
|
const thinkingToApply =
|
|
1202
1202
|
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
1203
1203
|
const primarySelector = formatModelStringWithRouting(primaryModel);
|
|
1204
|
-
this.#host.setModelWithProviderSessionReset(primaryModel);
|
|
1204
|
+
await this.#host.setModelWithProviderSessionReset(primaryModel);
|
|
1205
1205
|
this.#host.sessionManager.appendModelChange(primarySelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
1206
1206
|
this.#host.settings.getStorage()?.recordModelUsage(primarySelector);
|
|
1207
1207
|
this.#host.setThinkingLevel(thinkingToApply);
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
renderChangelogEntries,
|
|
54
54
|
} from "../utils/changelog";
|
|
55
55
|
import { copyToClipboard } from "../utils/clipboard";
|
|
56
|
+
import type { InspectImageMode } from "../utils/inspect-image-mode";
|
|
56
57
|
import { CollabQrCodeComponent } from "./helpers/collab-qrcode";
|
|
57
58
|
import { buildContextReportText } from "./helpers/context-report";
|
|
58
59
|
import { formatDuration } from "./helpers/format";
|
|
@@ -151,6 +152,33 @@ async function applyComputerUseToggle(session: AgentSession, enable: boolean): P
|
|
|
151
152
|
: "Computer use disabled for this session.";
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
/** Session-effective `/vision status` line. */
|
|
156
|
+
function formatVisionStatus(session: AgentSession): string {
|
|
157
|
+
const { mode, active, model } = session.inspectImageState();
|
|
158
|
+
const override = session.getInspectImageModeOverride();
|
|
159
|
+
const modelObj = session.model;
|
|
160
|
+
const capability = modelObj
|
|
161
|
+
? modelObj.input.includes("image")
|
|
162
|
+
? "native image input"
|
|
163
|
+
: "no native image input"
|
|
164
|
+
: "no active model";
|
|
165
|
+
return [
|
|
166
|
+
`inspect_image: ${active ? "active" : "inactive"}`,
|
|
167
|
+
`mode: ${mode}${override ? " (session override)" : ""}`,
|
|
168
|
+
...(override ? [`configured: ${session.settings.get("inspect_image.mode")}`] : []),
|
|
169
|
+
`model: ${model ?? "none"} (${capability})`,
|
|
170
|
+
].join(" · ");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Applies a `/vision` mode for this session and returns the operator feedback line. */
|
|
174
|
+
async function applyVisionMode(session: AgentSession, mode: InspectImageMode): Promise<string> {
|
|
175
|
+
const applied = await session.setInspectImageMode(mode);
|
|
176
|
+
if (!applied) {
|
|
177
|
+
return "inspect_image is unavailable in this session.";
|
|
178
|
+
}
|
|
179
|
+
return `Vision mode: ${mode}. ${formatVisionStatus(session)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
154
182
|
const AUTOCOMPLETE_DETAIL_LIMIT = 48;
|
|
155
183
|
|
|
156
184
|
function shortDetail(value: string, limit = AUTOCOMPLETE_DETAIL_LIMIT): string {
|
|
@@ -647,6 +675,47 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
647
675
|
runtime.ctx.editor.setText("");
|
|
648
676
|
},
|
|
649
677
|
},
|
|
678
|
+
{
|
|
679
|
+
name: "vision",
|
|
680
|
+
description: "Control the inspect_image vision-delegation tool for this session",
|
|
681
|
+
acpDescription: "Toggle vision delegation",
|
|
682
|
+
acpInputHint: "[on|off|auto|status]",
|
|
683
|
+
subcommands: [
|
|
684
|
+
{ name: "on", description: "Always expose inspect_image this session" },
|
|
685
|
+
{ name: "off", description: "Never expose inspect_image this session" },
|
|
686
|
+
{ name: "auto", description: "Follow inspect_image.mode (auto hides it for vision-capable models)" },
|
|
687
|
+
{ name: "status", description: "Show inspect_image status" },
|
|
688
|
+
],
|
|
689
|
+
allowArgs: true,
|
|
690
|
+
getTuiAutocompleteDescription: runtime => `Vision: ${runtime.ctx.session.inspectImageState().mode}`,
|
|
691
|
+
handle: async (command, runtime) => {
|
|
692
|
+
const arg = command.args.trim().toLowerCase();
|
|
693
|
+
if (arg === "status") {
|
|
694
|
+
await runtime.output(formatVisionStatus(runtime.session));
|
|
695
|
+
return commandConsumed();
|
|
696
|
+
}
|
|
697
|
+
if (arg === "on" || arg === "off" || arg === "auto") {
|
|
698
|
+
await runtime.output(await applyVisionMode(runtime.session, arg));
|
|
699
|
+
return commandConsumed();
|
|
700
|
+
}
|
|
701
|
+
return usage("Usage: /vision [on|off|auto|status]", runtime);
|
|
702
|
+
},
|
|
703
|
+
handleTui: async (command, runtime) => {
|
|
704
|
+
const arg = command.args.trim().toLowerCase();
|
|
705
|
+
if (arg === "status") {
|
|
706
|
+
runtime.ctx.showStatus(formatVisionStatus(runtime.ctx.session));
|
|
707
|
+
runtime.ctx.editor.setText("");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (arg === "on" || arg === "off" || arg === "auto") {
|
|
711
|
+
runtime.ctx.showStatus(await applyVisionMode(runtime.ctx.session, arg));
|
|
712
|
+
runtime.ctx.editor.setText("");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
runtime.ctx.showStatus("Usage: /vision [on|off|auto|status]");
|
|
716
|
+
runtime.ctx.editor.setText("");
|
|
717
|
+
},
|
|
718
|
+
},
|
|
650
719
|
{
|
|
651
720
|
name: "prewalk",
|
|
652
721
|
description: "Switch to a fast/cheap model at the next action (works even without --prewalk)",
|
package/src/tools/bash.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { invalidateGithubCacheForBashCommand } from "./gh-cache-invalidation";
|
|
|
37
37
|
import {
|
|
38
38
|
formatStyledTruncationWarning,
|
|
39
39
|
type OutputMeta,
|
|
40
|
+
resolveInlineByteCapBudget,
|
|
40
41
|
stripOutputNotice,
|
|
41
42
|
stripRawOutputArtifactNotice,
|
|
42
43
|
} from "./output-meta";
|
|
@@ -655,6 +656,18 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
655
656
|
details.exitCode = exitCode;
|
|
656
657
|
}
|
|
657
658
|
|
|
659
|
+
// Final-defense inline cap config, shared by the timeout and normal
|
|
660
|
+
// completion paths. The sink already bounds inline bodies to the spill
|
|
661
|
+
// threshold, so with the notice slack this only fires on paths that
|
|
662
|
+
// bypass the sink (client-bridge terminals, minimizer misses). When the
|
|
663
|
+
// sink spilled, its artifact already holds the full raw stream — reuse
|
|
664
|
+
// that id instead of saving a second (already-truncated) copy, so the
|
|
665
|
+
// `[raw output: artifact://N]` footer and the truncation notice agree.
|
|
666
|
+
const inlineCap = {
|
|
667
|
+
maxBytes: resolveInlineByteCapBudget(this.session.settings),
|
|
668
|
+
saveArtifact: (full: string) => result.artifactId ?? saveBashOriginalArtifact(this.session, full),
|
|
669
|
+
};
|
|
670
|
+
|
|
658
671
|
if (isTimeout) {
|
|
659
672
|
details.timedOut = true;
|
|
660
673
|
const message =
|
|
@@ -664,9 +677,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
664
677
|
if (!normalizeResultOutput(result).startsWith(`[${message}]\n`)) {
|
|
665
678
|
outputLines.push("", `[${message}]`);
|
|
666
679
|
}
|
|
667
|
-
const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"),
|
|
668
|
-
saveArtifact: full => saveBashOriginalArtifact(this.session, full),
|
|
669
|
-
});
|
|
680
|
+
const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"), inlineCap);
|
|
670
681
|
return toolResult(details)
|
|
671
682
|
.text(timeoutOutputText)
|
|
672
683
|
.truncationFromSummary(result, { direction: "tail" })
|
|
@@ -677,12 +688,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
677
688
|
// Non-timeout cancellations and missing exit status still propagate as thrown errors.
|
|
678
689
|
this.#throwIfUnfinished(result, timeoutSec, outputText);
|
|
679
690
|
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
// ~DEFAULT_MAX_BYTES inline. No-op for already-bounded output.
|
|
683
|
-
const cappedOutputText = await enforceInlineByteCap(outputText, {
|
|
684
|
-
saveArtifact: full => saveBashOriginalArtifact(this.session, full),
|
|
685
|
-
});
|
|
691
|
+
// No-op for already-bounded output; see `inlineCap` above.
|
|
692
|
+
const cappedOutputText = await enforceInlineByteCap(outputText, inlineCap);
|
|
686
693
|
|
|
687
694
|
const resultBuilder = toolResult(details)
|
|
688
695
|
.text(cappedOutputText)
|