@oh-my-pi/pi-coding-agent 16.3.4 → 16.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/cli.js +3522 -3485
- package/dist/types/edit/file-snapshot-store.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +0 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/shared-events.d.ts +8 -1
- package/dist/types/hindsight/content.d.ts +4 -0
- package/dist/types/internal-urls/artifact-protocol.d.ts +8 -0
- package/dist/types/internal-urls/types.d.ts +10 -0
- package/dist/types/lsp/config.d.ts +4 -0
- package/dist/types/lsp/edits.d.ts +2 -0
- package/dist/types/mcp/oauth-discovery.d.ts +30 -0
- package/dist/types/modes/components/assistant-message.d.ts +15 -10
- package/dist/types/modes/components/bash-execution.d.ts +6 -0
- package/dist/types/modes/components/eval-execution.d.ts +6 -0
- package/dist/types/modes/components/login-dialog.d.ts +10 -2
- package/dist/types/modes/components/tool-execution.d.ts +3 -13
- package/dist/types/modes/components/transcript-container.d.ts +26 -29
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +20 -3
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -3
- package/dist/types/modes/rpc/rpc-types.d.ts +6 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +15 -6
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/subprocess/worker-runtime.d.ts +11 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +0 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -2
- package/dist/types/tools/renderers.d.ts +0 -20
- package/dist/types/tools/ssh.d.ts +0 -2
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/git.d.ts +16 -0
- package/package.json +12 -12
- package/src/cli/auth-broker-cli.ts +13 -2
- package/src/cli/tiny-models-cli.ts +12 -5
- package/src/edit/file-snapshot-store.ts +5 -2
- package/src/edit/renderer.ts +0 -5
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/shared-events.ts +9 -1
- package/src/hindsight/content.ts +31 -0
- package/src/internal-urls/artifact-protocol.ts +97 -53
- package/src/internal-urls/types.ts +10 -0
- package/src/lsp/config.ts +15 -0
- package/src/lsp/edits.ts +28 -7
- package/src/lsp/index.ts +46 -4
- package/src/mcp/oauth-discovery.ts +88 -18
- package/src/mnemopi/state.ts +26 -2
- package/src/modes/components/assistant-message.ts +134 -82
- package/src/modes/components/bash-execution.ts +9 -0
- package/src/modes/components/chat-transcript-builder.ts +8 -4
- package/src/modes/components/eval-execution.ts +9 -0
- package/src/modes/components/login-dialog.ts +16 -2
- package/src/modes/components/mcp-add-wizard.ts +9 -1
- package/src/modes/components/tool-execution.ts +4 -50
- package/src/modes/components/transcript-container.ts +82 -458
- package/src/modes/components/tree-selector.ts +9 -3
- package/src/modes/controllers/command-controller.ts +0 -3
- package/src/modes/controllers/event-controller.ts +74 -14
- package/src/modes/controllers/extension-ui-controller.ts +0 -1
- package/src/modes/controllers/input-controller.ts +4 -10
- package/src/modes/controllers/mcp-command-controller.ts +106 -29
- package/src/modes/controllers/selector-controller.ts +9 -1
- package/src/modes/interactive-mode.ts +12 -8
- package/src/modes/rpc/rpc-client.ts +8 -4
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/modes/rpc/rpc-types.ts +13 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +18 -0
- package/src/modes/utils/transcript-render-helpers.ts +40 -13
- package/src/modes/utils/ui-helpers.ts +12 -7
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +148 -1
- package/src/session/session-context.ts +7 -0
- package/src/subprocess/worker-runtime.ts +219 -2
- package/src/task/worktree.ts +28 -6
- package/src/tiny/worker.ts +14 -4
- package/src/tools/bash-skill-urls.ts +3 -3
- package/src/tools/bash.ts +0 -4
- package/src/tools/browser/tab-supervisor.ts +47 -3
- package/src/tools/eval-render.ts +0 -20
- package/src/tools/grep.ts +19 -2
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/read.ts +261 -21
- package/src/tools/renderers.ts +0 -20
- package/src/tools/ssh.ts +0 -16
- package/src/tools/write.ts +13 -6
- package/src/utils/git.ts +20 -0
- package/src/utils/open.ts +51 -6
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { formatBytes, formatDuration } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type CustomMessage,
|
|
12
|
+
type FileMentionMessage,
|
|
13
|
+
resolveAbortLabel,
|
|
14
|
+
shouldRenderAbortReason,
|
|
15
|
+
} from "../../session/messages";
|
|
11
16
|
import { createIrcMessageCard } from "../../tools/irc";
|
|
17
|
+
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
12
18
|
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
13
19
|
import { TranscriptBlock } from "../components/transcript-container";
|
|
14
20
|
import { theme } from "../theme/theme";
|
|
@@ -138,20 +144,41 @@ export function normalizeToolArgs(args: unknown): Record<string, unknown> {
|
|
|
138
144
|
return args && typeof args === "object" && !Array.isArray(args) ? (args as Record<string, unknown>) : {};
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
export type AssistantErrorPresentation =
|
|
148
|
+
| { kind: "none" }
|
|
149
|
+
| { kind: "full"; text: string; isError: true }
|
|
150
|
+
| { kind: "compact-recovered"; text: string; isError: false };
|
|
151
|
+
|
|
152
|
+
function sanitizeRecoveredRetryNote(note: string): string {
|
|
153
|
+
const normalized = replaceTabs(note).replace(/\s+/g, " ").trim();
|
|
154
|
+
return truncateToWidth(normalized || "retried", TRUNCATE_LENGTHS.CONTENT);
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
/**
|
|
142
|
-
* Resolve the
|
|
143
|
-
* Silent aborts yield no label.
|
|
158
|
+
* Resolve the turn-ending assistant error presentation, if any.
|
|
159
|
+
* Silent and user-interrupt aborts yield no label. Recovered auto-retry errors
|
|
160
|
+
* collapse to a single non-error note; terminal errors keep the full red presentation.
|
|
144
161
|
*/
|
|
145
|
-
export function
|
|
162
|
+
export function resolveAssistantErrorPresentation(
|
|
146
163
|
message: AssistantAgentMessage,
|
|
147
164
|
retryAttempt = 0,
|
|
148
|
-
):
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
): AssistantErrorPresentation {
|
|
166
|
+
if (message.retryRecovery?.status === "recovered") {
|
|
167
|
+
return {
|
|
168
|
+
kind: "compact-recovered",
|
|
169
|
+
text: sanitizeRecoveredRetryNote(message.retryRecovery.note),
|
|
170
|
+
isError: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (message.stopReason === "aborted") {
|
|
174
|
+
if (!shouldRenderAbortReason(message)) return { kind: "none" };
|
|
175
|
+
return { kind: "full", text: resolveAbortLabel(message, retryAttempt), isError: true };
|
|
176
|
+
}
|
|
177
|
+
if (message.stopReason === "error") {
|
|
178
|
+
return { kind: "full", text: message.errorMessage || "Error", isError: true };
|
|
179
|
+
}
|
|
180
|
+
if (message.errorMessage && shouldRenderAbortReason(message)) {
|
|
181
|
+
return { kind: "full", text: message.errorMessage, isError: true };
|
|
182
|
+
}
|
|
183
|
+
return { kind: "none" };
|
|
157
184
|
}
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
buildFileMentionBlock,
|
|
55
55
|
buildIrcMessageCard,
|
|
56
56
|
normalizeToolArgs,
|
|
57
|
-
|
|
57
|
+
resolveAssistantErrorPresentation,
|
|
58
58
|
} from "./transcript-render-helpers";
|
|
59
59
|
|
|
60
60
|
type TextBlock = { type: "text"; text: string };
|
|
@@ -317,7 +317,11 @@ export class UiHelpers {
|
|
|
317
317
|
const previous = waitingPoll;
|
|
318
318
|
if (!previous) return;
|
|
319
319
|
waitingPoll = null;
|
|
320
|
-
if (
|
|
320
|
+
if (
|
|
321
|
+
nextToolName === "job" &&
|
|
322
|
+
previous.isDisplaceableBlock() &&
|
|
323
|
+
this.ctx.chatContainer.isBlockUncommitted(previous)
|
|
324
|
+
) {
|
|
321
325
|
this.ctx.chatContainer.removeChild(previous);
|
|
322
326
|
}
|
|
323
327
|
// Sealing freezes the block and stops the waiting-poll spinner that
|
|
@@ -334,7 +338,9 @@ export class UiHelpers {
|
|
|
334
338
|
}
|
|
335
339
|
if (previous.canBeDisplacedBy(nextToolName)) {
|
|
336
340
|
todoSnapshot = null;
|
|
337
|
-
this.ctx.chatContainer.
|
|
341
|
+
if (this.ctx.chatContainer.isBlockUncommitted(previous)) {
|
|
342
|
+
this.ctx.chatContainer.removeChild(previous);
|
|
343
|
+
}
|
|
338
344
|
previous.seal();
|
|
339
345
|
return;
|
|
340
346
|
}
|
|
@@ -372,10 +378,9 @@ export class UiHelpers {
|
|
|
372
378
|
readGroup?.seal();
|
|
373
379
|
readGroup = null;
|
|
374
380
|
}
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
);
|
|
381
|
+
const errorPresentation = resolveAssistantErrorPresentation(message, this.ctx.viewSession.retryAttempt);
|
|
382
|
+
const hasErrorStop = errorPresentation.kind === "full";
|
|
383
|
+
const errorMessage = hasErrorStop ? errorPresentation.text : null;
|
|
379
384
|
|
|
380
385
|
// Render tool call components
|
|
381
386
|
for (const content of message.content) {
|
|
@@ -67,7 +67,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
67
67
|
|
|
68
68
|
# Internal URIs
|
|
69
69
|
|
|
70
|
-
All URI schemes take the same line selectors. `artifact://<id>` recovers
|
|
70
|
+
All URI schemes take the same line selectors. `artifact://<id>` recovers spilled output; large artifacts block unbounded `:raw`, so page with `artifact://<id>:N-M` / `artifact://<id>:raw:N-M` and use the reported artifact file path for search/copy workflows.
|
|
71
71
|
|
|
72
72
|
`ssh://host/<absolute-path>` reads a remote text file (UTF-8, ≤1 MiB) or lists a directory one level deep, on a pre-configured SSH host or `~/.ssh/config` alias; `ssh://host/` lists the remote root and bare `ssh://` lists the configured hosts. Files are also writable via `write` and searchable via `search`; a directory only lists (`search` refuses a directory, `write` refuses to overwrite one). A literal `:`, `?`, or `#` in the remote path must be percent-encoded (`%3A`/`%3F`/`%23`) — a trailing `:sel` is read as a line selector, and `?`/`#` start a URL query/fragment. Requires a POSIX login shell (`sh`/`bash`/`zsh`); a Windows host or a non-POSIX shell (fish, csh/tcsh) is rejected — use the `ssh` tool there.
|
|
73
73
|
|
package/src/sdk.ts
CHANGED
|
@@ -81,6 +81,8 @@ import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/to
|
|
|
81
81
|
import type {
|
|
82
82
|
AssistantMessage,
|
|
83
83
|
AssistantMessageEvent,
|
|
84
|
+
AssistantRetryRecovery,
|
|
85
|
+
AssistantRetryRecoveryKind,
|
|
84
86
|
Context,
|
|
85
87
|
ImageContent,
|
|
86
88
|
Message,
|
|
@@ -219,6 +221,7 @@ import { createExtensionModelQuery } from "../extensibility/extensions/model-api
|
|
|
219
221
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
220
222
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
221
223
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
224
|
+
import type { RecoveredRetryError } from "../extensibility/shared-events";
|
|
222
225
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
223
226
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
224
227
|
import { GoalRuntime } from "../goals/runtime";
|
|
@@ -497,7 +500,13 @@ export type AgentSessionEvent =
|
|
|
497
500
|
errorMessage: string;
|
|
498
501
|
errorId?: number;
|
|
499
502
|
}
|
|
500
|
-
| {
|
|
503
|
+
| {
|
|
504
|
+
type: "auto_retry_end";
|
|
505
|
+
success: boolean;
|
|
506
|
+
attempt: number;
|
|
507
|
+
finalError?: string;
|
|
508
|
+
recoveredErrors?: RecoveredRetryError[];
|
|
509
|
+
}
|
|
501
510
|
| { type: "retry_fallback_applied"; from: string; to: string; role: string }
|
|
502
511
|
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
503
512
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
@@ -1425,6 +1434,13 @@ type MessageEndPersistenceSlot = {
|
|
|
1425
1434
|
persist: (persistMessage: () => void) => Promise<void>;
|
|
1426
1435
|
release: () => void;
|
|
1427
1436
|
};
|
|
1437
|
+
type PendingRecoveredRetryError = {
|
|
1438
|
+
entryId: string;
|
|
1439
|
+
persistenceKey: string;
|
|
1440
|
+
recovery: AssistantRetryRecoveryKind;
|
|
1441
|
+
attempt: number;
|
|
1442
|
+
note: string;
|
|
1443
|
+
};
|
|
1428
1444
|
|
|
1429
1445
|
type PostPromptSkipReason = "aborted" | "stale-generation";
|
|
1430
1446
|
|
|
@@ -1581,6 +1597,7 @@ export class AgentSession {
|
|
|
1581
1597
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
1582
1598
|
#retryResolve: (() => void) | undefined = undefined;
|
|
1583
1599
|
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
1600
|
+
#pendingRecoveredRetryErrors: PendingRecoveredRetryError[] = [];
|
|
1584
1601
|
// Todo completion reminder state
|
|
1585
1602
|
#todoReminderCount = 0;
|
|
1586
1603
|
/**
|
|
@@ -3663,11 +3680,14 @@ export class AgentSession {
|
|
|
3663
3680
|
role: this.#activeRetryFallback.role,
|
|
3664
3681
|
});
|
|
3665
3682
|
}
|
|
3683
|
+
const recoveredErrors = await this.#markPendingRecoveredRetryErrors(assistantMsg);
|
|
3666
3684
|
await this.#emitSessionEvent({
|
|
3667
3685
|
type: "auto_retry_end",
|
|
3668
3686
|
success: true,
|
|
3669
3687
|
attempt: this.#retryAttempt,
|
|
3688
|
+
recoveredErrors,
|
|
3670
3689
|
});
|
|
3690
|
+
this.#clearPendingRecoveredRetryErrors();
|
|
3671
3691
|
this.#retryAttempt = 0;
|
|
3672
3692
|
}
|
|
3673
3693
|
if (assistantMsg.provider === "opencode-go") {
|
|
@@ -5337,6 +5357,7 @@ export class AgentSession {
|
|
|
5337
5357
|
success: event.success,
|
|
5338
5358
|
attempt: event.attempt,
|
|
5339
5359
|
finalError: event.finalError,
|
|
5360
|
+
recoveredErrors: event.recoveredErrors,
|
|
5340
5361
|
});
|
|
5341
5362
|
} else if (event.type === "ttsr_triggered") {
|
|
5342
5363
|
await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules });
|
|
@@ -10386,6 +10407,126 @@ export class AgentSession {
|
|
|
10386
10407
|
return undefined;
|
|
10387
10408
|
}
|
|
10388
10409
|
|
|
10410
|
+
#clearPendingRecoveredRetryErrors(): void {
|
|
10411
|
+
this.#pendingRecoveredRetryErrors = [];
|
|
10412
|
+
}
|
|
10413
|
+
|
|
10414
|
+
#retryRecoveryKind(
|
|
10415
|
+
id: number,
|
|
10416
|
+
switchedCredential: boolean,
|
|
10417
|
+
switchedModel: boolean,
|
|
10418
|
+
delayMs: number,
|
|
10419
|
+
): AssistantRetryRecoveryKind {
|
|
10420
|
+
if (switchedCredential) return "credential";
|
|
10421
|
+
if (switchedModel) return "model";
|
|
10422
|
+
if (AIError.is(id, AIError.Flag.UsageLimit) && delayMs > 0) return "wait";
|
|
10423
|
+
return "plain";
|
|
10424
|
+
}
|
|
10425
|
+
|
|
10426
|
+
#retryRecoveryNote(recovery: AssistantRetryRecoveryKind, rateLimited: boolean): string {
|
|
10427
|
+
const parts: string[] = [];
|
|
10428
|
+
if (rateLimited) {
|
|
10429
|
+
parts.push("rate-limited");
|
|
10430
|
+
} else if (recovery === "plain") {
|
|
10431
|
+
parts.push("error");
|
|
10432
|
+
}
|
|
10433
|
+
if (recovery === "credential") {
|
|
10434
|
+
parts.push("switched account");
|
|
10435
|
+
} else if (recovery === "model") {
|
|
10436
|
+
parts.push("switched model");
|
|
10437
|
+
} else if (recovery === "wait") {
|
|
10438
|
+
parts.push("waited");
|
|
10439
|
+
}
|
|
10440
|
+
parts.push("retried");
|
|
10441
|
+
return parts.join("; ");
|
|
10442
|
+
}
|
|
10443
|
+
|
|
10444
|
+
async #recordPendingRecoveredRetryError(
|
|
10445
|
+
message: AssistantMessage,
|
|
10446
|
+
id: number,
|
|
10447
|
+
options: { switchedCredential: boolean; switchedModel: boolean; delayMs: number },
|
|
10448
|
+
): Promise<void> {
|
|
10449
|
+
await this.#waitForSessionMessagePersistence(message);
|
|
10450
|
+
const persistenceKey = sessionMessagePersistenceKey(message);
|
|
10451
|
+
if (!persistenceKey) return;
|
|
10452
|
+
let branchEntry: SessionEntry | undefined;
|
|
10453
|
+
for (const entry of this.sessionManager.getBranch().slice().reverse()) {
|
|
10454
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
10455
|
+
if (sessionMessagePersistenceKey(entry.message) !== persistenceKey) continue;
|
|
10456
|
+
if (!sameMessageContent(entry.message, message) && !this.#isSameAssistantMessage(entry.message, message)) {
|
|
10457
|
+
continue;
|
|
10458
|
+
}
|
|
10459
|
+
branchEntry = entry;
|
|
10460
|
+
break;
|
|
10461
|
+
}
|
|
10462
|
+
if (!branchEntry) return;
|
|
10463
|
+
if (this.#pendingRecoveredRetryErrors.some(error => error.entryId === branchEntry.id)) return;
|
|
10464
|
+
const rateLimited = AIError.is(id, AIError.Flag.UsageLimit);
|
|
10465
|
+
const recovery = this.#retryRecoveryKind(id, options.switchedCredential, options.switchedModel, options.delayMs);
|
|
10466
|
+
const note = this.#retryRecoveryNote(recovery, rateLimited);
|
|
10467
|
+
this.#pendingRecoveredRetryErrors.push({
|
|
10468
|
+
entryId: branchEntry.id,
|
|
10469
|
+
persistenceKey,
|
|
10470
|
+
recovery,
|
|
10471
|
+
attempt: this.#retryAttempt,
|
|
10472
|
+
note,
|
|
10473
|
+
});
|
|
10474
|
+
}
|
|
10475
|
+
|
|
10476
|
+
async #markPendingRecoveredRetryErrors(supersedingMessage: AssistantMessage): Promise<RecoveredRetryError[]> {
|
|
10477
|
+
if (this.#pendingRecoveredRetryErrors.length === 0) return [];
|
|
10478
|
+
const branch = this.sessionManager.getBranch();
|
|
10479
|
+
const branchById = new Map<string, SessionEntry>();
|
|
10480
|
+
for (const entry of branch) {
|
|
10481
|
+
branchById.set(entry.id, entry);
|
|
10482
|
+
}
|
|
10483
|
+
const recoveredAt = new Date().toISOString();
|
|
10484
|
+
const supersededBy: AssistantRetryRecovery["supersededBy"] = {
|
|
10485
|
+
timestamp: supersedingMessage.timestamp,
|
|
10486
|
+
provider: supersedingMessage.provider,
|
|
10487
|
+
model: supersedingMessage.model,
|
|
10488
|
+
};
|
|
10489
|
+
if (supersedingMessage.responseId) {
|
|
10490
|
+
supersededBy.responseId = supersedingMessage.responseId;
|
|
10491
|
+
}
|
|
10492
|
+
const recoveredErrors: RecoveredRetryError[] = [];
|
|
10493
|
+
for (const pending of this.#pendingRecoveredRetryErrors) {
|
|
10494
|
+
let entry = branchById.get(pending.entryId);
|
|
10495
|
+
if (entry?.type !== "message" || entry.message.role !== "assistant") {
|
|
10496
|
+
entry = branch
|
|
10497
|
+
.slice()
|
|
10498
|
+
.reverse()
|
|
10499
|
+
.find(
|
|
10500
|
+
candidate =>
|
|
10501
|
+
candidate.type === "message" &&
|
|
10502
|
+
candidate.message.role === "assistant" &&
|
|
10503
|
+
sessionMessagePersistenceKey(candidate.message) === pending.persistenceKey,
|
|
10504
|
+
);
|
|
10505
|
+
}
|
|
10506
|
+
if (entry?.type !== "message" || entry.message.role !== "assistant") continue;
|
|
10507
|
+
const retryRecovery: AssistantRetryRecovery = {
|
|
10508
|
+
kind: "auto-retry",
|
|
10509
|
+
status: "recovered",
|
|
10510
|
+
attempt: pending.attempt,
|
|
10511
|
+
recoveredAt,
|
|
10512
|
+
recovery: pending.recovery,
|
|
10513
|
+
note: pending.note,
|
|
10514
|
+
supersededBy,
|
|
10515
|
+
};
|
|
10516
|
+
entry.message.retryRecovery = retryRecovery;
|
|
10517
|
+
recoveredErrors.push({
|
|
10518
|
+
entryId: entry.id,
|
|
10519
|
+
persistenceKey: pending.persistenceKey,
|
|
10520
|
+
note: retryRecovery.note,
|
|
10521
|
+
retryRecovery,
|
|
10522
|
+
});
|
|
10523
|
+
}
|
|
10524
|
+
if (recoveredErrors.length > 0) {
|
|
10525
|
+
await this.sessionManager.rewriteEntries();
|
|
10526
|
+
}
|
|
10527
|
+
return recoveredErrors;
|
|
10528
|
+
}
|
|
10529
|
+
|
|
10389
10530
|
async #handleEmptyAssistantStop(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
10390
10531
|
if (!this.#isEmptyAssistantStop(assistantMessage)) {
|
|
10391
10532
|
this.#emptyStopRetryCount = 0;
|
|
@@ -10406,6 +10547,7 @@ export class AgentSession {
|
|
|
10406
10547
|
attempt: this.#retryAttempt,
|
|
10407
10548
|
finalError: "Assistant returned empty stop after retry cap",
|
|
10408
10549
|
});
|
|
10550
|
+
this.#clearPendingRecoveredRetryErrors();
|
|
10409
10551
|
this.#retryAttempt = 0;
|
|
10410
10552
|
}
|
|
10411
10553
|
this.#resolveRetry();
|
|
@@ -13265,6 +13407,7 @@ export class AgentSession {
|
|
|
13265
13407
|
attempt: this.#retryAttempt - 1,
|
|
13266
13408
|
finalError: message.errorMessage,
|
|
13267
13409
|
});
|
|
13410
|
+
this.#clearPendingRecoveredRetryErrors();
|
|
13268
13411
|
this.#retryAttempt = 0;
|
|
13269
13412
|
this.#resolveRetry(); // Resolve so waitForRetry() completes
|
|
13270
13413
|
return false;
|
|
@@ -13384,10 +13527,13 @@ export class AgentSession {
|
|
|
13384
13527
|
attempt,
|
|
13385
13528
|
finalError: `Provider requested ${delayMs}ms wait, exceeds retry.maxDelayMs (${maxDelayMs}ms). Original error: ${errorMessage}`,
|
|
13386
13529
|
});
|
|
13530
|
+
this.#clearPendingRecoveredRetryErrors();
|
|
13387
13531
|
this.#resolveRetry();
|
|
13388
13532
|
return false;
|
|
13389
13533
|
}
|
|
13390
13534
|
|
|
13535
|
+
await this.#recordPendingRecoveredRetryError(message, id, { switchedCredential, switchedModel, delayMs });
|
|
13536
|
+
|
|
13391
13537
|
await this.#emitSessionEvent({
|
|
13392
13538
|
type: "auto_retry_start",
|
|
13393
13539
|
attempt: this.#retryAttempt,
|
|
@@ -13425,6 +13571,7 @@ export class AgentSession {
|
|
|
13425
13571
|
attempt,
|
|
13426
13572
|
finalError: "Retry cancelled",
|
|
13427
13573
|
});
|
|
13574
|
+
this.#clearPendingRecoveredRetryErrors();
|
|
13428
13575
|
this.#resolveRetry();
|
|
13429
13576
|
return false;
|
|
13430
13577
|
}
|
|
@@ -251,6 +251,13 @@ export function buildSessionContext(
|
|
|
251
251
|
const appendMessage = (entry: SessionEntry) => {
|
|
252
252
|
handleEntryResetTracking(entry);
|
|
253
253
|
if (entry.type === "message") {
|
|
254
|
+
if (
|
|
255
|
+
!options?.transcript &&
|
|
256
|
+
entry.message.role === "assistant" &&
|
|
257
|
+
entry.message.retryRecovery?.status === "recovered"
|
|
258
|
+
) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
254
261
|
pushMessage(entry.message);
|
|
255
262
|
} else if (entry.type === "custom_message") {
|
|
256
263
|
pushMessage(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fsp from "node:fs/promises";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ProgressInfo } from "@huggingface/transformers";
|
|
@@ -27,6 +28,15 @@ import packageJson from "../../package.json" with { type: "json" };
|
|
|
27
28
|
|
|
28
29
|
export const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
29
30
|
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
|
|
31
|
+
const ONNX_RUNTIME_NODE_PACKAGE = "onnxruntime-node";
|
|
32
|
+
const ONNX_RUNTIME_CUDA_INSTALL = "cuda12";
|
|
33
|
+
const ONNX_RUNTIME_CUDA_PROVIDER_FILES = [
|
|
34
|
+
"libonnxruntime_providers_cuda.so",
|
|
35
|
+
"libonnxruntime_providers_shared.so",
|
|
36
|
+
"libonnxruntime_providers_tensorrt.so",
|
|
37
|
+
] as const;
|
|
38
|
+
const LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR = path.join("bin", "napi-v6", "linux", "x64");
|
|
39
|
+
|
|
30
40
|
const sourceRequire = createRequire(import.meta.url);
|
|
31
41
|
|
|
32
42
|
// ── Error serialization ─────────────────────────────────────────────
|
|
@@ -162,6 +172,87 @@ export async function installSharpStubResolver(runtimeDir: string): Promise<stri
|
|
|
162
172
|
return nodeModules;
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
function shouldInstallOnnxRuntimeCudaProviders(device: string | undefined): boolean {
|
|
176
|
+
const normalized = device?.trim().toLowerCase();
|
|
177
|
+
return (
|
|
178
|
+
process.platform === "linux" &&
|
|
179
|
+
process.arch === "x64" &&
|
|
180
|
+
(normalized === "cuda" || normalized === "gpu" || normalized === "auto")
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function missingOnnxRuntimeCudaProviderFiles(binDir: string): Promise<string[]> {
|
|
185
|
+
const missing: string[] = [];
|
|
186
|
+
for (const file of ONNX_RUNTIME_CUDA_PROVIDER_FILES) {
|
|
187
|
+
try {
|
|
188
|
+
await fsp.access(path.join(binDir, file));
|
|
189
|
+
} catch {
|
|
190
|
+
missing.push(file);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return missing;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
197
|
+
if (!stream) return "";
|
|
198
|
+
return new Response(stream).text();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function installOnnxRuntimeCudaProviders(packageDir: string, runtimeDir: string, binDir: string): Promise<void> {
|
|
202
|
+
const script = path.join(packageDir, "script", "install.js");
|
|
203
|
+
try {
|
|
204
|
+
await fsp.access(script);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`ONNX Runtime CUDA provider binaries are missing from ${binDir}, and ${script} is unavailable. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const proc = Bun.spawn([process.execPath, script], {
|
|
212
|
+
cwd: runtimeDir,
|
|
213
|
+
env: { ...Bun.env, BUN_BE_BUN: "1", ONNXRUNTIME_NODE_INSTALL: ONNX_RUNTIME_CUDA_INSTALL },
|
|
214
|
+
stdout: "pipe",
|
|
215
|
+
stderr: "pipe",
|
|
216
|
+
});
|
|
217
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
218
|
+
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
|
|
219
|
+
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
|
|
220
|
+
proc.exited,
|
|
221
|
+
]);
|
|
222
|
+
if (exitCode !== 0) {
|
|
223
|
+
const output = `${stdout}\n${stderr}`.trim();
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Failed to install ONNX Runtime CUDA provider binaries into ${binDir} with ${process.execPath} ${script} (exit ${exitCode}). Remove the tiny-model side runtime cache at ${runtimeDir} and retry with network access. ${output}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Repairs the compiled Transformers side runtime when CUDA was requested and
|
|
232
|
+
* Bun skipped `onnxruntime-node`'s NuGet sidecar install.
|
|
233
|
+
*/
|
|
234
|
+
export async function ensureOnnxRuntimeCudaProviders(
|
|
235
|
+
runtimeDir: string,
|
|
236
|
+
device = process.env.PI_TINY_DEVICE,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
if (!shouldInstallOnnxRuntimeCudaProviders(device)) return;
|
|
239
|
+
const nodeModules = path.join(runtimeDir, "node_modules");
|
|
240
|
+
const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
|
|
241
|
+
if (!manifest)
|
|
242
|
+
throw new Error(`Unable to resolve ${ONNX_RUNTIME_NODE_PACKAGE} in compiled runtime at ${nodeModules}`);
|
|
243
|
+
const packageDir = path.dirname(manifest);
|
|
244
|
+
const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
|
|
245
|
+
const missing = await missingOnnxRuntimeCudaProviderFiles(binDir);
|
|
246
|
+
if (missing.length === 0) return;
|
|
247
|
+
|
|
248
|
+
await installOnnxRuntimeCudaProviders(packageDir, runtimeDir, binDir);
|
|
249
|
+
const stillMissing = await missingOnnxRuntimeCudaProviderFiles(binDir);
|
|
250
|
+
if (stillMissing.length === 0) return;
|
|
251
|
+
throw new Error(
|
|
252
|
+
`ONNX Runtime CUDA provider install completed but ${stillMissing.join(", ")} are still missing from ${binDir}. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
165
256
|
/**
|
|
166
257
|
* Prepare a freshly-installed compiled runtime for loading and return the
|
|
167
258
|
* absolute entrypoint of `packageName` to `require`.
|
|
@@ -211,6 +302,114 @@ interface ConfigurableTransformers {
|
|
|
211
302
|
LogLevel: { ERROR: unknown };
|
|
212
303
|
}
|
|
213
304
|
|
|
305
|
+
export interface TransformersRuntimeMetadata {
|
|
306
|
+
__ompRuntimeNodeModules?: string;
|
|
307
|
+
__ompTransformersEntry?: string;
|
|
308
|
+
__ompCudaRepairError?: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function attachTransformersRuntimeMetadata<T extends ConfigurableTransformers>(
|
|
312
|
+
transformers: T,
|
|
313
|
+
metadata: TransformersRuntimeMetadata,
|
|
314
|
+
): T {
|
|
315
|
+
const runtime = transformers as T & TransformersRuntimeMetadata;
|
|
316
|
+
runtime.__ompRuntimeNodeModules = metadata.__ompRuntimeNodeModules;
|
|
317
|
+
runtime.__ompTransformersEntry = metadata.__ompTransformersEntry;
|
|
318
|
+
runtime.__ompCudaRepairError = metadata.__ompCudaRepairError;
|
|
319
|
+
return runtime;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const TRANSITIVE_CUDA_LIBRARY_RE =
|
|
323
|
+
/\b(lib(?:cu|nv)[A-Za-z0-9_.+-]*\.so(?:\.[0-9]+)*)\b[^:\n]*:\s*cannot open shared object file/iu;
|
|
324
|
+
const CUDA_DEVICE_UNAVAILABLE_RE = /\bCUDA failure 100\b|no CUDA-capable device is detected|cudaSetDevice|GPU=-1/iu;
|
|
325
|
+
|
|
326
|
+
function cudaDeviceUnavailable(error: unknown): boolean {
|
|
327
|
+
return CUDA_DEVICE_UNAVAILABLE_RE.test(errorText(error));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function missingCudaLibrary(error: unknown): string | undefined {
|
|
331
|
+
return TRANSITIVE_CUDA_LIBRARY_RE.exec(errorText(error))?.[1];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function cudaFailureCause(
|
|
335
|
+
metadata: TransformersRuntimeMetadata,
|
|
336
|
+
error: unknown,
|
|
337
|
+
missingFiles: readonly string[],
|
|
338
|
+
): string {
|
|
339
|
+
if (metadata.__ompCudaRepairError) {
|
|
340
|
+
return `ONNX Runtime CUDA provider install failed: ${metadata.__ompCudaRepairError}`;
|
|
341
|
+
}
|
|
342
|
+
if (missingFiles.length > 0) return `missing ONNX Runtime CUDA provider file(s): ${missingFiles.join(", ")}`;
|
|
343
|
+
const missingLibrary = missingCudaLibrary(error);
|
|
344
|
+
if (missingLibrary) return `${missingLibrary}: cannot open shared object file`;
|
|
345
|
+
if (cudaDeviceUnavailable(error)) {
|
|
346
|
+
return "CUDA provider files are present; CUDA runtime reports no CUDA-capable device";
|
|
347
|
+
}
|
|
348
|
+
return "CUDA provider files are present; inspect the original ONNX Runtime CUDA error";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function cudaFailureHint(
|
|
352
|
+
metadata: TransformersRuntimeMetadata,
|
|
353
|
+
error: unknown,
|
|
354
|
+
missingFiles: readonly string[],
|
|
355
|
+
): string {
|
|
356
|
+
if (metadata.__ompCudaRepairError) {
|
|
357
|
+
return "restore network access to nuget.org (or pre-populate the tiny side runtime) and rerun; CPU inference remained available";
|
|
358
|
+
}
|
|
359
|
+
if (missingFiles.length > 0) return "reinstall the tiny side runtime with ONNX Runtime postinstall enabled";
|
|
360
|
+
if (missingCudaLibrary(error)) {
|
|
361
|
+
return "install the matching CUDA/cuDNN shared libraries and expose them on the dynamic loader path";
|
|
362
|
+
}
|
|
363
|
+
if (cudaDeviceUnavailable(error)) {
|
|
364
|
+
return "make the NVIDIA GPU visible to this process/session, or use providers.tinyModelDevice=default/cpu";
|
|
365
|
+
}
|
|
366
|
+
return "check the host CUDA driver, device visibility, and ONNX Runtime CUDA compatibility";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function resolveOnnxRuntimePackageDir(metadata: TransformersRuntimeMetadata): string | null {
|
|
370
|
+
const entry = metadata.__ompTransformersEntry;
|
|
371
|
+
if (entry) {
|
|
372
|
+
try {
|
|
373
|
+
return path.dirname(createRequire(entry).resolve(`${ONNX_RUNTIME_NODE_PACKAGE}/package.json`));
|
|
374
|
+
} catch {
|
|
375
|
+
// Fall through to the side-runtime resolver below.
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const nodeModules = metadata.__ompRuntimeNodeModules;
|
|
379
|
+
if (!nodeModules) return null;
|
|
380
|
+
const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
|
|
381
|
+
return manifest ? path.dirname(manifest) : null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function formatOnnxRuntimeCudaDiagnostics(
|
|
385
|
+
metadata: TransformersRuntimeMetadata,
|
|
386
|
+
requestedDevice: string,
|
|
387
|
+
error: unknown,
|
|
388
|
+
): Promise<string | null> {
|
|
389
|
+
const device = requestedDevice.trim().toLowerCase();
|
|
390
|
+
if (device !== "cuda" && device !== "gpu" && device !== "auto") return null;
|
|
391
|
+
if (process.platform !== "linux" || process.arch !== "x64") return null;
|
|
392
|
+
const packageDir = resolveOnnxRuntimePackageDir(metadata);
|
|
393
|
+
if (!packageDir) {
|
|
394
|
+
return [
|
|
395
|
+
"ONNX Runtime CUDA diagnostics:",
|
|
396
|
+
` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
|
|
397
|
+
" cause: unable to resolve onnxruntime-node in the tiny-model runtime",
|
|
398
|
+
].join("\n");
|
|
399
|
+
}
|
|
400
|
+
const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
|
|
401
|
+
const missingFiles = await missingOnnxRuntimeCudaProviderFiles(binDir);
|
|
402
|
+
const sideRuntime = metadata.__ompRuntimeNodeModules;
|
|
403
|
+
const lines = [
|
|
404
|
+
"ONNX Runtime CUDA diagnostics:",
|
|
405
|
+
` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
|
|
406
|
+
sideRuntime ? ` side runtime: ${sideRuntime}` : ` onnxruntime-node: ${packageDir}`,
|
|
407
|
+
` cause: ${cudaFailureCause(metadata, error, missingFiles)}`,
|
|
408
|
+
];
|
|
409
|
+
lines.push(` hint: ${cudaFailureHint(metadata, error, missingFiles)}`);
|
|
410
|
+
return lines.join("\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
214
413
|
function configureTransformers<T extends ConfigurableTransformers>(transformers: T): T {
|
|
215
414
|
transformers.env.cacheDir = getTinyModelsCacheDir();
|
|
216
415
|
transformers.env.allowLocalModels = false;
|
|
@@ -251,7 +450,12 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
|
|
|
251
450
|
runtimeDir: () => string,
|
|
252
451
|
): Promise<T> {
|
|
253
452
|
return holder.load(async () => {
|
|
254
|
-
if (!isCompiledBinary())
|
|
453
|
+
if (!isCompiledBinary()) {
|
|
454
|
+
const entry = sourceRequire.resolve(TRANSFORMERS_PACKAGE);
|
|
455
|
+
return attachTransformersRuntimeMetadata(configureTransformers(sourceRequire(entry) as T), {
|
|
456
|
+
__ompTransformersEntry: entry,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
255
459
|
const installedDir = await ensureRuntimeInstalled({
|
|
256
460
|
runtimeDir: runtimeDir(),
|
|
257
461
|
install: {
|
|
@@ -270,8 +474,21 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
|
|
|
270
474
|
},
|
|
271
475
|
}),
|
|
272
476
|
});
|
|
477
|
+
let cudaRepairError: string | undefined;
|
|
478
|
+
try {
|
|
479
|
+
await ensureOnnxRuntimeCudaProviders(installedDir);
|
|
480
|
+
} catch (repairError) {
|
|
481
|
+
// Deferred failure: keep loading Transformers so `loadPipelineWithDeviceFallback`
|
|
482
|
+
// still gets its CUDA→CPU retry. The error is surfaced through the CUDA
|
|
483
|
+
// diagnostics attached to the runtime metadata.
|
|
484
|
+
cudaRepairError = errorMessage(repairError);
|
|
485
|
+
}
|
|
273
486
|
const entry = await prepareCompiledRuntime(installedDir, TRANSFORMERS_PACKAGE);
|
|
274
487
|
const require_ = createRequire(entry);
|
|
275
|
-
return configureTransformers(require_(entry) as T)
|
|
488
|
+
return attachTransformersRuntimeMetadata(configureTransformers(require_(entry) as T), {
|
|
489
|
+
__ompRuntimeNodeModules: path.join(installedDir, "node_modules"),
|
|
490
|
+
__ompTransformersEntry: entry,
|
|
491
|
+
__ompCudaRepairError: cudaRepairError,
|
|
492
|
+
});
|
|
276
493
|
});
|
|
277
494
|
}
|