@oh-my-pi/pi-coding-agent 16.0.1 → 16.0.3
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 +70 -0
- package/README.md +0 -1
- package/dist/cli.js +316 -371
- package/dist/types/advisor/advise-tool.d.ts +30 -1
- package/dist/types/commands/install.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +0 -10
- package/dist/types/eval/js/shared/runtime.d.ts +1 -0
- package/dist/types/eval/js/worker-core.d.ts +1 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/goals/runtime.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +14 -4
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +3 -2
- package/dist/types/modes/theme/mermaid-cache.d.ts +18 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +16 -1
- package/dist/types/sdk.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +20 -8
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-dump-format.d.ts +8 -2
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/session/session-history-format.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +22 -0
- package/dist/types/stt/downloader.d.ts +5 -5
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/persisted-revive.d.ts +36 -0
- package/dist/types/tiny/models.d.ts +8 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/utils/markit.d.ts +8 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +156 -12
- package/src/advisor/advise-tool.ts +48 -6
- package/src/advisor/runtime.ts +10 -3
- package/src/auto-thinking/classifier.ts +12 -3
- package/src/cli/args.ts +1 -0
- package/src/cli.ts +2 -2
- package/src/commands/install.ts +3 -3
- package/src/config/model-resolver.ts +63 -12
- package/src/config/settings-schema.ts +0 -11
- package/src/discovery/github.ts +89 -1
- package/src/eval/agent-bridge.ts +2 -0
- package/src/eval/js/context-manager.ts +2 -1
- package/src/eval/js/shared/runtime.ts +189 -15
- package/src/eval/js/worker-core.ts +19 -0
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/export/html/index.ts +1 -1
- package/src/export/html/tool-views.generated.js +34 -35
- package/src/extensibility/extensions/loader.ts +21 -9
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +154 -21
- package/src/extensibility/plugins/manager.ts +40 -33
- package/src/goals/runtime.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +9 -11
- package/src/main.ts +20 -0
- package/src/mcp/render.ts +11 -1
- package/src/mcp/tool-bridge.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +63 -18
- package/src/modes/components/custom-editor.ts +63 -15
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/input-controller.ts +15 -9
- package/src/modes/controllers/selector-controller.ts +13 -8
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/setup-wizard/wizard-overlay.ts +26 -4
- package/src/modes/theme/mermaid-cache.ts +74 -11
- package/src/modes/theme/theme.ts +14 -1
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/registry/agent-lifecycle.ts +60 -8
- package/src/sdk.ts +20 -26
- package/src/session/agent-session.ts +381 -110
- package/src/session/artifacts.ts +19 -1
- package/src/session/messages.ts +1 -1
- package/src/session/session-dump-format.ts +167 -23
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-history-format.ts +37 -3
- package/src/session/session-manager.ts +94 -4
- package/src/slash-commands/builtin-registry.ts +4 -7
- package/src/stt/asr-client.ts +6 -0
- package/src/stt/downloader.ts +13 -6
- package/src/stt/stt-controller.ts +52 -11
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +118 -6
- package/src/task/index.ts +2 -2
- package/src/task/persisted-revive.ts +128 -0
- package/src/tiny/models.ts +10 -0
- package/src/tiny/worker.ts +4 -3
- package/src/tools/builtin-names.ts +0 -1
- package/src/tools/index.ts +0 -4
- package/src/tools/output-meta.ts +17 -3
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/utils/title-generator.ts +4 -4
- package/dist/types/tools/render-mermaid.d.ts +0 -38
- package/src/prompts/tools/render-mermaid.md +0 -9
- package/src/tools/render-mermaid.ts +0 -69
|
@@ -126,7 +126,7 @@ import {
|
|
|
126
126
|
AdvisorRuntime,
|
|
127
127
|
type AdvisorSeverity,
|
|
128
128
|
formatAdvisorBatchContent,
|
|
129
|
-
|
|
129
|
+
resolveAdvisorDeliveryChannel,
|
|
130
130
|
} from "../advisor";
|
|
131
131
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
132
132
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
@@ -139,9 +139,11 @@ import {
|
|
|
139
139
|
filterAvailableModelsByEnabledPatterns,
|
|
140
140
|
formatModelSelectorValue,
|
|
141
141
|
formatModelString,
|
|
142
|
+
formatModelStringWithRouting,
|
|
142
143
|
getModelMatchPreferences,
|
|
143
144
|
parseModelString,
|
|
144
145
|
type ResolvedModelRoleValue,
|
|
146
|
+
resolveModelOverride,
|
|
145
147
|
resolveModelRoleValue,
|
|
146
148
|
resolveRoleSelection,
|
|
147
149
|
} from "../config/model-resolver";
|
|
@@ -248,7 +250,7 @@ import {
|
|
|
248
250
|
} from "../tool-discovery/tool-index";
|
|
249
251
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
250
252
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
251
|
-
import { outputMeta } from "../tools/output-meta";
|
|
253
|
+
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
252
254
|
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
253
255
|
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
|
254
256
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo";
|
|
@@ -273,6 +275,7 @@ import {
|
|
|
273
275
|
type BashExecutionMessage,
|
|
274
276
|
type CustomMessage,
|
|
275
277
|
convertToLlm,
|
|
278
|
+
GENERIC_ABORT_SENTINEL,
|
|
276
279
|
type PythonExecutionMessage,
|
|
277
280
|
readQueueChipText,
|
|
278
281
|
SILENT_ABORT_MARKER,
|
|
@@ -640,8 +643,7 @@ function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | u
|
|
|
640
643
|
}
|
|
641
644
|
|
|
642
645
|
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
643
|
-
|
|
644
|
-
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
|
646
|
+
return formatModelSelectorValue(formatModelStringWithRouting(model), thinkingLevel);
|
|
645
647
|
}
|
|
646
648
|
|
|
647
649
|
function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): string {
|
|
@@ -953,6 +955,44 @@ function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
|
953
955
|
return message.role === "custom" && message.customType === "advisor";
|
|
954
956
|
}
|
|
955
957
|
|
|
958
|
+
/**
|
|
959
|
+
* A queued message the user can restore to the editor / pull back as a draft.
|
|
960
|
+
* Only genuinely user-authored messages qualify: plain user turns, or custom
|
|
961
|
+
* messages explicitly attributed to the user (e.g. `/skill` invocations).
|
|
962
|
+
* Agent-authored queued cards — advisor concern/blocker notes, IRC asides,
|
|
963
|
+
* extension notices, hidden goal/plan/budget steers — ride the same
|
|
964
|
+
* steer/follow-up queues but must never be dumped into the editor on Esc/Alt+Up.
|
|
965
|
+
*/
|
|
966
|
+
function isUserQueuedMessage(message: AgentMessage): boolean {
|
|
967
|
+
if (message.role === "user") return true;
|
|
968
|
+
return message.role === "custom" && message.attribution === "user" && message.display !== false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Custom-message types of the hidden magic-keyword notices that `#createMagicKeywordNotices`
|
|
972
|
+
* enqueues alongside a user prompt. Keep in sync with that method. */
|
|
973
|
+
const MAGIC_KEYWORD_NOTICE_TYPES: ReadonlySet<string> = new Set([
|
|
974
|
+
"ultrathink-notice",
|
|
975
|
+
"orchestrate-notice",
|
|
976
|
+
"workflow-notice",
|
|
977
|
+
]);
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* A hidden, user-attributed companion of a queued user prompt: the magic-keyword
|
|
981
|
+
* notices (`ultrathink`/`orchestrate`/`workflow`) enqueued alongside the user
|
|
982
|
+
* message. They are `attribution: "user"` but `display: false`, so they are not
|
|
983
|
+
* editor-restorable; when the user pulls their prompt back out of the queue these
|
|
984
|
+
* must leave with it rather than linger as stale, companion-less steering. Scoped to
|
|
985
|
+
* the known notice types so an unrelated hidden user custom is never silently dropped.
|
|
986
|
+
*/
|
|
987
|
+
function isHiddenUserCompanion(message: AgentMessage): boolean {
|
|
988
|
+
return (
|
|
989
|
+
message.role === "custom" &&
|
|
990
|
+
message.attribution === "user" &&
|
|
991
|
+
message.display === false &&
|
|
992
|
+
MAGIC_KEYWORD_NOTICE_TYPES.has(message.customType)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
956
996
|
function queueChipText(message: AgentMessage): string {
|
|
957
997
|
if (message.role === "custom") {
|
|
958
998
|
return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
|
|
@@ -1258,7 +1298,70 @@ export class AgentSession {
|
|
|
1258
1298
|
* queue was consumed normally or a new turn already started. */
|
|
1259
1299
|
#drainStrandedQueuedMessages(): void {
|
|
1260
1300
|
if (this.#abortInProgress) return;
|
|
1301
|
+
// A concern steered into a resumed streaming run after a user interrupt can
|
|
1302
|
+
// strand at the turn tail (steered past the loop's final boundary poll). While
|
|
1303
|
+
// that interrupt's suppression is still in effect, reclaim such advisor steers
|
|
1304
|
+
// as visible advice once idle — mirroring abort's #extractQueuedAdvisorCards —
|
|
1305
|
+
// so they neither auto-resume the run the user stopped (a non-empty steer queue
|
|
1306
|
+
// otherwise bypasses the latch in #canAutoContinueForFollowUp) nor linger to
|
|
1307
|
+
// flush at the next prompt. Real user steers/follow-ups are left untouched.
|
|
1308
|
+
if (this.#advisorAutoResumeSuppressed && !this.isStreaming) {
|
|
1309
|
+
for (const card of this.#extractQueuedAdvisorCards()) {
|
|
1310
|
+
this.#preserveAdvisorCard(card);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1261
1313
|
this.#scheduleQueuedMessageDrain();
|
|
1314
|
+
this.#resumeStrandedIrcAsides();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** IRC asides that arrive after the loop's final aside poll — or while an abort skipped that
|
|
1318
|
+
* poll — land in #pendingIrcAsides with no loop left to drain them; the queued-message drain's
|
|
1319
|
+
* gate (agent.hasQueuedMessages()) does not count them. Once idle, wake a turn so the agent
|
|
1320
|
+
* responds to the peer. Skip only when a queued steer/follow-up will itself drive a resume turn
|
|
1321
|
+
* whose aside poll already consumes these (no double-wake). */
|
|
1322
|
+
#resumeStrandedIrcAsides(): void {
|
|
1323
|
+
if (this.#isDisposed || this.isStreaming) return;
|
|
1324
|
+
if (this.#pendingIrcAsides.length === 0) return;
|
|
1325
|
+
if (this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages()) return;
|
|
1326
|
+
const records = this.#pendingIrcAsides;
|
|
1327
|
+
this.#pendingIrcAsides = [];
|
|
1328
|
+
this.#wakeForIrc(records);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** Fire-and-forget wake turn for incoming IRC — idle delivery and stranded-aside resume both
|
|
1332
|
+
* route here. Wrapped in #beginInFlight/#endInFlight so the turn is tracked and its settle
|
|
1333
|
+
* re-drains anything that stranded during it. A user interrupt may have intentionally left a
|
|
1334
|
+
* follow-up queued behind an invalid tail (seam #5); the wake turn's loop would otherwise drain
|
|
1335
|
+
* it, so park the follow-up queue across the wake and restore it after. It stays queued post-wake
|
|
1336
|
+
* because #canAutoContinueForFollowUp suppresses follow-up auto-resume while a user interrupt is
|
|
1337
|
+
* in effect, even though the wake left a provider-valid tail. */
|
|
1338
|
+
#wakeForIrc(records: CustomMessage[]): void {
|
|
1339
|
+
// Park only a *blocked* follow-up (one a user interrupt is intentionally holding); an
|
|
1340
|
+
// already-resumable follow-up can ride the wake turn normally without reordering.
|
|
1341
|
+
const parkedFollowUps =
|
|
1342
|
+
this.agent.peekSteeringQueue().length === 0 &&
|
|
1343
|
+
this.agent.peekFollowUpQueue().length > 0 &&
|
|
1344
|
+
!this.#canAutoContinueForFollowUp()
|
|
1345
|
+
? [...this.agent.peekFollowUpQueue()]
|
|
1346
|
+
: [];
|
|
1347
|
+
if (parkedFollowUps.length > 0) {
|
|
1348
|
+
this.agent.replaceQueues([...this.agent.peekSteeringQueue()], []);
|
|
1349
|
+
}
|
|
1350
|
+
this.#beginInFlight();
|
|
1351
|
+
void this.agent
|
|
1352
|
+
.prompt(records)
|
|
1353
|
+
.catch(error => {
|
|
1354
|
+
logger.warn("IRC wake turn failed", { error: String(error) });
|
|
1355
|
+
})
|
|
1356
|
+
.finally(() => {
|
|
1357
|
+
if (parkedFollowUps.length > 0) {
|
|
1358
|
+
this.agent.replaceQueues(
|
|
1359
|
+
[...this.agent.peekSteeringQueue()],
|
|
1360
|
+
[...parkedFollowUps, ...this.agent.peekFollowUpQueue()],
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
this.#endInFlight();
|
|
1364
|
+
});
|
|
1262
1365
|
}
|
|
1263
1366
|
|
|
1264
1367
|
/** Remove advisor concern/blocker cards from the agent-core steer/follow-up
|
|
@@ -1279,14 +1382,14 @@ export class AgentSession {
|
|
|
1279
1382
|
}
|
|
1280
1383
|
|
|
1281
1384
|
/** Record a suppressed advisor concern as visible, persisted advice without
|
|
1282
|
-
* triggering a turn. When the agent is idle (the normal post-interrupt case
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
* live streamMessage. */
|
|
1385
|
+
* triggering a turn. When the agent is idle (the normal post-interrupt case,
|
|
1386
|
+
* including the post-prompt unwind window where the core loop has ended), emit
|
|
1387
|
+
* message_start/message_end like #flushPendingIrcAsides so #handleAgentEvent
|
|
1388
|
+
* renders it live (TUI/ACP) and persists it as a CustomMessageEntry. Only while
|
|
1389
|
+
* an abort is still tearing a live turn down do we park it hidden, so abort's
|
|
1390
|
+
* settle step replays it once idle — never appended into a live streamMessage. */
|
|
1288
1391
|
#preserveAdvisorCard(card: CustomMessage): void {
|
|
1289
|
-
if (this.isStreaming) {
|
|
1392
|
+
if (this.#abortInProgress && this.isStreaming) {
|
|
1290
1393
|
this.#pendingNextTurnMessages.push(card);
|
|
1291
1394
|
return;
|
|
1292
1395
|
}
|
|
@@ -1526,33 +1629,48 @@ export class AgentSession {
|
|
|
1526
1629
|
// channel (aborting in-flight tools at the next steering boundary); when the
|
|
1527
1630
|
// loop has already yielded, triggerTurn resumes it so the advice is acted on
|
|
1528
1631
|
// immediately rather than waiting for the next user prompt. After a deliberate
|
|
1529
|
-
// user interrupt
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1632
|
+
// user interrupt the auto-resume is suppressed — but only while the agent is
|
|
1633
|
+
// idle or still tearing the interrupted turn down: a concern is then recorded
|
|
1634
|
+
// as a visible card and re-enters context when the user resumes. Once a turn
|
|
1635
|
+
// is streaming again (a resume the user already drove) it is steered in live,
|
|
1636
|
+
// since steering an active run auto-resumes nothing; parking it there would
|
|
1637
|
+
// strand the advice and dump the backlog as one burst at the next prompt. A
|
|
1638
|
+
// plain nit always rides the non-interrupting YieldQueue aside.
|
|
1532
1639
|
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
});
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
void this.sendCustomMessage(
|
|
1550
|
-
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1551
|
-
{ deliverAs: "steer", triggerTurn: true },
|
|
1552
|
-
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1640
|
+
const channel = resolveAdvisorDeliveryChannel({
|
|
1641
|
+
severity,
|
|
1642
|
+
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
1643
|
+
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
1644
|
+
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
1645
|
+
// loop will consume a steer at its next boundary; steering into the unwind
|
|
1646
|
+
// window would strand the card and let #drainStrandedQueuedMessages
|
|
1647
|
+
// auto-resume it despite the user's interrupt.
|
|
1648
|
+
streaming: this.agent.state.isStreaming,
|
|
1649
|
+
aborting: this.#abortInProgress,
|
|
1650
|
+
});
|
|
1651
|
+
if (channel === "aside") {
|
|
1652
|
+
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
1553
1653
|
return;
|
|
1554
1654
|
}
|
|
1555
|
-
|
|
1655
|
+
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1656
|
+
const content = formatAdvisorBatchContent(notes);
|
|
1657
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
1658
|
+
if (channel === "preserve") {
|
|
1659
|
+
this.#preserveAdvisorCard({
|
|
1660
|
+
role: "custom",
|
|
1661
|
+
customType: "advisor",
|
|
1662
|
+
content,
|
|
1663
|
+
display: true,
|
|
1664
|
+
attribution: "agent",
|
|
1665
|
+
details,
|
|
1666
|
+
timestamp: Date.now(),
|
|
1667
|
+
});
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
void this.sendCustomMessage(
|
|
1671
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1672
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
1673
|
+
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1556
1674
|
};
|
|
1557
1675
|
|
|
1558
1676
|
const adviseTool = new AdviseTool(enqueueAdvice);
|
|
@@ -2402,6 +2520,11 @@ export class AgentSession {
|
|
|
2402
2520
|
return;
|
|
2403
2521
|
}
|
|
2404
2522
|
|
|
2523
|
+
if (this.#isRetryableReasonlessAbort(msg)) {
|
|
2524
|
+
const didRetry = await this.#handleRetryableError(msg, { allowModelFallback: false });
|
|
2525
|
+
if (didRetry) return;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2405
2528
|
// A deliberate abort should settle the current turn, not trigger queued continuations.
|
|
2406
2529
|
if (msg.stopReason === "aborted") {
|
|
2407
2530
|
this.#resolveRetry();
|
|
@@ -2566,6 +2689,11 @@ export class AgentSession {
|
|
|
2566
2689
|
|
|
2567
2690
|
#scheduleAutoContinuePrompt(generation: number): void {
|
|
2568
2691
|
const continuePrompt = async () => {
|
|
2692
|
+
// Compaction summarizes away the first-message eager preludes, so re-assert the
|
|
2693
|
+
// delegate-via-tasks / phased-todo reminders on this auto-resumed turn. This runs
|
|
2694
|
+
// at invocation (past the abort check below), so an aborted continuation queues
|
|
2695
|
+
// nothing; scoped to this request via prependMessages, never the shared queue.
|
|
2696
|
+
const eagerNudges = this.#buildPostCompactionEagerNudges();
|
|
2569
2697
|
await this.#promptWithMessage(
|
|
2570
2698
|
{
|
|
2571
2699
|
role: "developer",
|
|
@@ -2574,7 +2702,10 @@ export class AgentSession {
|
|
|
2574
2702
|
timestamp: Date.now(),
|
|
2575
2703
|
},
|
|
2576
2704
|
autoContinuePrompt,
|
|
2577
|
-
{
|
|
2705
|
+
{
|
|
2706
|
+
skipPostPromptRecoveryWait: true,
|
|
2707
|
+
prependMessages: eagerNudges.length > 0 ? eagerNudges : undefined,
|
|
2708
|
+
},
|
|
2578
2709
|
);
|
|
2579
2710
|
};
|
|
2580
2711
|
this.#schedulePostPromptTask(
|
|
@@ -3629,7 +3760,7 @@ export class AgentSession {
|
|
|
3629
3760
|
*/
|
|
3630
3761
|
beginDispose(): void {
|
|
3631
3762
|
this.#isDisposed = true;
|
|
3632
|
-
this.#
|
|
3763
|
+
this.#flushPendingIrcAsides();
|
|
3633
3764
|
this.yieldQueue.clear();
|
|
3634
3765
|
this.agent.setAsideMessageProvider(undefined);
|
|
3635
3766
|
this.#stopAdvisorRuntime();
|
|
@@ -4480,7 +4611,7 @@ export class AgentSession {
|
|
|
4480
4611
|
});
|
|
4481
4612
|
|
|
4482
4613
|
for (const customTool of mcpTools) {
|
|
4483
|
-
const wrapped = CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool;
|
|
4614
|
+
const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool);
|
|
4484
4615
|
const finalTool = (
|
|
4485
4616
|
this.#extensionRunner ? new ExtensionToolWrapper(wrapped, this.#extensionRunner) : wrapped
|
|
4486
4617
|
) as AgentTool;
|
|
@@ -4540,8 +4671,9 @@ export class AgentSession {
|
|
|
4540
4671
|
this.#rpcHostToolNames.clear();
|
|
4541
4672
|
|
|
4542
4673
|
for (const tool of rpcTools) {
|
|
4674
|
+
const metaWrapped = wrapToolWithMetaNotice(tool);
|
|
4543
4675
|
const finalTool = (
|
|
4544
|
-
this.#extensionRunner ? new ExtensionToolWrapper(
|
|
4676
|
+
this.#extensionRunner ? new ExtensionToolWrapper(metaWrapped, this.#extensionRunner) : metaWrapped
|
|
4545
4677
|
) as AgentTool;
|
|
4546
4678
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
4547
4679
|
this.#rpcHostToolNames.add(finalTool.name);
|
|
@@ -5107,15 +5239,16 @@ export class AgentSession {
|
|
|
5107
5239
|
if (!options?.streamingBehavior) {
|
|
5108
5240
|
throw new AgentBusyError();
|
|
5109
5241
|
}
|
|
5242
|
+
// Steer/follow-up the keyword notices BEFORE the queued user message so the
|
|
5243
|
+
// model reads the steering notice ahead of the prompt it modifies.
|
|
5244
|
+
for (const notice of keywordNotices) {
|
|
5245
|
+
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5246
|
+
}
|
|
5110
5247
|
if (options.streamingBehavior === "followUp") {
|
|
5111
5248
|
await this.#queueUserMessage(expandedText, options?.images, "followUp");
|
|
5112
5249
|
} else {
|
|
5113
5250
|
await this.#queueUserMessage(expandedText, options?.images, "steer");
|
|
5114
5251
|
}
|
|
5115
|
-
// Steer/follow-up the keyword notices alongside the queued user message.
|
|
5116
|
-
for (const notice of keywordNotices) {
|
|
5117
|
-
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5118
|
-
}
|
|
5119
5252
|
return true;
|
|
5120
5253
|
}
|
|
5121
5254
|
|
|
@@ -5154,8 +5287,10 @@ export class AgentSession {
|
|
|
5154
5287
|
await this.#promptWithMessage(message, expandedText, {
|
|
5155
5288
|
...options,
|
|
5156
5289
|
images: normalizedImages,
|
|
5157
|
-
prependMessages:
|
|
5158
|
-
|
|
5290
|
+
prependMessages:
|
|
5291
|
+
preludeMessages.length > 0 || keywordNotices.length > 0
|
|
5292
|
+
? [...preludeMessages, ...keywordNotices]
|
|
5293
|
+
: undefined,
|
|
5159
5294
|
});
|
|
5160
5295
|
} finally {
|
|
5161
5296
|
// Clean up residual eager-todo directive if the prompt never consumed it
|
|
@@ -5194,13 +5329,13 @@ export class AgentSession {
|
|
|
5194
5329
|
if (!options?.streamingBehavior) {
|
|
5195
5330
|
throw new AgentBusyError();
|
|
5196
5331
|
}
|
|
5332
|
+
for (const notice of keywordNotices) {
|
|
5333
|
+
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5334
|
+
}
|
|
5197
5335
|
await this.sendCustomMessage(message, {
|
|
5198
5336
|
deliverAs: options.streamingBehavior,
|
|
5199
5337
|
queueChipText: options.queueChipText,
|
|
5200
5338
|
});
|
|
5201
|
-
for (const notice of keywordNotices) {
|
|
5202
|
-
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5203
|
-
}
|
|
5204
5339
|
return;
|
|
5205
5340
|
}
|
|
5206
5341
|
|
|
@@ -5216,7 +5351,7 @@ export class AgentSession {
|
|
|
5216
5351
|
|
|
5217
5352
|
await this.#promptWithMessage(customMessage, textContent, {
|
|
5218
5353
|
...options,
|
|
5219
|
-
|
|
5354
|
+
prependMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
5220
5355
|
});
|
|
5221
5356
|
}
|
|
5222
5357
|
|
|
@@ -5225,7 +5360,6 @@ export class AgentSession {
|
|
|
5225
5360
|
expandedText: string,
|
|
5226
5361
|
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
|
|
5227
5362
|
prependMessages?: AgentMessage[];
|
|
5228
|
-
appendMessages?: AgentMessage[];
|
|
5229
5363
|
skipPostPromptRecoveryWait?: boolean;
|
|
5230
5364
|
},
|
|
5231
5365
|
): Promise<void> {
|
|
@@ -5292,12 +5426,6 @@ export class AgentSession {
|
|
|
5292
5426
|
|
|
5293
5427
|
messages.push(message);
|
|
5294
5428
|
|
|
5295
|
-
// Inject the ultrathink notice (and any other per-turn appends) right after the
|
|
5296
|
-
// user message so the model reads it as part of the same turn.
|
|
5297
|
-
if (options?.appendMessages) {
|
|
5298
|
-
messages.push(...options.appendMessages);
|
|
5299
|
-
}
|
|
5300
|
-
|
|
5301
5429
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
5302
5430
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
5303
5431
|
if (this.#promptGeneration !== generation) {
|
|
@@ -5619,12 +5747,24 @@ export class AgentSession {
|
|
|
5619
5747
|
#canAutoContinueForFollowUp(): boolean {
|
|
5620
5748
|
if (this.isStreaming) return false;
|
|
5621
5749
|
if (this.isRetrying) return false;
|
|
5750
|
+
// A queued steer resumes from ANY tail: Agent.continue() runs #runLoop(undefined),
|
|
5751
|
+
// whose initial steering poll injects the steer before the first provider call, so the
|
|
5752
|
+
// request tail becomes the steer (valid) regardless of any injected custom / bashExecution
|
|
5753
|
+
// / pythonExecution record a user interrupt left as the literal transcript tail. This is
|
|
5754
|
+
// why a queued user steer stranded behind a preserved advisor card (or a flushed IRC aside
|
|
5755
|
+
// / eval execution record) still resumes — no tail-role enumeration needed.
|
|
5756
|
+
if (this.agent.peekSteeringQueue().length > 0) return true;
|
|
5757
|
+
// Follow-up-only auto-resume stays suppressed while a deliberate user interrupt is in effect
|
|
5758
|
+
// (#advisorAutoResumeSuppressed, cleared on the next user prompt): the user stopped, so their
|
|
5759
|
+
// queued follow-up waits for an explicit resume — even if an interleaving IRC wake turn has
|
|
5760
|
+
// since left a provider-valid tail.
|
|
5761
|
+
if (this.#advisorAutoResumeSuppressed) return false;
|
|
5762
|
+
// Follow-up-only resume has no steer to inject, so Agent.continue() continues from the
|
|
5763
|
+
// existing context tail — which must itself be a valid provider tail. An injected
|
|
5764
|
+
// non-conversational tail (advisor card → `developer`, bash/python execution) would make
|
|
5765
|
+
// the first model call invalid, so leave the follow-up queued for the next explicit resume.
|
|
5622
5766
|
const messages = this.agent.state.messages;
|
|
5623
5767
|
const last = messages[messages.length - 1];
|
|
5624
|
-
// A user interrupt during tool execution can leave the transcript ending
|
|
5625
|
-
// with the emitted tool result, not the aborted assistant message. Continuing
|
|
5626
|
-
// from that state is still resumable: Agent.continue() first polls queued
|
|
5627
|
-
// steering before making the next model call.
|
|
5628
5768
|
return last?.role === "assistant" || last?.role === "toolResult";
|
|
5629
5769
|
}
|
|
5630
5770
|
|
|
@@ -5857,15 +5997,32 @@ export class AgentSession {
|
|
|
5857
5997
|
});
|
|
5858
5998
|
}
|
|
5859
5999
|
|
|
5860
|
-
/** Clear queued messages and return
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
6000
|
+
/** Clear queued messages and return the user-restorable ones (text plus any attached images).
|
|
6001
|
+
* Only user-authored messages (plain user turns, `attribution:"user"` custom like `/skill`) are
|
|
6002
|
+
* returned for editor restore. Other queued messages stay in the agent-core queues so a continuing
|
|
6003
|
+
* stream still delivers them — EXCEPT on `forInterrupt` (Esc+abort), where only advisor cards are
|
|
6004
|
+
* kept (abort()'s #extractQueuedAdvisorCards preserves them as visible advice) and every other
|
|
6005
|
+
* non-user steer (hidden goal/plan/budget, IRC/extension asides) is dropped, so abort()'s
|
|
6006
|
+
* #drainStrandedQueuedMessages can't auto-resume the run the user just interrupted (the drain only
|
|
6007
|
+
* fires while agent.hasQueuedMessages()). Plain Alt+Up dequeue preserves those non-user steers. */
|
|
6008
|
+
clearQueue(options?: { forInterrupt?: boolean }): {
|
|
6009
|
+
steering: RestoredQueuedMessage[];
|
|
6010
|
+
followUp: RestoredQueuedMessage[];
|
|
6011
|
+
} {
|
|
6012
|
+
const steeringAll = this.agent.peekSteeringQueue();
|
|
6013
|
+
const followUpAll = this.agent.peekFollowUpQueue();
|
|
6014
|
+
const steering = steeringAll.filter(isUserQueuedMessage).map(toRestoredQueuedMessage);
|
|
6015
|
+
const followUp = followUpAll.filter(isUserQueuedMessage).map(toRestoredQueuedMessage);
|
|
6016
|
+
const keep: (m: AgentMessage) => boolean = options?.forInterrupt
|
|
6017
|
+
? isAdvisorCard
|
|
6018
|
+
: m => !isUserQueuedMessage(m) && !isHiddenUserCompanion(m);
|
|
6019
|
+
this.agent.replaceQueues(steeringAll.filter(keep), followUpAll.filter(keep));
|
|
5865
6020
|
return { steering, followUp };
|
|
5866
6021
|
}
|
|
5867
6022
|
|
|
5868
|
-
/** Number of pending displayable messages (includes steering, follow-up, and next-turn messages)
|
|
6023
|
+
/** Number of pending displayable messages (includes steering, follow-up, and next-turn messages).
|
|
6024
|
+
* Reflects actual queued work (advisor cards included) — feeds hasPendingMessages()/RPC and the
|
|
6025
|
+
* empty-submit abort gate. The user-restorable subset is surfaced by getQueuedMessages()/clearQueue(). */
|
|
5869
6026
|
get queuedMessageCount(): number {
|
|
5870
6027
|
return (
|
|
5871
6028
|
this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).length +
|
|
@@ -5876,18 +6033,48 @@ export class AgentSession {
|
|
|
5876
6033
|
|
|
5877
6034
|
getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
|
|
5878
6035
|
return {
|
|
5879
|
-
steering: this.agent.peekSteeringQueue().filter(
|
|
5880
|
-
followUp: this.agent.peekFollowUpQueue().filter(
|
|
6036
|
+
steering: this.agent.peekSteeringQueue().filter(isUserQueuedMessage).map(queueChipText),
|
|
6037
|
+
followUp: this.agent.peekFollowUpQueue().filter(isUserQueuedMessage).map(queueChipText),
|
|
5881
6038
|
};
|
|
5882
6039
|
}
|
|
5883
6040
|
|
|
5884
6041
|
/**
|
|
5885
6042
|
* Pop the last queued message (steering first, then follow-up).
|
|
5886
6043
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
6044
|
+
* Steps over agent-authored queued messages (advisor cards, hidden/internal steers).
|
|
5887
6045
|
*/
|
|
5888
6046
|
popLastQueuedMessage(): RestoredQueuedMessage | undefined {
|
|
5889
|
-
const
|
|
5890
|
-
|
|
6047
|
+
const steering = this.agent.peekSteeringQueue();
|
|
6048
|
+
const followUp = this.agent.peekFollowUpQueue();
|
|
6049
|
+
const lastUserIndex = (queue: readonly AgentMessage[]): number => {
|
|
6050
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
6051
|
+
if (isUserQueuedMessage(queue[i])) return i;
|
|
6052
|
+
}
|
|
6053
|
+
return -1;
|
|
6054
|
+
};
|
|
6055
|
+
// Notices queue immediately before their user message, so dropping the popped
|
|
6056
|
+
// prompt means also dropping the contiguous hidden-user companions right before
|
|
6057
|
+
// it — companions of other queued prompts stay put.
|
|
6058
|
+
const removeWithCompanions = (queue: readonly AgentMessage[], userIndex: number): AgentMessage[] => {
|
|
6059
|
+
let start = userIndex;
|
|
6060
|
+
while (start > 0 && isHiddenUserCompanion(queue[start - 1])) start--;
|
|
6061
|
+
const next = queue.slice();
|
|
6062
|
+
next.splice(start, userIndex - start + 1);
|
|
6063
|
+
return next;
|
|
6064
|
+
};
|
|
6065
|
+
const fromSteer = lastUserIndex(steering);
|
|
6066
|
+
if (fromSteer >= 0) {
|
|
6067
|
+
const removed = steering[fromSteer];
|
|
6068
|
+
this.agent.replaceQueues(removeWithCompanions(steering, fromSteer), followUp.slice());
|
|
6069
|
+
return toRestoredQueuedMessage(removed);
|
|
6070
|
+
}
|
|
6071
|
+
const fromFollowUp = lastUserIndex(followUp);
|
|
6072
|
+
if (fromFollowUp >= 0) {
|
|
6073
|
+
const removed = followUp[fromFollowUp];
|
|
6074
|
+
this.agent.replaceQueues(steering.slice(), removeWithCompanions(followUp, fromFollowUp));
|
|
6075
|
+
return toRestoredQueuedMessage(removed);
|
|
6076
|
+
}
|
|
6077
|
+
return undefined;
|
|
5891
6078
|
}
|
|
5892
6079
|
|
|
5893
6080
|
get skillsSettings(): SkillsSettings | undefined {
|
|
@@ -7774,7 +7961,9 @@ export class AgentSession {
|
|
|
7774
7961
|
};
|
|
7775
7962
|
}
|
|
7776
7963
|
|
|
7777
|
-
#createEagerTodoPrelude(
|
|
7964
|
+
#createEagerTodoPrelude(
|
|
7965
|
+
promptText: string | undefined,
|
|
7966
|
+
): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
|
|
7778
7967
|
const mode = this.settings.get("todo.eager");
|
|
7779
7968
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
7780
7969
|
if (mode === "default" || !todosEnabled) {
|
|
@@ -7791,14 +7980,18 @@ export class AgentSession {
|
|
|
7791
7980
|
// Only inject on the first user message of the conversation. Subsequent user
|
|
7792
7981
|
// turns must not receive the eager todo reminder — they often correct, clarify,
|
|
7793
7982
|
// or redirect the prior task, and forcing a brand-new todo list there is wrong.
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7983
|
+
// When `promptText` is undefined (post-compaction re-injection) there is no fresh
|
|
7984
|
+
// user message to gate on, so skip the first-message and prompt-suffix checks.
|
|
7985
|
+
if (promptText !== undefined) {
|
|
7986
|
+
const hasPriorUserMessage = this.agent.state.messages.some(m => m.role === "user");
|
|
7987
|
+
if (hasPriorUserMessage) {
|
|
7988
|
+
return undefined;
|
|
7989
|
+
}
|
|
7798
7990
|
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7991
|
+
const trimmedPromptText = promptText.trimEnd();
|
|
7992
|
+
if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
|
|
7993
|
+
return undefined;
|
|
7994
|
+
}
|
|
7802
7995
|
}
|
|
7803
7996
|
|
|
7804
7997
|
// Must check the active tool set, not just the registry: tool discovery
|
|
@@ -7821,8 +8014,10 @@ export class AgentSession {
|
|
|
7821
8014
|
timestamp: Date.now(),
|
|
7822
8015
|
};
|
|
7823
8016
|
// `preferred` suggests a todo list (reminder only); `always` also forces the
|
|
7824
|
-
// `todo` tool on the first turn — the previous boolean-on behavior.
|
|
7825
|
-
|
|
8017
|
+
// `todo` tool on the first turn — the previous boolean-on behavior. Post-compaction
|
|
8018
|
+
// re-injection (`promptText === undefined`) is always reminder-only: forcing a tool
|
|
8019
|
+
// onto the auto-resumed turn would override the agent's in-flight action.
|
|
8020
|
+
if (promptText === undefined || mode === "preferred") {
|
|
7826
8021
|
return { message };
|
|
7827
8022
|
}
|
|
7828
8023
|
const todoToolChoice = buildNamedToolChoice("todo", this.model);
|
|
@@ -7840,7 +8035,7 @@ export class AgentSession {
|
|
|
7840
8035
|
return { message, toolChoice: todoToolChoice };
|
|
7841
8036
|
}
|
|
7842
8037
|
|
|
7843
|
-
#createEagerTaskPrelude(promptText: string): AgentMessage | undefined {
|
|
8038
|
+
#createEagerTaskPrelude(promptText: string | undefined): AgentMessage | undefined {
|
|
7844
8039
|
if (this.settings.get("task.eager") !== "always") return undefined;
|
|
7845
8040
|
// Main agent only: subagents keep `task` active (the parent only filters `todo`),
|
|
7846
8041
|
// so a salient delegate-reminder there would amplify nested fan-out. Gate on the
|
|
@@ -7848,9 +8043,13 @@ export class AgentSession {
|
|
|
7848
8043
|
// still gets the reminder.
|
|
7849
8044
|
if (this.#agentKind === "sub") return undefined;
|
|
7850
8045
|
if (this.#planModeState?.enabled) return undefined;
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
if (
|
|
8046
|
+
// First-message-only gates are skipped post-compaction (`promptText === undefined`),
|
|
8047
|
+
// where there is no fresh user message to suppress the reminder for.
|
|
8048
|
+
if (promptText !== undefined) {
|
|
8049
|
+
if (this.agent.state.messages.some(m => m.role === "user")) return undefined;
|
|
8050
|
+
const trimmed = promptText.trimEnd();
|
|
8051
|
+
if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
|
|
8052
|
+
}
|
|
7854
8053
|
if (!this.getActiveToolNames().includes("task")) return undefined;
|
|
7855
8054
|
return {
|
|
7856
8055
|
role: "custom",
|
|
@@ -7861,6 +8060,24 @@ export class AgentSession {
|
|
|
7861
8060
|
timestamp: Date.now(),
|
|
7862
8061
|
};
|
|
7863
8062
|
}
|
|
8063
|
+
|
|
8064
|
+
/**
|
|
8065
|
+
* Build the eager task/todo reminders to re-inject on the auto-continuation turn that
|
|
8066
|
+
* follows a compaction. The first-message preludes are the oldest messages, so
|
|
8067
|
+
* compaction summarizes them away and the agent silently loses the delegate-via-tasks
|
|
8068
|
+
* and phased-todo guidance mid-work; this re-asserts them, reminder-only (the todo
|
|
8069
|
+
* builder drops its forced tool_choice when `promptText` is undefined). Each builder
|
|
8070
|
+
* still applies its own mode / agent-kind / plan-mode / tool-active / surviving-todo
|
|
8071
|
+
* gates, so an empty array means nothing currently warrants a nudge.
|
|
8072
|
+
*/
|
|
8073
|
+
#buildPostCompactionEagerNudges(): AgentMessage[] {
|
|
8074
|
+
const nudges: AgentMessage[] = [];
|
|
8075
|
+
const todo = this.#createEagerTodoPrelude(undefined);
|
|
8076
|
+
if (todo) nudges.push(todo.message);
|
|
8077
|
+
const task = this.#createEagerTaskPrelude(undefined);
|
|
8078
|
+
if (task) nudges.push(task);
|
|
8079
|
+
return nudges;
|
|
8080
|
+
}
|
|
7864
8081
|
/**
|
|
7865
8082
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
7866
8083
|
*/
|
|
@@ -9136,9 +9353,31 @@ export class AgentSession {
|
|
|
9136
9353
|
// Auto-Retry
|
|
9137
9354
|
// =========================================================================
|
|
9138
9355
|
|
|
9356
|
+
/**
|
|
9357
|
+
* Retry an empty, reason-less provider abort: a turn that ended `aborted`
|
|
9358
|
+
* with no content and the generic sentinel (bare `abort()`), but only while
|
|
9359
|
+
* the session is neither aborting nor tearing down. A user/lifecycle abort
|
|
9360
|
+
* (`#abortInProgress`), a dispose-driven abort (`#isDisposed`), or a
|
|
9361
|
+
* session-induced streaming-edit guard abort (`#streamingEditAbortTriggered` —
|
|
9362
|
+
* auto-generated-file guard or failed-patch preview) is deliberate and MUST
|
|
9363
|
+
* settle the turn instead: routing it through retry would orphan
|
|
9364
|
+
* `#retryPromise` on a continuation the guard skips (hanging the in-flight
|
|
9365
|
+
* `prompt()`) or silently undo the guard's intended abort.
|
|
9366
|
+
*/
|
|
9367
|
+
#isRetryableReasonlessAbort(message: AssistantMessage): boolean {
|
|
9368
|
+
return (
|
|
9369
|
+
message.stopReason === "aborted" &&
|
|
9370
|
+
message.content.length === 0 &&
|
|
9371
|
+
message.errorMessage === GENERIC_ABORT_SENTINEL &&
|
|
9372
|
+
!this.#abortInProgress &&
|
|
9373
|
+
!this.#isDisposed &&
|
|
9374
|
+
!this.#streamingEditAbortTriggered
|
|
9375
|
+
);
|
|
9376
|
+
}
|
|
9377
|
+
|
|
9139
9378
|
/**
|
|
9140
9379
|
* Check if an error is retryable (transient errors or usage limits).
|
|
9141
|
-
* Context overflow
|
|
9380
|
+
* Context overflow is NOT retryable (handled by compaction instead).
|
|
9142
9381
|
* Usage-limit errors are retryable because the retry handler performs credential switching.
|
|
9143
9382
|
*/
|
|
9144
9383
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
@@ -9295,11 +9534,25 @@ export class AgentSession {
|
|
|
9295
9534
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9296
9535
|
if (!parsedCurrent) return undefined;
|
|
9297
9536
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
9537
|
+
const currentPlainSelector = this.model
|
|
9538
|
+
? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
|
|
9539
|
+
: undefined;
|
|
9540
|
+
const currentPlainBaseSelector =
|
|
9541
|
+
currentPlainSelector && currentPlainSelector !== currentSelector
|
|
9542
|
+
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
9543
|
+
: undefined;
|
|
9544
|
+
|
|
9545
|
+
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
9546
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9547
|
+
if (primarySelector?.raw === currentSelector) return role;
|
|
9548
|
+
}
|
|
9298
9549
|
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
9299
9550
|
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9300
9551
|
if (!primarySelector) continue;
|
|
9301
|
-
if (primarySelector.raw ===
|
|
9302
|
-
|
|
9552
|
+
if (currentPlainSelector && primarySelector.raw === currentPlainSelector) return role;
|
|
9553
|
+
const primaryBaseSelector = formatRetryFallbackBaseSelector(primarySelector);
|
|
9554
|
+
if (primaryBaseSelector === currentBaseSelector) return role;
|
|
9555
|
+
if (currentPlainBaseSelector && primaryBaseSelector === currentPlainBaseSelector) return role;
|
|
9303
9556
|
}
|
|
9304
9557
|
return undefined;
|
|
9305
9558
|
}
|
|
@@ -9323,10 +9576,23 @@ export class AgentSession {
|
|
|
9323
9576
|
if (chain.length <= 1) return [];
|
|
9324
9577
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9325
9578
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
9326
|
-
const
|
|
9579
|
+
const currentPlainSelector =
|
|
9580
|
+
this.model && parsedCurrent
|
|
9581
|
+
? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
|
|
9582
|
+
: undefined;
|
|
9583
|
+
const currentPlainBaseSelector =
|
|
9584
|
+
parsedCurrent && currentPlainSelector && currentPlainSelector !== currentSelector
|
|
9585
|
+
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
9586
|
+
: undefined;
|
|
9587
|
+
const exactIndex = chain.findIndex(
|
|
9588
|
+
selector => selector.raw === currentSelector || selector.raw === currentPlainSelector,
|
|
9589
|
+
);
|
|
9327
9590
|
if (exactIndex >= 0) return chain.slice(exactIndex + 1);
|
|
9328
9591
|
const baseIndex = currentBaseSelector
|
|
9329
|
-
? chain.findIndex(selector =>
|
|
9592
|
+
? chain.findIndex(selector => {
|
|
9593
|
+
const selectorBase = formatRetryFallbackBaseSelector(selector);
|
|
9594
|
+
return selectorBase === currentBaseSelector || selectorBase === currentPlainBaseSelector;
|
|
9595
|
+
})
|
|
9330
9596
|
: -1;
|
|
9331
9597
|
if (baseIndex >= 0) return chain.slice(baseIndex + 1);
|
|
9332
9598
|
return chain.slice(1);
|
|
@@ -9338,7 +9604,8 @@ export class AgentSession {
|
|
|
9338
9604
|
currentSelector: string,
|
|
9339
9605
|
options?: { pinFallback?: boolean },
|
|
9340
9606
|
): Promise<void> {
|
|
9341
|
-
const
|
|
9607
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
9608
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
9342
9609
|
if (!candidate) {
|
|
9343
9610
|
throw new Error(`Retry fallback model not found: ${selector.raw}`);
|
|
9344
9611
|
}
|
|
@@ -9351,10 +9618,10 @@ export class AgentSession {
|
|
|
9351
9618
|
// `auto` instead of collapsing it to the level it resolved to this turn.
|
|
9352
9619
|
const currentThinkingLevel = this.configuredThinkingLevel();
|
|
9353
9620
|
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
9354
|
-
|
|
9621
|
+
const candidateSelector = formatModelStringWithRouting(candidate);
|
|
9355
9622
|
this.#setModelWithProviderSessionReset(candidate);
|
|
9356
|
-
this.sessionManager.appendModelChange(
|
|
9357
|
-
this.settings.getStorage()?.recordModelUsage(
|
|
9623
|
+
this.sessionManager.appendModelChange(candidateSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
9624
|
+
this.settings.getStorage()?.recordModelUsage(candidateSelector);
|
|
9358
9625
|
this.setThinkingLevel(nextThinkingLevel);
|
|
9359
9626
|
if (!this.#activeRetryFallback) {
|
|
9360
9627
|
this.#activeRetryFallback = {
|
|
@@ -9382,7 +9649,8 @@ export class AgentSession {
|
|
|
9382
9649
|
|
|
9383
9650
|
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector)) {
|
|
9384
9651
|
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
9385
|
-
const
|
|
9652
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
9653
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
9386
9654
|
if (!candidate) continue;
|
|
9387
9655
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
9388
9656
|
if (!apiKey) continue;
|
|
@@ -9420,7 +9688,9 @@ export class AgentSession {
|
|
|
9420
9688
|
}
|
|
9421
9689
|
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
9422
9690
|
|
|
9423
|
-
const
|
|
9691
|
+
const resolvedPrimary = resolveModelOverride([originalSelector.raw], this.#modelRegistry, this.settings);
|
|
9692
|
+
const primaryModel =
|
|
9693
|
+
resolvedPrimary.model ?? this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
9424
9694
|
if (!primaryModel) return;
|
|
9425
9695
|
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
|
|
9426
9696
|
if (!apiKey) return;
|
|
@@ -9428,9 +9698,10 @@ export class AgentSession {
|
|
|
9428
9698
|
const currentThinkingLevel = this.configuredThinkingLevel();
|
|
9429
9699
|
const thinkingToApply =
|
|
9430
9700
|
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
9701
|
+
const primarySelector = formatModelStringWithRouting(primaryModel);
|
|
9431
9702
|
this.#setModelWithProviderSessionReset(primaryModel);
|
|
9432
|
-
this.sessionManager.appendModelChange(
|
|
9433
|
-
this.settings.getStorage()?.recordModelUsage(
|
|
9703
|
+
this.sessionManager.appendModelChange(primarySelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
9704
|
+
this.settings.getStorage()?.recordModelUsage(primarySelector);
|
|
9434
9705
|
this.setThinkingLevel(thinkingToApply);
|
|
9435
9706
|
this.#clearActiveRetryFallback();
|
|
9436
9707
|
}
|
|
@@ -9490,7 +9761,10 @@ export class AgentSession {
|
|
|
9490
9761
|
* Handle retryable errors with exponential backoff.
|
|
9491
9762
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
9492
9763
|
*/
|
|
9493
|
-
async #handleRetryableError(
|
|
9764
|
+
async #handleRetryableError(
|
|
9765
|
+
message: AssistantMessage,
|
|
9766
|
+
options?: { allowModelFallback?: boolean },
|
|
9767
|
+
): Promise<boolean> {
|
|
9494
9768
|
const retrySettings = this.settings.getGroup("retry");
|
|
9495
9769
|
if (!retrySettings.enabled) return false;
|
|
9496
9770
|
const classifierRefusal = this.#isClassifierRefusal(message);
|
|
@@ -9578,9 +9852,10 @@ export class AgentSession {
|
|
|
9578
9852
|
}
|
|
9579
9853
|
}
|
|
9580
9854
|
|
|
9855
|
+
const allowModelFallback = options?.allowModelFallback !== false;
|
|
9581
9856
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
9582
9857
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
9583
|
-
if (retrySettings.modelFallback) {
|
|
9858
|
+
if (allowModelFallback && retrySettings.modelFallback) {
|
|
9584
9859
|
if (!classifierRefusal) {
|
|
9585
9860
|
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
9586
9861
|
}
|
|
@@ -10089,11 +10364,8 @@ export class AgentSession {
|
|
|
10089
10364
|
if (autoReply) void this.#runIrcAutoReply(msg);
|
|
10090
10365
|
return "injected";
|
|
10091
10366
|
}
|
|
10092
|
-
// Idle:
|
|
10093
|
-
|
|
10094
|
-
this.agent.prompt(record).catch(error => {
|
|
10095
|
-
logger.warn("IRC wake turn failed", { from: msg.from, to: msg.to, error: String(error) });
|
|
10096
|
-
});
|
|
10367
|
+
// Idle: wake a real turn so the recipient responds (shared with the stranded-aside resume).
|
|
10368
|
+
this.#wakeForIrc([record]);
|
|
10097
10369
|
return "woken";
|
|
10098
10370
|
}
|
|
10099
10371
|
|
|
@@ -11278,13 +11550,12 @@ export class AgentSession {
|
|
|
11278
11550
|
}
|
|
11279
11551
|
|
|
11280
11552
|
/**
|
|
11281
|
-
* Format the entire session as plain text for clipboard export
|
|
11282
|
-
*
|
|
11553
|
+
* Format the entire session as plain text for clipboard export: system
|
|
11554
|
+
* prompt, model/thinking config, tool inventory, and the full transcript
|
|
11555
|
+
* rendered with markdown role headings (`## User`, `## Assistant`,
|
|
11556
|
+
* `### Tool Call`/`### Tool Result`).
|
|
11283
11557
|
*/
|
|
11284
|
-
formatSessionAsText(
|
|
11285
|
-
if (options?.compact) {
|
|
11286
|
-
return formatSessionHistoryMarkdown(this.messages);
|
|
11287
|
-
}
|
|
11558
|
+
formatSessionAsText(): string {
|
|
11288
11559
|
return formatSessionDumpText({
|
|
11289
11560
|
messages: this.messages,
|
|
11290
11561
|
systemPrompt: this.agent.state.systemPrompt,
|