@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.1
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 +56 -0
- package/dist/cli.js +3305 -3302
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +9 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +9 -1
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +22 -12
- package/src/advisor/advise-tool.ts +7 -3
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/config/model-registry.ts +9 -0
- package/src/config/model-resolver.ts +43 -5
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +2 -1
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/export/html/template.js +2 -0
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/lib/xai-http.ts +28 -2
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +17 -2
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +6 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +19 -17
- package/src/modes/interactive-mode.ts +66 -11
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +8 -3
- package/src/modes/rpc/rpc-mode.ts +12 -3
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/sdk.ts +6 -2
- package/src/session/agent-session.ts +79 -34
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +2 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/eval.ts +3 -0
- package/src/tools/image-gen.ts +561 -512
- package/src/tools/index.ts +3 -1
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/xai.ts +40 -9
|
@@ -170,6 +170,7 @@ import {
|
|
|
170
170
|
} from "../advisor";
|
|
171
171
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
172
172
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
173
|
+
import { reset as resetCapabilities } from "../capability";
|
|
173
174
|
import type { Rule } from "../capability/rule";
|
|
174
175
|
import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
|
|
175
176
|
import type { ModelRegistry } from "../config/model-registry";
|
|
@@ -237,7 +238,7 @@ import type { CompactOptions, ContextUsage } from "../extensibility/extensions/t
|
|
|
237
238
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
238
239
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
239
240
|
import type { RecoveredRetryError } from "../extensibility/shared-events";
|
|
240
|
-
import type
|
|
241
|
+
import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
|
|
241
242
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
242
243
|
import { GoalRuntime } from "../goals/runtime";
|
|
243
244
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
@@ -314,7 +315,7 @@ import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
|
314
315
|
import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
315
316
|
import type { CheckpointState, CompletedRewindState } from "../tools/checkpoint";
|
|
316
317
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
317
|
-
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
318
|
+
import { isInternalUrlPath, normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
318
319
|
import {
|
|
319
320
|
buildResolveReminderMessage,
|
|
320
321
|
isPreviewResolutionToolCall,
|
|
@@ -827,6 +828,8 @@ export interface AgentSessionConfig {
|
|
|
827
828
|
skills?: Skill[];
|
|
828
829
|
/** Skill loading warnings (already captured by SDK) */
|
|
829
830
|
skillWarnings?: SkillWarning[];
|
|
831
|
+
/** Whether runtime reloads may rediscover disk-backed skills for this session. */
|
|
832
|
+
skillsReloadable?: boolean;
|
|
830
833
|
/** Custom commands (TypeScript slash commands) */
|
|
831
834
|
customCommands?: LoadedCustomCommand[];
|
|
832
835
|
skillsSettings?: SkillsSettings;
|
|
@@ -1536,15 +1539,20 @@ function queuedTextContent(message: AgentMessage): string | undefined {
|
|
|
1536
1539
|
if (!("content" in message)) return undefined;
|
|
1537
1540
|
const content = message.content;
|
|
1538
1541
|
if (typeof content === "string") return content;
|
|
1539
|
-
|
|
1542
|
+
for (const part of content) {
|
|
1543
|
+
if (part.type === "text") return part.text;
|
|
1544
|
+
}
|
|
1545
|
+
return undefined;
|
|
1540
1546
|
}
|
|
1541
1547
|
|
|
1542
1548
|
function queuedImageContent(message: AgentMessage): ImageContent[] | undefined {
|
|
1543
1549
|
if (!("content" in message) || typeof message.content === "string") return undefined;
|
|
1544
|
-
const images =
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1550
|
+
const images: ImageContent[] = [];
|
|
1551
|
+
for (const part of message.content) {
|
|
1552
|
+
if (part.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string") {
|
|
1553
|
+
images.push(part);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1548
1556
|
return images.length > 0 ? images : undefined;
|
|
1549
1557
|
}
|
|
1550
1558
|
|
|
@@ -1907,6 +1915,7 @@ export class AgentSession {
|
|
|
1907
1915
|
#mcpPromptCommands: LoadedCustomCommand[] = [];
|
|
1908
1916
|
|
|
1909
1917
|
#skillsSettings: SkillsSettings | undefined;
|
|
1918
|
+
#skillsReloadable: boolean;
|
|
1910
1919
|
|
|
1911
1920
|
// Model registry for API key resolution
|
|
1912
1921
|
#modelRegistry: ModelRegistry;
|
|
@@ -2516,6 +2525,7 @@ export class AgentSession {
|
|
|
2516
2525
|
this.#skills = config.skills ?? [];
|
|
2517
2526
|
this.#skillWarnings = config.skillWarnings ?? [];
|
|
2518
2527
|
this.#customCommands = config.customCommands ?? [];
|
|
2528
|
+
this.#skillsReloadable = config.skillsReloadable ?? true;
|
|
2519
2529
|
this.#skillsSettings = config.skillsSettings;
|
|
2520
2530
|
this.#modelRegistry = config.modelRegistry;
|
|
2521
2531
|
// Resolve the wire service-tier per request so the Fireworks Priority
|
|
@@ -3110,11 +3120,11 @@ export class AgentSession {
|
|
|
3110
3120
|
/**
|
|
3111
3121
|
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
3112
3122
|
* blocker interrupt the running agent through the steering channel; once the
|
|
3113
|
-
* loop has yielded, `triggerTurn` resumes it.
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
3116
|
-
* user interrupt auto-resume is suppressed while idle/unwinding
|
|
3117
|
-
* becomes a preserved card re-entering on resume); a live-streaming turn is
|
|
3123
|
+
* loop has yielded, `triggerTurn` resumes it. After a terminal text answer with
|
|
3124
|
+
* no queued work, a concern is preserved as a visible advisor card, while a
|
|
3125
|
+
* blocker wakes the primary to acknowledge work it handed off incorrectly.
|
|
3126
|
+
* After a deliberate user interrupt auto-resume is suppressed while idle/unwinding
|
|
3127
|
+
* (the note becomes a preserved card re-entering on resume); a live-streaming turn is
|
|
3118
3128
|
* steered in directly. A plain nit always rides the non-interrupting YieldQueue
|
|
3119
3129
|
* aside. Suppression by the per-advisor emission guard drops the note silently —
|
|
3120
3130
|
* the model still saw `Recorded.`, so it isn't tempted to rephrase the same note
|
|
@@ -3171,10 +3181,19 @@ export class AgentSession {
|
|
|
3171
3181
|
});
|
|
3172
3182
|
return;
|
|
3173
3183
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3184
|
+
// A steered interrupting note only continues the run when the session can
|
|
3185
|
+
// actually start (or is already running) a turn. Two idle cases cannot, so
|
|
3186
|
+
// `sendCustomMessage({ triggerTurn: true })` would silently bury the card in
|
|
3187
|
+
// `#pendingNextTurnMessages` until the next user prompt — strictly worse than
|
|
3188
|
+
// the visible preserved card. Preserve instead:
|
|
3189
|
+
// - Plan mode: only user-driven turns converge on ask/resolve.
|
|
3190
|
+
// - ACP bridges with `deferAgentInitiatedTurns`: the client cannot show an
|
|
3191
|
+
// agent-initiated turn as busy, so idle triggers are refused (#5628 review).
|
|
3192
|
+
const cannotAutoTrigger =
|
|
3193
|
+
!this.agent.state.isStreaming &&
|
|
3194
|
+
this.#clientBridge?.deferAgentInitiatedTurns === true &&
|
|
3195
|
+
!this.#allowAcpAgentInitiatedTurns;
|
|
3196
|
+
if (this.#planModeState?.enabled || cannotAutoTrigger) {
|
|
3178
3197
|
this.#preserveAdvisorCard({
|
|
3179
3198
|
role: "custom",
|
|
3180
3199
|
customType: "advisor",
|
|
@@ -3186,6 +3205,11 @@ export class AgentSession {
|
|
|
3186
3205
|
});
|
|
3187
3206
|
return;
|
|
3188
3207
|
}
|
|
3208
|
+
// Arm the post-interrupt immune window only now that a turn is actually
|
|
3209
|
+
// being steered/triggered. A merely preserved card never interrupts, so
|
|
3210
|
+
// arming earlier would downgrade the next `advisor.immuneTurns` worth of
|
|
3211
|
+
// real concerns/blockers to skip-idle-flush asides (#5628 review).
|
|
3212
|
+
this.#recordAdvisorInterruptDelivered();
|
|
3189
3213
|
void this.sendCustomMessage(
|
|
3190
3214
|
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
3191
3215
|
{ deliverAs: "steer", triggerTurn: true },
|
|
@@ -5585,10 +5609,9 @@ export class AgentSession {
|
|
|
5585
5609
|
|
|
5586
5610
|
// `local://` URLs (e.g. local://PLAN.md for plan-mode) resolve to a real
|
|
5587
5611
|
// on-disk artifacts path; pre-caching works as long as we ask the
|
|
5588
|
-
// local-protocol handler. Other internal-scheme URLs
|
|
5589
|
-
//
|
|
5590
|
-
//
|
|
5591
|
-
// them through its normal dispatch path.
|
|
5612
|
+
// local-protocol handler. Other internal-scheme URLs have no local
|
|
5613
|
+
// filesystem representation; skip pre-cache entirely for those — the
|
|
5614
|
+
// edit tool itself will reject them through its normal dispatch path.
|
|
5592
5615
|
const resolvedPath = this.#resolveSessionFsPath(path);
|
|
5593
5616
|
if (resolvedPath === undefined) return undefined;
|
|
5594
5617
|
|
|
@@ -5688,9 +5711,8 @@ export class AgentSession {
|
|
|
5688
5711
|
* - `local://` URLs route through the local-protocol handler so they map
|
|
5689
5712
|
* onto the session's on-disk artifacts directory; pre-caching, ENOENT
|
|
5690
5713
|
* handling, and post-edit invalidation all work normally.
|
|
5691
|
-
* - Other internal-scheme URLs
|
|
5692
|
-
*
|
|
5693
|
-
* so callers skip filesystem-only operations.
|
|
5714
|
+
* - Other internal-scheme URLs have no local filesystem path; this returns
|
|
5715
|
+
* `undefined` so callers skip filesystem-only operations.
|
|
5694
5716
|
* - Cwd-relative and absolute paths resolve via `resolveToCwd`.
|
|
5695
5717
|
*/
|
|
5696
5718
|
#resolveSessionFsPath(filePath: string): string | undefined {
|
|
@@ -5698,15 +5720,7 @@ export class AgentSession {
|
|
|
5698
5720
|
if (normalized.startsWith("local:")) {
|
|
5699
5721
|
return resolveLocalUrlToPath(normalized, this.#localProtocolOptions());
|
|
5700
5722
|
}
|
|
5701
|
-
if (
|
|
5702
|
-
normalized.startsWith("agent://") ||
|
|
5703
|
-
normalized.startsWith("skill://") ||
|
|
5704
|
-
normalized.startsWith("rule://") ||
|
|
5705
|
-
normalized.startsWith("mcp://") ||
|
|
5706
|
-
normalized.startsWith("artifact://")
|
|
5707
|
-
) {
|
|
5708
|
-
return undefined;
|
|
5709
|
-
}
|
|
5723
|
+
if (isInternalUrlPath(normalized)) return undefined;
|
|
5710
5724
|
return resolveToCwd(normalized, this.sessionManager.getCwd());
|
|
5711
5725
|
}
|
|
5712
5726
|
|
|
@@ -6817,6 +6831,34 @@ export class AgentSession {
|
|
|
6817
6831
|
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
6818
6832
|
}
|
|
6819
6833
|
|
|
6834
|
+
/**
|
|
6835
|
+
* Rediscover disk-backed skills and rebuild prompt-facing state without
|
|
6836
|
+
* recreating the session. Explicit skill snapshots (`--no-skills`,
|
|
6837
|
+
* SDK-provided `skills`) remain fixed for the lifetime of the session.
|
|
6838
|
+
*/
|
|
6839
|
+
async refreshSkills(): Promise<void> {
|
|
6840
|
+
if (!this.#skillsReloadable) {
|
|
6841
|
+
return;
|
|
6842
|
+
}
|
|
6843
|
+
|
|
6844
|
+
resetCapabilities();
|
|
6845
|
+
const skillsSettings = this.settings.getGroup("skills");
|
|
6846
|
+
const discovered = await loadSkills({
|
|
6847
|
+
...skillsSettings,
|
|
6848
|
+
cwd: this.sessionManager.getCwd(),
|
|
6849
|
+
disabledExtensions: this.settings.get("disabledExtensions") ?? [],
|
|
6850
|
+
});
|
|
6851
|
+
this.#skills = discovered.skills;
|
|
6852
|
+
this.#skillWarnings = discovered.warnings;
|
|
6853
|
+
this.#skillsSettings = skillsSettings;
|
|
6854
|
+
|
|
6855
|
+
if (this.#agentKind === "main") {
|
|
6856
|
+
setActiveSkills(this.#skills);
|
|
6857
|
+
}
|
|
6858
|
+
await this.refreshBaseSystemPrompt();
|
|
6859
|
+
this.#notifyCommandMetadataChanged();
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6820
6862
|
/**
|
|
6821
6863
|
* Set active tools by name.
|
|
6822
6864
|
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
@@ -8264,8 +8306,11 @@ export class AgentSession {
|
|
|
8264
8306
|
},
|
|
8265
8307
|
hasPendingMessages: () => this.queuedMessageCount > 0,
|
|
8266
8308
|
shutdown: () => {
|
|
8267
|
-
|
|
8268
|
-
|
|
8309
|
+
// Await the idempotent dispose() before exiting so the browser
|
|
8310
|
+
// reaper and other bounded teardown complete — a fire-and-forget
|
|
8311
|
+
// `void this.dispose()` raced process.exit() and could leave an
|
|
8312
|
+
// OMP-owned Chromium alive (#5643).
|
|
8313
|
+
void this.dispose().finally(() => process.exit(0));
|
|
8269
8314
|
},
|
|
8270
8315
|
getContextUsage: () => this.getContextUsage(),
|
|
8271
8316
|
waitForIdle: () => this.waitForIdle(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { Message
|
|
3
|
+
import type { Message } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { getAgentDir as getDefaultAgentDir, logger, parseJsonlLenient, toError } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { computeDefaultSessionDir } from "./session-paths";
|
|
6
6
|
import { FileSessionStorage, type SessionStorage } from "./session-storage";
|
|
@@ -108,10 +108,11 @@ function sessionDisplayName(info: SessionInfo): string {
|
|
|
108
108
|
|
|
109
109
|
function extractTextFromContent(content: Message["content"]): string {
|
|
110
110
|
if (typeof content === "string") return content;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
const text: string[] = [];
|
|
112
|
+
for (const block of content) {
|
|
113
|
+
if (block.type === "text") text.push(block.text);
|
|
114
|
+
}
|
|
115
|
+
return text.join(" ");
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
@@ -252,6 +252,15 @@ async function resolvePersistedBlobRefs(value: unknown, blobStore: BlobStore, ke
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
if (typeof value !== "object" || value === null) return;
|
|
255
|
+
if (
|
|
256
|
+
"type" in value &&
|
|
257
|
+
value.type === "image_generation_call" &&
|
|
258
|
+
"result" in value &&
|
|
259
|
+
typeof value.result === "string" &&
|
|
260
|
+
isBlobRef(value.result)
|
|
261
|
+
) {
|
|
262
|
+
value.result = await resolveImageData(blobStore, value.result);
|
|
263
|
+
}
|
|
255
264
|
|
|
256
265
|
if (hasImageUrl(value) && isBlobRef(value.image_url)) {
|
|
257
266
|
value.image_url = await resolveImageDataUrl(blobStore, value.image_url);
|
|
@@ -79,6 +79,17 @@ function isNonEmptyString(value: unknown): value is string {
|
|
|
79
79
|
*/
|
|
80
80
|
function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string): unknown {
|
|
81
81
|
if (obj === null || obj === undefined) return obj;
|
|
82
|
+
if (
|
|
83
|
+
typeof obj === "object" &&
|
|
84
|
+
"type" in obj &&
|
|
85
|
+
obj.type === "image_generation_call" &&
|
|
86
|
+
"result" in obj &&
|
|
87
|
+
typeof obj.result === "string" &&
|
|
88
|
+
!isBlobRef(obj.result) &&
|
|
89
|
+
obj.result.length >= BLOB_EXTERNALIZE_THRESHOLD
|
|
90
|
+
) {
|
|
91
|
+
return { ...obj, result: externalizeImageDataSync(blobStore, obj.result) };
|
|
92
|
+
}
|
|
82
93
|
if (shouldExternalizeImagePayload(obj, key)) {
|
|
83
94
|
return { ...obj, data: externalizeImageDataSync(blobStore, obj.data, obj.mimeType) };
|
|
84
95
|
}
|
|
@@ -43,6 +43,8 @@ export interface OutputSummary {
|
|
|
43
43
|
columnDroppedBytes?: number;
|
|
44
44
|
/** Number of distinct lines that hit the per-line column cap. */
|
|
45
45
|
columnTruncatedLines?: number;
|
|
46
|
+
/** Configured per-line column cap in effect (chars), when > 0. */
|
|
47
|
+
columnMax?: number;
|
|
46
48
|
/** Artifact ID for internal URL access (artifact://<id>) when truncated */
|
|
47
49
|
artifactId?: string;
|
|
48
50
|
}
|
|
@@ -836,7 +838,6 @@ export class OutputSink {
|
|
|
836
838
|
const capped = this.#maxColumns > 0 ? this.#applyColumnCap(chunk) : chunk;
|
|
837
839
|
const cappedBytes = capped === chunk ? rawBytes : Buffer.byteLength(capped, "utf-8");
|
|
838
840
|
const cappedThisChunk = cappedBytes < rawBytes;
|
|
839
|
-
if (cappedThisChunk) this.#truncated = true;
|
|
840
841
|
|
|
841
842
|
// Mirror RAW chunk to the artifact file so the on-disk record is the full
|
|
842
843
|
// uncapped stream. Mirror triggers on: in-memory overflow OR this chunk's
|
|
@@ -1276,6 +1277,7 @@ export class OutputSink {
|
|
|
1276
1277
|
elidedLines,
|
|
1277
1278
|
columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
|
|
1278
1279
|
columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
|
|
1280
|
+
columnMax: this.#columnTruncatedLines > 0 ? this.#maxColumns : undefined,
|
|
1279
1281
|
artifactId: this.#file?.artifactId,
|
|
1280
1282
|
};
|
|
1281
1283
|
}
|
|
@@ -2254,6 +2254,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
2254
2254
|
// listClaudePluginRoots re-reads from disk on next access.
|
|
2255
2255
|
const projectPath = await resolveActiveProjectRegistryPath(runtime.ctx.sessionManager.getCwd());
|
|
2256
2256
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
2257
|
+
await runtime.ctx.refreshSkillState();
|
|
2257
2258
|
await runtime.ctx.refreshSlashCommandState();
|
|
2258
2259
|
resetCapabilities();
|
|
2259
2260
|
runtime.ctx.showStatus("Plugins reloaded.");
|
|
@@ -2601,6 +2602,7 @@ export async function executeBuiltinSlashCommand(
|
|
|
2601
2602
|
reloadPlugins: async () => {
|
|
2602
2603
|
const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
|
|
2603
2604
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
2605
|
+
await ctx.refreshSkillState();
|
|
2604
2606
|
await ctx.refreshSlashCommandState();
|
|
2605
2607
|
resetCapabilities();
|
|
2606
2608
|
},
|
|
@@ -64,10 +64,10 @@ export interface SlashCommandRuntime {
|
|
|
64
64
|
/** Re-advertise the available command list (no-op outside ACP). */
|
|
65
65
|
refreshCommands: () => Promise<void> | void;
|
|
66
66
|
/**
|
|
67
|
-
* Reload plugin state (caches, slash command registry, project
|
|
68
|
-
* and re-emit available commands. Used by `/reload-plugins`,
|
|
69
|
-
* `/marketplace`/`/plugins` mutations so the session sees a
|
|
70
|
-
* after plugin or project-scope changes.
|
|
67
|
+
* Reload plugin state (caches, skills, slash command registry, project
|
|
68
|
+
* registries) and re-emit available commands. Used by `/reload-plugins`,
|
|
69
|
+
* `/move`, and `/marketplace`/`/plugins` mutations so the session sees a
|
|
70
|
+
* consistent view after plugin or project-scope changes.
|
|
71
71
|
*/
|
|
72
72
|
reloadPlugins: () => Promise<void>;
|
|
73
73
|
notifyTitleChanged?: () => Promise<void> | void;
|
package/src/system-prompt.ts
CHANGED
|
@@ -470,7 +470,7 @@ export interface BuildSystemPromptOptions {
|
|
|
470
470
|
/** Pre-loaded context files (skips discovery if provided). */
|
|
471
471
|
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
|
|
472
472
|
/** Skills provided directly to system prompt construction. */
|
|
473
|
-
skills?: Skill[];
|
|
473
|
+
skills?: readonly Skill[];
|
|
474
474
|
/** Pre-loaded rulebook rules (descriptions, excluding TTSR and always-apply). */
|
|
475
475
|
rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
|
|
476
476
|
/** Intent field name injected into every tool schema. If set, explains the field in the prompt. */
|
|
@@ -637,7 +637,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
637
637
|
totalLines: 0,
|
|
638
638
|
agentsMdFiles: [],
|
|
639
639
|
});
|
|
640
|
-
const skillsPromise: Promise<Skill[]> =
|
|
640
|
+
const skillsPromise: Promise<readonly Skill[]> =
|
|
641
641
|
providedSkills !== undefined
|
|
642
642
|
? Promise.resolve(providedSkills)
|
|
643
643
|
: skillsSettings?.enabled !== false
|
|
@@ -47,6 +47,13 @@ export interface CmuxBrowserHandle extends BrowserHandleCommon {
|
|
|
47
47
|
|
|
48
48
|
export type BrowserHandle = PuppeteerBrowserHandle | CmuxBrowserHandle;
|
|
49
49
|
|
|
50
|
+
/** Controls bounded browser-handle teardown and identifies the owning resource in timeout diagnostics. */
|
|
51
|
+
export interface ReleaseBrowserOptions {
|
|
52
|
+
kill: boolean;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
resource?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
const browsers = new Map<string, BrowserHandle>();
|
|
51
58
|
|
|
52
59
|
function browserKey(kind: BrowserKind): string {
|
|
@@ -221,7 +228,7 @@ export function holdBrowser(handle: BrowserHandle): void {
|
|
|
221
228
|
handle.refCount++;
|
|
222
229
|
}
|
|
223
230
|
|
|
224
|
-
export async function releaseBrowser(handle: BrowserHandle, opts:
|
|
231
|
+
export async function releaseBrowser(handle: BrowserHandle, opts: ReleaseBrowserOptions): Promise<void> {
|
|
225
232
|
handle.refCount = Math.max(0, handle.refCount - 1);
|
|
226
233
|
if (handle.refCount === 0) {
|
|
227
234
|
// Only evict if the registry still points at THIS handle. After a disconnect,
|
|
@@ -232,7 +239,7 @@ export async function releaseBrowser(handle: BrowserHandle, opts: { kill: boolea
|
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
235
|
-
async function disposeBrowserHandle(handle: BrowserHandle, opts:
|
|
242
|
+
async function disposeBrowserHandle(handle: BrowserHandle, opts: ReleaseBrowserOptions): Promise<void> {
|
|
236
243
|
if ("client" in handle) {
|
|
237
244
|
handle.client.close();
|
|
238
245
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPuppeteerDir, logger, postmortem, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { getPuppeteerDir, logger, postmortem, Snowflake, withTimeout, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Page, Target } from "puppeteer-core";
|
|
3
3
|
import { callSessionTool } from "../../eval/js/tool-bridge";
|
|
4
4
|
import { webpExclusionForModel } from "../../utils/image-loading";
|
|
@@ -123,6 +123,8 @@ export interface RunInTabOptions {
|
|
|
123
123
|
|
|
124
124
|
export interface ReleaseTabOptions {
|
|
125
125
|
kill?: boolean;
|
|
126
|
+
/** Maximum time for each asynchronous cleanup resource before close fails with diagnostics. */
|
|
127
|
+
timeoutMs?: number;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
const tabs = new Map<string, TabSession>();
|
|
@@ -135,6 +137,22 @@ const GRACE_MS = 750;
|
|
|
135
137
|
// mapped to the kill reason. Lets the next `run` on that name explain WHY the tab
|
|
136
138
|
// vanished instead of a bare "not alive". Cleared when the name is opened again.
|
|
137
139
|
const killedTabs = new Map<string, string>();
|
|
140
|
+
const DEFAULT_TAB_CLOSE_TIMEOUT_MS = 5_000;
|
|
141
|
+
|
|
142
|
+
async function waitForTabCleanup<T>(
|
|
143
|
+
tab: TabSession,
|
|
144
|
+
timeoutMs: number,
|
|
145
|
+
pendingResource: string,
|
|
146
|
+
promise: Promise<T>,
|
|
147
|
+
): Promise<T> {
|
|
148
|
+
const message = `Timed out after ${timeoutMs}ms closing ${tab.kindTag} browser tab ${JSON.stringify(tab.name)}; pending resource: ${pendingResource}`;
|
|
149
|
+
try {
|
|
150
|
+
return await withTimeout(promise, timeoutMs, message);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof Error && error.message === message) throw new ToolError(message);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
138
156
|
|
|
139
157
|
export function getTab(name: string): TabSession | undefined {
|
|
140
158
|
return tabs.get(name);
|
|
@@ -519,26 +537,42 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
519
537
|
pending.reject(closeError);
|
|
520
538
|
}
|
|
521
539
|
tab.pending.clear();
|
|
540
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TAB_CLOSE_TIMEOUT_MS;
|
|
522
541
|
if (tab.backend === "cmux") {
|
|
523
|
-
let
|
|
542
|
+
let closeError: unknown;
|
|
524
543
|
if (wasAlive && tab.cmuxOwnsSurface) {
|
|
525
544
|
try {
|
|
526
|
-
await
|
|
545
|
+
await waitForTabCleanup(
|
|
546
|
+
tab,
|
|
547
|
+
timeoutMs,
|
|
548
|
+
`cmux surface ${JSON.stringify(tab.targetId)} (surface.close)`,
|
|
549
|
+
tab.browser.client.request("surface.close", { surface_id: tab.targetId }, { timeoutMs }),
|
|
550
|
+
);
|
|
527
551
|
} catch (err) {
|
|
528
552
|
if (isLastSurfaceCloseError(err)) {
|
|
529
553
|
logger.debug("Leaving cmux browser surface open because it is the last surface in the workspace", {
|
|
530
554
|
error: err instanceof Error ? err.message : String(err),
|
|
531
555
|
});
|
|
532
556
|
} else {
|
|
533
|
-
|
|
557
|
+
closeError = err;
|
|
534
558
|
}
|
|
535
559
|
}
|
|
536
560
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
561
|
+
try {
|
|
562
|
+
await releaseBrowser(tab.browser, {
|
|
563
|
+
kill: opts.kill ?? false,
|
|
564
|
+
timeoutMs,
|
|
565
|
+
resource: `tab ${JSON.stringify(name)}`,
|
|
566
|
+
});
|
|
567
|
+
} catch (error) {
|
|
568
|
+
closeError ??= error;
|
|
569
|
+
} finally {
|
|
570
|
+
tabs.delete(name);
|
|
571
|
+
}
|
|
572
|
+
if (closeError) throw closeError;
|
|
540
573
|
return true;
|
|
541
574
|
}
|
|
575
|
+
let cleanupError: unknown;
|
|
542
576
|
let forced = false;
|
|
543
577
|
if (wasAlive) {
|
|
544
578
|
try {
|
|
@@ -549,9 +583,30 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
549
583
|
}
|
|
550
584
|
}
|
|
551
585
|
await tab.worker.terminate().catch(() => undefined);
|
|
552
|
-
if (forced && tab.kindTag === "headless")
|
|
553
|
-
|
|
554
|
-
|
|
586
|
+
if (forced && tab.kindTag === "headless") {
|
|
587
|
+
try {
|
|
588
|
+
await waitForTabCleanup(
|
|
589
|
+
tab,
|
|
590
|
+
timeoutMs,
|
|
591
|
+
`orphan CDP target ${JSON.stringify(tab.targetId)} (Page.close)`,
|
|
592
|
+
closeOrphanTarget(tab),
|
|
593
|
+
);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
cleanupError = error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
await releaseBrowser(tab.browser, {
|
|
600
|
+
kill: opts.kill ?? false,
|
|
601
|
+
timeoutMs,
|
|
602
|
+
resource: `tab ${JSON.stringify(name)}`,
|
|
603
|
+
});
|
|
604
|
+
} catch (error) {
|
|
605
|
+
cleanupError ??= error;
|
|
606
|
+
} finally {
|
|
607
|
+
tabs.delete(name);
|
|
608
|
+
}
|
|
609
|
+
if (cleanupError) throw cleanupError;
|
|
555
610
|
return true;
|
|
556
611
|
}
|
|
557
612
|
|
package/src/tools/browser.ts
CHANGED
|
@@ -199,7 +199,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
199
199
|
case "open":
|
|
200
200
|
return await this.#open(name, params, details, timeoutMs, signal);
|
|
201
201
|
case "close":
|
|
202
|
-
return await this.#close(name, params, details, signal);
|
|
202
|
+
return await this.#close(name, params, details, timeoutMs, signal);
|
|
203
203
|
case "run":
|
|
204
204
|
return await this.#run(name, params, details, timeoutMs, signal);
|
|
205
205
|
default:
|
|
@@ -284,15 +284,16 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
284
284
|
name: string,
|
|
285
285
|
params: BrowserParams,
|
|
286
286
|
details: BrowserToolDetails,
|
|
287
|
+
timeoutMs: number,
|
|
287
288
|
signal?: AbortSignal,
|
|
288
289
|
): Promise<AgentToolResult<BrowserToolDetails>> {
|
|
289
290
|
const kill = !!params.kill;
|
|
290
291
|
if (params.all) {
|
|
291
|
-
const count = await untilAborted(signal, () => releaseAllTabs({ kill }));
|
|
292
|
+
const count = await untilAborted(signal, () => releaseAllTabs({ kill, timeoutMs }));
|
|
292
293
|
details.result = `Closed ${count} tab(s)`;
|
|
293
294
|
return toolResult(details).text(details.result).done();
|
|
294
295
|
}
|
|
295
|
-
const closed = await untilAborted(signal, () => releaseTab(name, { kill }));
|
|
296
|
+
const closed = await untilAborted(signal, () => releaseTab(name, { kill, timeoutMs }));
|
|
296
297
|
details.result = closed ? `Closed tab ${JSON.stringify(name)}` : `No tab named ${JSON.stringify(name)}`;
|
|
297
298
|
return toolResult(details).text(details.result).done();
|
|
298
299
|
}
|
package/src/tools/eval.ts
CHANGED
|
@@ -768,5 +768,8 @@ async function summarizeFinal(
|
|
|
768
768
|
outputLines,
|
|
769
769
|
outputBytes,
|
|
770
770
|
artifactId: rawSummary.artifactId,
|
|
771
|
+
columnDroppedBytes: rawSummary.columnDroppedBytes,
|
|
772
|
+
columnTruncatedLines: rawSummary.columnTruncatedLines,
|
|
773
|
+
columnMax: rawSummary.columnMax,
|
|
771
774
|
};
|
|
772
775
|
}
|