@oh-my-pi/pi-coding-agent 16.0.0 → 16.0.2
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 +140 -133
- package/dist/cli.js +250 -218
- package/dist/types/config/model-resolver.d.ts +14 -0
- package/dist/types/config/settings-schema.d.ts +22 -0
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/task/index.d.ts +21 -0
- package/dist/types/tools/github-cache.d.ts +5 -4
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/utils/markit.d.ts +8 -0
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +44 -0
- package/src/cli/args.ts +2 -0
- package/src/collab/host.ts +1 -1
- package/src/config/model-resolver.ts +35 -1
- package/src/config/settings-schema.ts +23 -1
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +189 -6
- package/src/discovery/helpers.ts +11 -0
- package/src/eval/__tests__/prelude-agent.test.ts +73 -0
- package/src/eval/js/shared/prelude.txt +12 -3
- package/src/eval/py/prelude.py +26 -2
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +157 -23
- package/src/extensibility/plugins/manager.ts +44 -36
- package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +5 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +7 -0
- package/src/modes/interactive-mode.ts +47 -0
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +5 -0
- package/src/prompts/agents/designer.md +8 -0
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +4 -1
- package/src/prompts/tools/eval.md +13 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +260 -50
- package/src/session/messages.ts +1 -1
- package/src/session/session-manager.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +5 -2
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +105 -8
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type AsideMessage,
|
|
33
33
|
type CompactionSummaryMessage,
|
|
34
34
|
resolveTelemetry,
|
|
35
|
+
STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL,
|
|
35
36
|
ThinkingLevel,
|
|
36
37
|
} from "@oh-my-pi/pi-agent-core";
|
|
37
38
|
|
|
@@ -138,9 +139,11 @@ import {
|
|
|
138
139
|
filterAvailableModelsByEnabledPatterns,
|
|
139
140
|
formatModelSelectorValue,
|
|
140
141
|
formatModelString,
|
|
142
|
+
formatModelStringWithRouting,
|
|
141
143
|
getModelMatchPreferences,
|
|
142
144
|
parseModelString,
|
|
143
145
|
type ResolvedModelRoleValue,
|
|
146
|
+
resolveModelOverride,
|
|
144
147
|
resolveModelRoleValue,
|
|
145
148
|
resolveRoleSelection,
|
|
146
149
|
} from "../config/model-resolver";
|
|
@@ -272,11 +275,13 @@ import {
|
|
|
272
275
|
type BashExecutionMessage,
|
|
273
276
|
type CustomMessage,
|
|
274
277
|
convertToLlm,
|
|
278
|
+
GENERIC_ABORT_SENTINEL,
|
|
275
279
|
type PythonExecutionMessage,
|
|
276
280
|
readQueueChipText,
|
|
277
281
|
SILENT_ABORT_MARKER,
|
|
278
282
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
279
283
|
stripImagesFromMessage,
|
|
284
|
+
USER_INTERRUPT_LABEL,
|
|
280
285
|
} from "./messages";
|
|
281
286
|
import type { SessionContext } from "./session-context";
|
|
282
287
|
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
|
|
@@ -496,6 +501,12 @@ export interface PromptOptions {
|
|
|
496
501
|
toolChoice?: ToolChoice;
|
|
497
502
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
498
503
|
synthetic?: boolean;
|
|
504
|
+
/** Marks this prompt as a deliberate user action (typed message, `.`/`c`
|
|
505
|
+
* continue). Clears advisor auto-resume suppression that a user interrupt set.
|
|
506
|
+
* Defaults to `!synthetic`; manual-continue is synthetic yet user-initiated, so
|
|
507
|
+
* it sets this explicitly. Agent-initiated synthetic prompts (auto-continue,
|
|
508
|
+
* plan re-prime, reminders) leave it unset and keep suppression latched. */
|
|
509
|
+
userInitiated?: boolean;
|
|
499
510
|
/** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */
|
|
500
511
|
attribution?: MessageAttribution;
|
|
501
512
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
@@ -632,8 +643,7 @@ function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | u
|
|
|
632
643
|
}
|
|
633
644
|
|
|
634
645
|
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
635
|
-
|
|
636
|
-
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
|
646
|
+
return formatModelSelectorValue(formatModelStringWithRouting(model), thinkingLevel);
|
|
637
647
|
}
|
|
638
648
|
|
|
639
649
|
function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): string {
|
|
@@ -941,6 +951,10 @@ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
|
|
|
941
951
|
return !(message.role === "custom" && message.display === false);
|
|
942
952
|
}
|
|
943
953
|
|
|
954
|
+
function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
955
|
+
return message.role === "custom" && message.customType === "advisor";
|
|
956
|
+
}
|
|
957
|
+
|
|
944
958
|
function queueChipText(message: AgentMessage): string {
|
|
945
959
|
if (message.role === "custom") {
|
|
946
960
|
return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
|
|
@@ -988,10 +1002,15 @@ export class AgentSession {
|
|
|
988
1002
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
989
1003
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
990
1004
|
#queuedMessageDrainScheduled = false;
|
|
1005
|
+
/** Latched true when the user deliberately interrupts (USER_INTERRUPT_LABEL);
|
|
1006
|
+
* suppresses advisor concern/blocker auto-resume until the user next resumes.
|
|
1007
|
+
* Advisor advice is still recorded into the transcript, just not auto-run. */
|
|
1008
|
+
#advisorAutoResumeSuppressed = false;
|
|
991
1009
|
#planModeState: PlanModeState | undefined;
|
|
992
1010
|
#goalModeState: GoalModeState | undefined;
|
|
993
1011
|
#goalRuntime: GoalRuntime;
|
|
994
1012
|
#advisorRuntime?: AdvisorRuntime;
|
|
1013
|
+
#advisorEnabled = false;
|
|
995
1014
|
/** The advisor's own agent, retained so `/dump advisor` can serialize its transcript. Undefined when no advisor is active. */
|
|
996
1015
|
#advisorAgent?: Agent;
|
|
997
1016
|
#advisorReadOnlyTools?: AgentTool[];
|
|
@@ -1244,6 +1263,39 @@ export class AgentSession {
|
|
|
1244
1263
|
this.#scheduleQueuedMessageDrain();
|
|
1245
1264
|
}
|
|
1246
1265
|
|
|
1266
|
+
/** Remove advisor concern/blocker cards from the agent-core steer/follow-up
|
|
1267
|
+
* queues and return them. Used on a deliberate user interrupt so the post-abort
|
|
1268
|
+
* stranded-message drain cannot auto-resume the run on an advisor card that was
|
|
1269
|
+
* steered in just before the user stopped; real user follow-ups stay queued.
|
|
1270
|
+
* Synchronous and await-free so it runs before the abort path polls the queue. */
|
|
1271
|
+
#extractQueuedAdvisorCards(): CustomMessage[] {
|
|
1272
|
+
const steering = this.agent.peekSteeringQueue();
|
|
1273
|
+
const followUp = this.agent.peekFollowUpQueue();
|
|
1274
|
+
const cards = [...steering, ...followUp].filter(isAdvisorCard);
|
|
1275
|
+
if (cards.length === 0) return [];
|
|
1276
|
+
this.agent.replaceQueues(
|
|
1277
|
+
steering.filter(m => !isAdvisorCard(m)),
|
|
1278
|
+
followUp.filter(m => !isAdvisorCard(m)),
|
|
1279
|
+
);
|
|
1280
|
+
return cards;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** Record a suppressed advisor concern as visible, persisted advice without
|
|
1284
|
+
* triggering a turn. When the agent is idle (the normal post-interrupt case),
|
|
1285
|
+
* emit message_start/message_end like #flushPendingIrcAsides so
|
|
1286
|
+
* #handleAgentEvent renders it live (TUI/ACP) and persists it as a
|
|
1287
|
+
* CustomMessageEntry. While a turn is still tearing down (mid-abort), park it
|
|
1288
|
+
* hidden so abort's settle step replays it once idle — never appended into a
|
|
1289
|
+
* live streamMessage. */
|
|
1290
|
+
#preserveAdvisorCard(card: CustomMessage): void {
|
|
1291
|
+
if (this.isStreaming) {
|
|
1292
|
+
this.#pendingNextTurnMessages.push(card);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
this.agent.emitExternalEvent({ type: "message_start", message: card });
|
|
1296
|
+
this.agent.emitExternalEvent({ type: "message_end", message: card });
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1247
1299
|
#resetInFlight(): void {
|
|
1248
1300
|
this.#promptInFlightCount = 0;
|
|
1249
1301
|
this.#releasePowerAssertion();
|
|
@@ -1443,7 +1495,8 @@ export class AgentSession {
|
|
|
1443
1495
|
},
|
|
1444
1496
|
});
|
|
1445
1497
|
|
|
1446
|
-
|
|
1498
|
+
this.#advisorEnabled = this.settings.get("advisor.enabled") as boolean;
|
|
1499
|
+
if (this.#advisorEnabled) this.#buildAdvisorRuntime();
|
|
1447
1500
|
|
|
1448
1501
|
// Always subscribe to agent events for internal handling
|
|
1449
1502
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
@@ -1457,7 +1510,7 @@ export class AgentSession {
|
|
|
1457
1510
|
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1458
1511
|
if (this.#isDisposed) return false;
|
|
1459
1512
|
if (this.#advisorRuntime) return true;
|
|
1460
|
-
if (!this
|
|
1513
|
+
if (!this.#advisorEnabled) return false;
|
|
1461
1514
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1462
1515
|
|
|
1463
1516
|
const advisorSel = resolveRoleSelection(
|
|
@@ -1472,23 +1525,33 @@ export class AgentSession {
|
|
|
1472
1525
|
}
|
|
1473
1526
|
|
|
1474
1527
|
// Concern and blocker interrupt the running agent through the steering
|
|
1475
|
-
// channel (aborting in-flight tools at the next steering boundary); when
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1528
|
+
// channel (aborting in-flight tools at the next steering boundary); when the
|
|
1529
|
+
// loop has already yielded, triggerTurn resumes it so the advice is acted on
|
|
1530
|
+
// immediately rather than waiting for the next user prompt. After a deliberate
|
|
1531
|
+
// user interrupt that auto-resume is suppressed: the concern is recorded as
|
|
1532
|
+
// visible advice and re-enters context only when the user resumes. A plain nit
|
|
1533
|
+
// rides the non-interrupting YieldQueue aside.
|
|
1479
1534
|
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1480
1535
|
if (isInterruptingSeverity(severity)) {
|
|
1481
1536
|
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1482
|
-
|
|
1483
|
-
|
|
1537
|
+
const content = formatAdvisorBatchContent(notes);
|
|
1538
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
1539
|
+
if (this.#advisorAutoResumeSuppressed) {
|
|
1540
|
+
this.#preserveAdvisorCard({
|
|
1541
|
+
role: "custom",
|
|
1484
1542
|
customType: "advisor",
|
|
1485
|
-
content
|
|
1543
|
+
content,
|
|
1486
1544
|
display: true,
|
|
1487
1545
|
attribution: "agent",
|
|
1488
|
-
details
|
|
1489
|
-
|
|
1546
|
+
details,
|
|
1547
|
+
timestamp: Date.now(),
|
|
1548
|
+
});
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
void this.sendCustomMessage(
|
|
1552
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1490
1553
|
{ deliverAs: "steer", triggerTurn: true },
|
|
1491
|
-
).catch(err => logger.debug("advisor
|
|
1554
|
+
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1492
1555
|
return;
|
|
1493
1556
|
}
|
|
1494
1557
|
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
@@ -2341,6 +2404,11 @@ export class AgentSession {
|
|
|
2341
2404
|
return;
|
|
2342
2405
|
}
|
|
2343
2406
|
|
|
2407
|
+
if (this.#isRetryableReasonlessAbort(msg)) {
|
|
2408
|
+
const didRetry = await this.#handleRetryableError(msg, { allowModelFallback: false });
|
|
2409
|
+
if (didRetry) return;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2344
2412
|
// A deliberate abort should settle the current turn, not trigger queued continuations.
|
|
2345
2413
|
if (msg.stopReason === "aborted") {
|
|
2346
2414
|
this.#resolveRetry();
|
|
@@ -2505,6 +2573,11 @@ export class AgentSession {
|
|
|
2505
2573
|
|
|
2506
2574
|
#scheduleAutoContinuePrompt(generation: number): void {
|
|
2507
2575
|
const continuePrompt = async () => {
|
|
2576
|
+
// Compaction summarizes away the first-message eager preludes, so re-assert the
|
|
2577
|
+
// delegate-via-tasks / phased-todo reminders on this auto-resumed turn. This runs
|
|
2578
|
+
// at invocation (past the abort check below), so an aborted continuation queues
|
|
2579
|
+
// nothing; scoped to this request via prependMessages, never the shared queue.
|
|
2580
|
+
const eagerNudges = this.#buildPostCompactionEagerNudges();
|
|
2508
2581
|
await this.#promptWithMessage(
|
|
2509
2582
|
{
|
|
2510
2583
|
role: "developer",
|
|
@@ -2513,7 +2586,10 @@ export class AgentSession {
|
|
|
2513
2586
|
timestamp: Date.now(),
|
|
2514
2587
|
},
|
|
2515
2588
|
autoContinuePrompt,
|
|
2516
|
-
{
|
|
2589
|
+
{
|
|
2590
|
+
skipPostPromptRecoveryWait: true,
|
|
2591
|
+
prependMessages: eagerNudges.length > 0 ? eagerNudges : undefined,
|
|
2592
|
+
},
|
|
2517
2593
|
);
|
|
2518
2594
|
};
|
|
2519
2595
|
this.#schedulePostPromptTask(
|
|
@@ -5034,6 +5110,13 @@ export class AgentSession {
|
|
|
5034
5110
|
// agent-initiated turns never trigger them.
|
|
5035
5111
|
const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
|
|
5036
5112
|
|
|
5113
|
+
// A user-initiated prompt (typed message or the `.`/`c` continue shortcut)
|
|
5114
|
+
// re-enables advisor auto-resume that a prior user interrupt suppressed.
|
|
5115
|
+
// Agent-initiated synthetic prompts (auto-continue, plan, reminders) do not.
|
|
5116
|
+
if (options?.userInitiated ?? !options?.synthetic) {
|
|
5117
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5037
5120
|
// If streaming, queue via steer() or followUp() based on option
|
|
5038
5121
|
if (this.isStreaming) {
|
|
5039
5122
|
if (!options?.streamingBehavior) {
|
|
@@ -5494,6 +5577,10 @@ export class AgentSession {
|
|
|
5494
5577
|
images: ImageContent[] | undefined,
|
|
5495
5578
|
mode: "steer" | "followUp",
|
|
5496
5579
|
): Promise<void> {
|
|
5580
|
+
// A queued user message (RPC/SDK/collab steer or follow-up, or a typed message
|
|
5581
|
+
// while streaming) is a deliberate resume; re-enable advisor auto-resume that
|
|
5582
|
+
// a user interrupt suppressed.
|
|
5583
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
5497
5584
|
const normalizedImages = await this.#normalizeImagesForModel(images);
|
|
5498
5585
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
5499
5586
|
if (normalizedImages?.length) {
|
|
@@ -5872,6 +5959,12 @@ export class AgentSession {
|
|
|
5872
5959
|
* abort. Omit it for internal/lifecycle aborts.
|
|
5873
5960
|
*/
|
|
5874
5961
|
async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
|
|
5962
|
+
const userInterrupt = options?.reason === USER_INTERRUPT_LABEL;
|
|
5963
|
+
if (userInterrupt) this.#advisorAutoResumeSuppressed = true;
|
|
5964
|
+
// Pull advisor concerns out of the steer/follow-up queues before any await so
|
|
5965
|
+
// the post-abort stranded-message drain can't auto-resume the run on them.
|
|
5966
|
+
// They are re-recorded as visible advice once the agent settles (below).
|
|
5967
|
+
const strandedAdvisorCards = userInterrupt ? this.#extractQueuedAdvisorCards() : [];
|
|
5875
5968
|
// Session switch/compact paths disconnect first; explicit aborts should
|
|
5876
5969
|
// leave any queued steer/follow-up visible for the user rather than
|
|
5877
5970
|
// auto-starting a fresh turn during cleanup.
|
|
@@ -5900,6 +5993,19 @@ export class AgentSession {
|
|
|
5900
5993
|
if (this.#toolChoiceQueue.hasInFlight) {
|
|
5901
5994
|
this.#toolChoiceQueue.reject("aborted");
|
|
5902
5995
|
}
|
|
5996
|
+
// Re-record advisor concerns the interrupt would otherwise strand, as
|
|
5997
|
+
// visible/persisted advice without triggering a turn (the agent is idle
|
|
5998
|
+
// now): cards steered into the queue before the user stopped, plus any
|
|
5999
|
+
// that arrived via enqueueAdvice mid-abort and were parked hidden in
|
|
6000
|
+
// #pendingNextTurnMessages while the turn was still tearing down. Other
|
|
6001
|
+
// deferred next-turn context (non-advisor) stays queued, in order.
|
|
6002
|
+
const parkedAdvisorCards = this.#pendingNextTurnMessages.filter(isAdvisorCard);
|
|
6003
|
+
if (parkedAdvisorCards.length > 0) {
|
|
6004
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(m => !isAdvisorCard(m));
|
|
6005
|
+
}
|
|
6006
|
+
for (const card of [...strandedAdvisorCards, ...parkedAdvisorCards]) {
|
|
6007
|
+
this.#preserveAdvisorCard(card);
|
|
6008
|
+
}
|
|
5903
6009
|
} finally {
|
|
5904
6010
|
this.#abortInProgress = false;
|
|
5905
6011
|
this.#drainStrandedQueuedMessages();
|
|
@@ -7683,7 +7789,9 @@ export class AgentSession {
|
|
|
7683
7789
|
};
|
|
7684
7790
|
}
|
|
7685
7791
|
|
|
7686
|
-
#createEagerTodoPrelude(
|
|
7792
|
+
#createEagerTodoPrelude(
|
|
7793
|
+
promptText: string | undefined,
|
|
7794
|
+
): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
|
|
7687
7795
|
const mode = this.settings.get("todo.eager");
|
|
7688
7796
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
7689
7797
|
if (mode === "default" || !todosEnabled) {
|
|
@@ -7700,14 +7808,18 @@ export class AgentSession {
|
|
|
7700
7808
|
// Only inject on the first user message of the conversation. Subsequent user
|
|
7701
7809
|
// turns must not receive the eager todo reminder — they often correct, clarify,
|
|
7702
7810
|
// or redirect the prior task, and forcing a brand-new todo list there is wrong.
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7811
|
+
// When `promptText` is undefined (post-compaction re-injection) there is no fresh
|
|
7812
|
+
// user message to gate on, so skip the first-message and prompt-suffix checks.
|
|
7813
|
+
if (promptText !== undefined) {
|
|
7814
|
+
const hasPriorUserMessage = this.agent.state.messages.some(m => m.role === "user");
|
|
7815
|
+
if (hasPriorUserMessage) {
|
|
7816
|
+
return undefined;
|
|
7817
|
+
}
|
|
7707
7818
|
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7819
|
+
const trimmedPromptText = promptText.trimEnd();
|
|
7820
|
+
if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
|
|
7821
|
+
return undefined;
|
|
7822
|
+
}
|
|
7711
7823
|
}
|
|
7712
7824
|
|
|
7713
7825
|
// Must check the active tool set, not just the registry: tool discovery
|
|
@@ -7730,8 +7842,10 @@ export class AgentSession {
|
|
|
7730
7842
|
timestamp: Date.now(),
|
|
7731
7843
|
};
|
|
7732
7844
|
// `preferred` suggests a todo list (reminder only); `always` also forces the
|
|
7733
|
-
// `todo` tool on the first turn — the previous boolean-on behavior.
|
|
7734
|
-
|
|
7845
|
+
// `todo` tool on the first turn — the previous boolean-on behavior. Post-compaction
|
|
7846
|
+
// re-injection (`promptText === undefined`) is always reminder-only: forcing a tool
|
|
7847
|
+
// onto the auto-resumed turn would override the agent's in-flight action.
|
|
7848
|
+
if (promptText === undefined || mode === "preferred") {
|
|
7735
7849
|
return { message };
|
|
7736
7850
|
}
|
|
7737
7851
|
const todoToolChoice = buildNamedToolChoice("todo", this.model);
|
|
@@ -7749,7 +7863,7 @@ export class AgentSession {
|
|
|
7749
7863
|
return { message, toolChoice: todoToolChoice };
|
|
7750
7864
|
}
|
|
7751
7865
|
|
|
7752
|
-
#createEagerTaskPrelude(promptText: string): AgentMessage | undefined {
|
|
7866
|
+
#createEagerTaskPrelude(promptText: string | undefined): AgentMessage | undefined {
|
|
7753
7867
|
if (this.settings.get("task.eager") !== "always") return undefined;
|
|
7754
7868
|
// Main agent only: subagents keep `task` active (the parent only filters `todo`),
|
|
7755
7869
|
// so a salient delegate-reminder there would amplify nested fan-out. Gate on the
|
|
@@ -7757,9 +7871,13 @@ export class AgentSession {
|
|
|
7757
7871
|
// still gets the reminder.
|
|
7758
7872
|
if (this.#agentKind === "sub") return undefined;
|
|
7759
7873
|
if (this.#planModeState?.enabled) return undefined;
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
if (
|
|
7874
|
+
// First-message-only gates are skipped post-compaction (`promptText === undefined`),
|
|
7875
|
+
// where there is no fresh user message to suppress the reminder for.
|
|
7876
|
+
if (promptText !== undefined) {
|
|
7877
|
+
if (this.agent.state.messages.some(m => m.role === "user")) return undefined;
|
|
7878
|
+
const trimmed = promptText.trimEnd();
|
|
7879
|
+
if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
|
|
7880
|
+
}
|
|
7763
7881
|
if (!this.getActiveToolNames().includes("task")) return undefined;
|
|
7764
7882
|
return {
|
|
7765
7883
|
role: "custom",
|
|
@@ -7770,6 +7888,24 @@ export class AgentSession {
|
|
|
7770
7888
|
timestamp: Date.now(),
|
|
7771
7889
|
};
|
|
7772
7890
|
}
|
|
7891
|
+
|
|
7892
|
+
/**
|
|
7893
|
+
* Build the eager task/todo reminders to re-inject on the auto-continuation turn that
|
|
7894
|
+
* follows a compaction. The first-message preludes are the oldest messages, so
|
|
7895
|
+
* compaction summarizes them away and the agent silently loses the delegate-via-tasks
|
|
7896
|
+
* and phased-todo guidance mid-work; this re-asserts them, reminder-only (the todo
|
|
7897
|
+
* builder drops its forced tool_choice when `promptText` is undefined). Each builder
|
|
7898
|
+
* still applies its own mode / agent-kind / plan-mode / tool-active / surviving-todo
|
|
7899
|
+
* gates, so an empty array means nothing currently warrants a nudge.
|
|
7900
|
+
*/
|
|
7901
|
+
#buildPostCompactionEagerNudges(): AgentMessage[] {
|
|
7902
|
+
const nudges: AgentMessage[] = [];
|
|
7903
|
+
const todo = this.#createEagerTodoPrelude(undefined);
|
|
7904
|
+
if (todo) nudges.push(todo.message);
|
|
7905
|
+
const task = this.#createEagerTaskPrelude(undefined);
|
|
7906
|
+
if (task) nudges.push(task);
|
|
7907
|
+
return nudges;
|
|
7908
|
+
}
|
|
7773
7909
|
/**
|
|
7774
7910
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
7775
7911
|
*/
|
|
@@ -9045,9 +9181,31 @@ export class AgentSession {
|
|
|
9045
9181
|
// Auto-Retry
|
|
9046
9182
|
// =========================================================================
|
|
9047
9183
|
|
|
9184
|
+
/**
|
|
9185
|
+
* Retry an empty, reason-less provider abort: a turn that ended `aborted`
|
|
9186
|
+
* with no content and the generic sentinel (bare `abort()`), but only while
|
|
9187
|
+
* the session is neither aborting nor tearing down. A user/lifecycle abort
|
|
9188
|
+
* (`#abortInProgress`), a dispose-driven abort (`#isDisposed`), or a
|
|
9189
|
+
* session-induced streaming-edit guard abort (`#streamingEditAbortTriggered` —
|
|
9190
|
+
* auto-generated-file guard or failed-patch preview) is deliberate and MUST
|
|
9191
|
+
* settle the turn instead: routing it through retry would orphan
|
|
9192
|
+
* `#retryPromise` on a continuation the guard skips (hanging the in-flight
|
|
9193
|
+
* `prompt()`) or silently undo the guard's intended abort.
|
|
9194
|
+
*/
|
|
9195
|
+
#isRetryableReasonlessAbort(message: AssistantMessage): boolean {
|
|
9196
|
+
return (
|
|
9197
|
+
message.stopReason === "aborted" &&
|
|
9198
|
+
message.content.length === 0 &&
|
|
9199
|
+
message.errorMessage === GENERIC_ABORT_SENTINEL &&
|
|
9200
|
+
!this.#abortInProgress &&
|
|
9201
|
+
!this.#isDisposed &&
|
|
9202
|
+
!this.#streamingEditAbortTriggered
|
|
9203
|
+
);
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9048
9206
|
/**
|
|
9049
9207
|
* Check if an error is retryable (transient errors or usage limits).
|
|
9050
|
-
* Context overflow
|
|
9208
|
+
* Context overflow is NOT retryable (handled by compaction instead).
|
|
9051
9209
|
* Usage-limit errors are retryable because the retry handler performs credential switching.
|
|
9052
9210
|
*/
|
|
9053
9211
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
@@ -9058,11 +9216,22 @@ export class AgentSession {
|
|
|
9058
9216
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
9059
9217
|
|
|
9060
9218
|
if (this.#isClassifierRefusal(message)) return true;
|
|
9219
|
+
if (this.#streamInterruptedAfterObservableOutput(message)) return false;
|
|
9061
9220
|
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
9062
9221
|
|
|
9063
9222
|
const err = message.errorMessage;
|
|
9064
9223
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
9065
9224
|
}
|
|
9225
|
+
#streamInterruptedAfterObservableOutput(message: AssistantMessage): boolean {
|
|
9226
|
+
if (message.stopDetails?.type === STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL) return true;
|
|
9227
|
+
for (const block of message.content) {
|
|
9228
|
+
if (block.type === "toolCall") return true;
|
|
9229
|
+
if (block.type === "text" && block.text.length > 0) return true;
|
|
9230
|
+
if (block.type === "thinking" && block.thinking.length > 0) return true;
|
|
9231
|
+
if (block.type === "redactedThinking" && block.data.length > 0) return true;
|
|
9232
|
+
}
|
|
9233
|
+
return false;
|
|
9234
|
+
}
|
|
9066
9235
|
|
|
9067
9236
|
#isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
|
|
9068
9237
|
const currentApi = this.model?.api;
|
|
@@ -9193,11 +9362,25 @@ export class AgentSession {
|
|
|
9193
9362
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9194
9363
|
if (!parsedCurrent) return undefined;
|
|
9195
9364
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
9365
|
+
const currentPlainSelector = this.model
|
|
9366
|
+
? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
|
|
9367
|
+
: undefined;
|
|
9368
|
+
const currentPlainBaseSelector =
|
|
9369
|
+
currentPlainSelector && currentPlainSelector !== currentSelector
|
|
9370
|
+
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
9371
|
+
: undefined;
|
|
9372
|
+
|
|
9373
|
+
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
9374
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9375
|
+
if (primarySelector?.raw === currentSelector) return role;
|
|
9376
|
+
}
|
|
9196
9377
|
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
9197
9378
|
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9198
9379
|
if (!primarySelector) continue;
|
|
9199
|
-
if (primarySelector.raw ===
|
|
9200
|
-
|
|
9380
|
+
if (currentPlainSelector && primarySelector.raw === currentPlainSelector) return role;
|
|
9381
|
+
const primaryBaseSelector = formatRetryFallbackBaseSelector(primarySelector);
|
|
9382
|
+
if (primaryBaseSelector === currentBaseSelector) return role;
|
|
9383
|
+
if (currentPlainBaseSelector && primaryBaseSelector === currentPlainBaseSelector) return role;
|
|
9201
9384
|
}
|
|
9202
9385
|
return undefined;
|
|
9203
9386
|
}
|
|
@@ -9221,10 +9404,23 @@ export class AgentSession {
|
|
|
9221
9404
|
if (chain.length <= 1) return [];
|
|
9222
9405
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9223
9406
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
9224
|
-
const
|
|
9407
|
+
const currentPlainSelector =
|
|
9408
|
+
this.model && parsedCurrent
|
|
9409
|
+
? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
|
|
9410
|
+
: undefined;
|
|
9411
|
+
const currentPlainBaseSelector =
|
|
9412
|
+
parsedCurrent && currentPlainSelector && currentPlainSelector !== currentSelector
|
|
9413
|
+
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
9414
|
+
: undefined;
|
|
9415
|
+
const exactIndex = chain.findIndex(
|
|
9416
|
+
selector => selector.raw === currentSelector || selector.raw === currentPlainSelector,
|
|
9417
|
+
);
|
|
9225
9418
|
if (exactIndex >= 0) return chain.slice(exactIndex + 1);
|
|
9226
9419
|
const baseIndex = currentBaseSelector
|
|
9227
|
-
? chain.findIndex(selector =>
|
|
9420
|
+
? chain.findIndex(selector => {
|
|
9421
|
+
const selectorBase = formatRetryFallbackBaseSelector(selector);
|
|
9422
|
+
return selectorBase === currentBaseSelector || selectorBase === currentPlainBaseSelector;
|
|
9423
|
+
})
|
|
9228
9424
|
: -1;
|
|
9229
9425
|
if (baseIndex >= 0) return chain.slice(baseIndex + 1);
|
|
9230
9426
|
return chain.slice(1);
|
|
@@ -9236,7 +9432,8 @@ export class AgentSession {
|
|
|
9236
9432
|
currentSelector: string,
|
|
9237
9433
|
options?: { pinFallback?: boolean },
|
|
9238
9434
|
): Promise<void> {
|
|
9239
|
-
const
|
|
9435
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
9436
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
9240
9437
|
if (!candidate) {
|
|
9241
9438
|
throw new Error(`Retry fallback model not found: ${selector.raw}`);
|
|
9242
9439
|
}
|
|
@@ -9249,10 +9446,10 @@ export class AgentSession {
|
|
|
9249
9446
|
// `auto` instead of collapsing it to the level it resolved to this turn.
|
|
9250
9447
|
const currentThinkingLevel = this.configuredThinkingLevel();
|
|
9251
9448
|
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
9252
|
-
|
|
9449
|
+
const candidateSelector = formatModelStringWithRouting(candidate);
|
|
9253
9450
|
this.#setModelWithProviderSessionReset(candidate);
|
|
9254
|
-
this.sessionManager.appendModelChange(
|
|
9255
|
-
this.settings.getStorage()?.recordModelUsage(
|
|
9451
|
+
this.sessionManager.appendModelChange(candidateSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
9452
|
+
this.settings.getStorage()?.recordModelUsage(candidateSelector);
|
|
9256
9453
|
this.setThinkingLevel(nextThinkingLevel);
|
|
9257
9454
|
if (!this.#activeRetryFallback) {
|
|
9258
9455
|
this.#activeRetryFallback = {
|
|
@@ -9280,7 +9477,8 @@ export class AgentSession {
|
|
|
9280
9477
|
|
|
9281
9478
|
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector)) {
|
|
9282
9479
|
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
9283
|
-
const
|
|
9480
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
9481
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
9284
9482
|
if (!candidate) continue;
|
|
9285
9483
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
9286
9484
|
if (!apiKey) continue;
|
|
@@ -9318,7 +9516,9 @@ export class AgentSession {
|
|
|
9318
9516
|
}
|
|
9319
9517
|
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
9320
9518
|
|
|
9321
|
-
const
|
|
9519
|
+
const resolvedPrimary = resolveModelOverride([originalSelector.raw], this.#modelRegistry, this.settings);
|
|
9520
|
+
const primaryModel =
|
|
9521
|
+
resolvedPrimary.model ?? this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
9322
9522
|
if (!primaryModel) return;
|
|
9323
9523
|
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
|
|
9324
9524
|
if (!apiKey) return;
|
|
@@ -9326,9 +9526,10 @@ export class AgentSession {
|
|
|
9326
9526
|
const currentThinkingLevel = this.configuredThinkingLevel();
|
|
9327
9527
|
const thinkingToApply =
|
|
9328
9528
|
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
9529
|
+
const primarySelector = formatModelStringWithRouting(primaryModel);
|
|
9329
9530
|
this.#setModelWithProviderSessionReset(primaryModel);
|
|
9330
|
-
this.sessionManager.appendModelChange(
|
|
9331
|
-
this.settings.getStorage()?.recordModelUsage(
|
|
9531
|
+
this.sessionManager.appendModelChange(primarySelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
9532
|
+
this.settings.getStorage()?.recordModelUsage(primarySelector);
|
|
9332
9533
|
this.setThinkingLevel(thinkingToApply);
|
|
9333
9534
|
this.#clearActiveRetryFallback();
|
|
9334
9535
|
}
|
|
@@ -9388,7 +9589,10 @@ export class AgentSession {
|
|
|
9388
9589
|
* Handle retryable errors with exponential backoff.
|
|
9389
9590
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
9390
9591
|
*/
|
|
9391
|
-
async #handleRetryableError(
|
|
9592
|
+
async #handleRetryableError(
|
|
9593
|
+
message: AssistantMessage,
|
|
9594
|
+
options?: { allowModelFallback?: boolean },
|
|
9595
|
+
): Promise<boolean> {
|
|
9392
9596
|
const retrySettings = this.settings.getGroup("retry");
|
|
9393
9597
|
if (!retrySettings.enabled) return false;
|
|
9394
9598
|
const classifierRefusal = this.#isClassifierRefusal(message);
|
|
@@ -9476,9 +9680,10 @@ export class AgentSession {
|
|
|
9476
9680
|
}
|
|
9477
9681
|
}
|
|
9478
9682
|
|
|
9683
|
+
const allowModelFallback = options?.allowModelFallback !== false;
|
|
9479
9684
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
9480
9685
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
9481
|
-
if (retrySettings.modelFallback) {
|
|
9686
|
+
if (allowModelFallback && retrySettings.modelFallback) {
|
|
9482
9687
|
if (!classifierRefusal) {
|
|
9483
9688
|
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
9484
9689
|
}
|
|
@@ -11193,18 +11398,16 @@ export class AgentSession {
|
|
|
11193
11398
|
}
|
|
11194
11399
|
|
|
11195
11400
|
/**
|
|
11196
|
-
* Enable or disable the advisor for this session. The setting is
|
|
11401
|
+
* Enable or disable the advisor for this session. The setting is overridden for the session,
|
|
11197
11402
|
* and the runtime is started or stopped to match.
|
|
11198
11403
|
*
|
|
11199
11404
|
* @returns true when the advisor is actively running after the call.
|
|
11200
11405
|
*/
|
|
11201
11406
|
setAdvisorEnabled(enabled: boolean): boolean {
|
|
11407
|
+
this.#advisorEnabled = enabled;
|
|
11202
11408
|
if (enabled) {
|
|
11203
|
-
this.settings.clearOverride("advisor.enabled");
|
|
11204
|
-
this.settings.set("advisor.enabled", true);
|
|
11205
11409
|
return this.#buildAdvisorRuntime(true);
|
|
11206
11410
|
}
|
|
11207
|
-
this.settings.set("advisor.enabled", false);
|
|
11208
11411
|
this.#stopAdvisorRuntime();
|
|
11209
11412
|
return false;
|
|
11210
11413
|
}
|
|
@@ -11215,7 +11418,14 @@ export class AgentSession {
|
|
|
11215
11418
|
* @returns true when the advisor is actively running after the call.
|
|
11216
11419
|
*/
|
|
11217
11420
|
toggleAdvisorEnabled(): boolean {
|
|
11218
|
-
return this.setAdvisorEnabled(!this
|
|
11421
|
+
return this.setAdvisorEnabled(!this.#advisorEnabled);
|
|
11422
|
+
}
|
|
11423
|
+
|
|
11424
|
+
/**
|
|
11425
|
+
* Whether the advisor setting is enabled for this session.
|
|
11426
|
+
*/
|
|
11427
|
+
isAdvisorEnabled(): boolean {
|
|
11428
|
+
return this.#advisorEnabled;
|
|
11219
11429
|
}
|
|
11220
11430
|
|
|
11221
11431
|
/**
|
|
@@ -11232,7 +11442,7 @@ export class AgentSession {
|
|
|
11232
11442
|
* Return structured advisor stats for the status command and TUI panel.
|
|
11233
11443
|
*/
|
|
11234
11444
|
getAdvisorStats(): AdvisorStats {
|
|
11235
|
-
const configured = this
|
|
11445
|
+
const configured = this.#advisorEnabled;
|
|
11236
11446
|
const advisor = this.#advisorAgent;
|
|
11237
11447
|
if (!advisor) {
|
|
11238
11448
|
return {
|
package/src/session/messages.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function shouldRenderAbortReason(errorMessage: string | undefined): boole
|
|
|
94
94
|
|
|
95
95
|
/** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
|
|
96
96
|
* reason (bare `abort()`). Renderers treat it as "no specific reason given". */
|
|
97
|
-
const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
97
|
+
export const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
98
98
|
|
|
99
99
|
/** Resolve the operator-facing label for an aborted assistant turn. A custom
|
|
100
100
|
* abort reason threaded onto `errorMessage` is returned verbatim; aborts with
|
|
@@ -1522,15 +1522,17 @@ export class SessionManager {
|
|
|
1522
1522
|
/**
|
|
1523
1523
|
* Open a specific session file.
|
|
1524
1524
|
* @param sessionDir Optional dir for /new or /branch; defaults to the file's parent.
|
|
1525
|
+
* @param options.initialCwd Cwd to use when the file is empty or missing.
|
|
1525
1526
|
*/
|
|
1526
1527
|
static async open(
|
|
1527
1528
|
filePath: string,
|
|
1528
1529
|
sessionDir?: string,
|
|
1529
1530
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1531
|
+
options?: { initialCwd?: string },
|
|
1530
1532
|
): Promise<SessionManager> {
|
|
1531
1533
|
const loaded = await loadEntriesFromFile(filePath, storage);
|
|
1532
1534
|
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
1533
|
-
const cwd = header?.cwd ?? getProjectDir();
|
|
1535
|
+
const cwd = header?.cwd ?? options?.initialCwd ?? getProjectDir();
|
|
1534
1536
|
const dir = sessionDir ?? path.dirname(path.resolve(filePath));
|
|
1535
1537
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1536
1538
|
await manager.setSessionFile(filePath);
|