@oh-my-pi/pi-coding-agent 15.13.3 → 16.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -133
- package/dist/cli.js +621 -530
- package/dist/types/advisor/__tests__/advisor.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +58 -0
- package/dist/types/advisor/index.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +52 -0
- package/dist/types/advisor/watchdog.d.ts +5 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +66 -5
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/components/advisor-message.d.ts +9 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/types.d.ts +8 -1
- package/dist/types/sdk.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +81 -2
- package/dist/types/session/session-history-format.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/session/yield-queue.d.ts +2 -0
- 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/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/report-tool-issue.d.ts +0 -1
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/__tests__/advisor.test.ts +586 -0
- package/src/advisor/advise-tool.ts +87 -0
- package/src/advisor/index.ts +3 -0
- package/src/advisor/runtime.ts +248 -0
- package/src/advisor/watchdog.ts +83 -0
- package/src/cli/args.ts +1 -0
- package/src/collab/host.ts +1 -1
- package/src/config/model-roles.ts +13 -1
- package/src/config/settings-schema.ts +65 -6
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +101 -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/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/plugins/loader.ts +3 -2
- package/src/extensibility/plugins/manager.ts +4 -3
- 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 +10 -9
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +9 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/advisor-message.ts +99 -0
- package/src/modes/components/agent-hub.ts +7 -0
- package/src/modes/components/assistant-message.ts +86 -0
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/segments.ts +20 -7
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/command-controller.ts +69 -2
- 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 +59 -2
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/ui-helpers.ts +9 -0
- package/src/prompts/advisor/advise-tool.md +1 -0
- package/src/prompts/advisor/system.md +31 -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 +61 -14
- package/src/session/agent-session.ts +667 -13
- package/src/session/session-dump-format.ts +15 -131
- package/src/session/session-history-format.ts +30 -11
- package/src/session/session-manager.ts +3 -1
- package/src/session/yield-queue.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +105 -4
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +5 -4
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/tools/path-utils.ts +33 -2
- package/src/tools/report-tool-issue.ts +2 -7
- package/src/web/scrapers/docs-rs.ts +2 -3
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -22,7 +22,7 @@ import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
|
22
22
|
import {
|
|
23
23
|
type AfterToolCallContext,
|
|
24
24
|
type AfterToolCallResult,
|
|
25
|
-
|
|
25
|
+
Agent,
|
|
26
26
|
AgentBusyError,
|
|
27
27
|
type AgentEvent,
|
|
28
28
|
type AgentMessage,
|
|
@@ -30,7 +30,9 @@ import {
|
|
|
30
30
|
type AgentTool,
|
|
31
31
|
AppendOnlyContextManager,
|
|
32
32
|
type AsideMessage,
|
|
33
|
+
type CompactionSummaryMessage,
|
|
33
34
|
resolveTelemetry,
|
|
35
|
+
STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL,
|
|
34
36
|
ThinkingLevel,
|
|
35
37
|
} from "@oh-my-pi/pi-agent-core";
|
|
36
38
|
|
|
@@ -54,6 +56,8 @@ import {
|
|
|
54
56
|
generateHandoff,
|
|
55
57
|
prepareCompaction,
|
|
56
58
|
resolveThresholdTokens,
|
|
59
|
+
type SessionEntry,
|
|
60
|
+
type SessionMessageEntry,
|
|
57
61
|
type ShakeConfig,
|
|
58
62
|
type ShakeRegion,
|
|
59
63
|
type SummaryOptions,
|
|
@@ -114,6 +118,16 @@ import {
|
|
|
114
118
|
Snowflake,
|
|
115
119
|
} from "@oh-my-pi/pi-utils";
|
|
116
120
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
121
|
+
import {
|
|
122
|
+
AdviseTool,
|
|
123
|
+
type AdvisorAgent,
|
|
124
|
+
type AdvisorMessageDetails,
|
|
125
|
+
type AdvisorNote,
|
|
126
|
+
AdvisorRuntime,
|
|
127
|
+
type AdvisorSeverity,
|
|
128
|
+
formatAdvisorBatchContent,
|
|
129
|
+
isInterruptingSeverity,
|
|
130
|
+
} from "../advisor";
|
|
117
131
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
118
132
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
119
133
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -129,6 +143,7 @@ import {
|
|
|
129
143
|
parseModelString,
|
|
130
144
|
type ResolvedModelRoleValue,
|
|
131
145
|
resolveModelRoleValue,
|
|
146
|
+
resolveRoleSelection,
|
|
132
147
|
} from "../config/model-resolver";
|
|
133
148
|
import { MODEL_ROLE_IDS } from "../config/model-roles";
|
|
134
149
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
@@ -189,6 +204,7 @@ import { computeNonMessageTokens } from "../modes/utils/context-usage";
|
|
|
189
204
|
import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
|
|
190
205
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
191
206
|
import type { PlanModeState } from "../plan-mode/state";
|
|
207
|
+
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
192
208
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
193
209
|
import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
|
|
194
210
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
@@ -262,12 +278,14 @@ import {
|
|
|
262
278
|
SILENT_ABORT_MARKER,
|
|
263
279
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
264
280
|
stripImagesFromMessage,
|
|
281
|
+
USER_INTERRUPT_LABEL,
|
|
265
282
|
} from "./messages";
|
|
266
283
|
import type { SessionContext } from "./session-context";
|
|
267
284
|
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
|
|
268
285
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
269
286
|
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
|
|
270
287
|
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
|
|
288
|
+
import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
271
289
|
import type { SessionManager } from "./session-manager";
|
|
272
290
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
273
291
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
@@ -457,6 +475,15 @@ export interface AgentSessionConfig {
|
|
|
457
475
|
* so that credential sticky selection is consistent with the session's streaming calls.
|
|
458
476
|
*/
|
|
459
477
|
providerSessionId?: string;
|
|
478
|
+
/**
|
|
479
|
+
* Hard-isolated read-only tools (read/search/find) for the advisor agent,
|
|
480
|
+
* pre-built in `createAgentSession` against a distinct `ToolSession` so the
|
|
481
|
+
* advisor's reads never share the primary's snapshot/seen-lines/conflict
|
|
482
|
+
* caches. Undefined when the advisor is disabled.
|
|
483
|
+
*/
|
|
484
|
+
advisorReadOnlyTools?: AgentTool[];
|
|
485
|
+
/** Preloaded watchdog prompt content for the advisor. */
|
|
486
|
+
advisorWatchdogPrompt?: string;
|
|
460
487
|
}
|
|
461
488
|
|
|
462
489
|
/** Options for AgentSession.prompt() */
|
|
@@ -471,6 +498,12 @@ export interface PromptOptions {
|
|
|
471
498
|
toolChoice?: ToolChoice;
|
|
472
499
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
473
500
|
synthetic?: boolean;
|
|
501
|
+
/** Marks this prompt as a deliberate user action (typed message, `.`/`c`
|
|
502
|
+
* continue). Clears advisor auto-resume suppression that a user interrupt set.
|
|
503
|
+
* Defaults to `!synthetic`; manual-continue is synthetic yet user-initiated, so
|
|
504
|
+
* it sets this explicitly. Agent-initiated synthetic prompts (auto-continue,
|
|
505
|
+
* plan re-prime, reminders) leave it unset and keep suppression latched. */
|
|
506
|
+
userInitiated?: boolean;
|
|
474
507
|
/** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */
|
|
475
508
|
attribution?: MessageAttribution;
|
|
476
509
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
@@ -539,6 +572,28 @@ export interface SessionStats {
|
|
|
539
572
|
cost: number;
|
|
540
573
|
}
|
|
541
574
|
|
|
575
|
+
/** Advisor statistics for /advisor status command. */
|
|
576
|
+
export interface AdvisorStats {
|
|
577
|
+
configured: boolean;
|
|
578
|
+
active: boolean;
|
|
579
|
+
model?: Model;
|
|
580
|
+
contextWindow: number;
|
|
581
|
+
contextTokens: number;
|
|
582
|
+
tokens: {
|
|
583
|
+
input: number;
|
|
584
|
+
output: number;
|
|
585
|
+
cacheRead: number;
|
|
586
|
+
cacheWrite: number;
|
|
587
|
+
total: number;
|
|
588
|
+
};
|
|
589
|
+
cost: number;
|
|
590
|
+
messages: {
|
|
591
|
+
user: number;
|
|
592
|
+
assistant: number;
|
|
593
|
+
total: number;
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
542
597
|
export interface FreshSessionResult {
|
|
543
598
|
previousSessionId: string;
|
|
544
599
|
sessionId: string;
|
|
@@ -894,6 +949,10 @@ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
|
|
|
894
949
|
return !(message.role === "custom" && message.display === false);
|
|
895
950
|
}
|
|
896
951
|
|
|
952
|
+
function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
953
|
+
return message.role === "custom" && message.customType === "advisor";
|
|
954
|
+
}
|
|
955
|
+
|
|
897
956
|
function queueChipText(message: AgentMessage): string {
|
|
898
957
|
if (message.role === "custom") {
|
|
899
958
|
return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
|
|
@@ -941,9 +1000,20 @@ export class AgentSession {
|
|
|
941
1000
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
942
1001
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
943
1002
|
#queuedMessageDrainScheduled = false;
|
|
1003
|
+
/** Latched true when the user deliberately interrupts (USER_INTERRUPT_LABEL);
|
|
1004
|
+
* suppresses advisor concern/blocker auto-resume until the user next resumes.
|
|
1005
|
+
* Advisor advice is still recorded into the transcript, just not auto-run. */
|
|
1006
|
+
#advisorAutoResumeSuppressed = false;
|
|
944
1007
|
#planModeState: PlanModeState | undefined;
|
|
945
1008
|
#goalModeState: GoalModeState | undefined;
|
|
946
1009
|
#goalRuntime: GoalRuntime;
|
|
1010
|
+
#advisorRuntime?: AdvisorRuntime;
|
|
1011
|
+
#advisorEnabled = false;
|
|
1012
|
+
/** The advisor's own agent, retained so `/dump advisor` can serialize its transcript. Undefined when no advisor is active. */
|
|
1013
|
+
#advisorAgent?: Agent;
|
|
1014
|
+
#advisorReadOnlyTools?: AgentTool[];
|
|
1015
|
+
#advisorWatchdogPrompt?: string;
|
|
1016
|
+
#advisorYieldQueueUnsubscribe?: () => void;
|
|
947
1017
|
#goalTurnCounter = 0;
|
|
948
1018
|
#planReferenceSent = false;
|
|
949
1019
|
#planReferencePath = "local://PLAN.md";
|
|
@@ -1191,6 +1261,39 @@ export class AgentSession {
|
|
|
1191
1261
|
this.#scheduleQueuedMessageDrain();
|
|
1192
1262
|
}
|
|
1193
1263
|
|
|
1264
|
+
/** Remove advisor concern/blocker cards from the agent-core steer/follow-up
|
|
1265
|
+
* queues and return them. Used on a deliberate user interrupt so the post-abort
|
|
1266
|
+
* stranded-message drain cannot auto-resume the run on an advisor card that was
|
|
1267
|
+
* steered in just before the user stopped; real user follow-ups stay queued.
|
|
1268
|
+
* Synchronous and await-free so it runs before the abort path polls the queue. */
|
|
1269
|
+
#extractQueuedAdvisorCards(): CustomMessage[] {
|
|
1270
|
+
const steering = this.agent.peekSteeringQueue();
|
|
1271
|
+
const followUp = this.agent.peekFollowUpQueue();
|
|
1272
|
+
const cards = [...steering, ...followUp].filter(isAdvisorCard);
|
|
1273
|
+
if (cards.length === 0) return [];
|
|
1274
|
+
this.agent.replaceQueues(
|
|
1275
|
+
steering.filter(m => !isAdvisorCard(m)),
|
|
1276
|
+
followUp.filter(m => !isAdvisorCard(m)),
|
|
1277
|
+
);
|
|
1278
|
+
return cards;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** 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
|
+
* emit message_start/message_end like #flushPendingIrcAsides so
|
|
1284
|
+
* #handleAgentEvent renders it live (TUI/ACP) and persists it as a
|
|
1285
|
+
* CustomMessageEntry. While a turn is still tearing down (mid-abort), park it
|
|
1286
|
+
* hidden so abort's settle step replays it once idle — never appended into a
|
|
1287
|
+
* live streamMessage. */
|
|
1288
|
+
#preserveAdvisorCard(card: CustomMessage): void {
|
|
1289
|
+
if (this.isStreaming) {
|
|
1290
|
+
this.#pendingNextTurnMessages.push(card);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
this.agent.emitExternalEvent({ type: "message_start", message: card });
|
|
1294
|
+
this.agent.emitExternalEvent({ type: "message_end", message: card });
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1194
1297
|
#resetInFlight(): void {
|
|
1195
1298
|
this.#promptInFlightCount = 0;
|
|
1196
1299
|
this.#releasePowerAssertion();
|
|
@@ -1234,6 +1337,8 @@ export class AgentSession {
|
|
|
1234
1337
|
this.#customCommands = config.customCommands ?? [];
|
|
1235
1338
|
this.#skillsSettings = config.skillsSettings;
|
|
1236
1339
|
this.#modelRegistry = config.modelRegistry;
|
|
1340
|
+
this.#advisorReadOnlyTools = config.advisorReadOnlyTools;
|
|
1341
|
+
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1237
1342
|
this.#validateRetryFallbackChains();
|
|
1238
1343
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
1239
1344
|
this.#requestedToolNames = config.requestedToolNames;
|
|
@@ -1266,6 +1371,17 @@ export class AgentSession {
|
|
|
1266
1371
|
};
|
|
1267
1372
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
1268
1373
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1374
|
+
this.agent.setOnTurnEnd(async (messages, signal) => {
|
|
1375
|
+
if (signal?.aborted) return;
|
|
1376
|
+
if (this.#advisorRuntime && !this.#advisorRuntime.disposed) {
|
|
1377
|
+
this.#advisorRuntime.onTurnEnd(messages);
|
|
1378
|
+
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
1379
|
+
if (syncBacklog !== "off") {
|
|
1380
|
+
const threshold = parseInt(syncBacklog, 10);
|
|
1381
|
+
await this.#advisorRuntime.waitForCatchup(30000, threshold, signal);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1269
1385
|
this.yieldQueue = new YieldQueue({
|
|
1270
1386
|
isStreaming: () => this.isStreaming,
|
|
1271
1387
|
injectIdle: async messages => {
|
|
@@ -1377,12 +1493,315 @@ export class AgentSession {
|
|
|
1377
1493
|
},
|
|
1378
1494
|
});
|
|
1379
1495
|
|
|
1496
|
+
this.#advisorEnabled = this.settings.get("advisor.enabled") as boolean;
|
|
1497
|
+
if (this.#advisorEnabled) this.#buildAdvisorRuntime();
|
|
1498
|
+
|
|
1380
1499
|
// Always subscribe to agent events for internal handling
|
|
1381
1500
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
1382
1501
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
1383
1502
|
// Re-evaluate append-only context mode when the setting changes at runtime.
|
|
1384
1503
|
this.#unsubscribeAppendOnly = onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model));
|
|
1385
1504
|
}
|
|
1505
|
+
// -------------------------------------------------------------------------
|
|
1506
|
+
// Advisor runtime lifecycle
|
|
1507
|
+
// -------------------------------------------------------------------------
|
|
1508
|
+
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1509
|
+
if (this.#isDisposed) return false;
|
|
1510
|
+
if (this.#advisorRuntime) return true;
|
|
1511
|
+
if (!this.#advisorEnabled) return false;
|
|
1512
|
+
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1513
|
+
|
|
1514
|
+
const advisorSel = resolveRoleSelection(
|
|
1515
|
+
["advisor"],
|
|
1516
|
+
this.settings,
|
|
1517
|
+
this.#modelRegistry.getAvailable(),
|
|
1518
|
+
this.#modelRegistry,
|
|
1519
|
+
);
|
|
1520
|
+
if (!advisorSel) {
|
|
1521
|
+
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive");
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Concern and blocker interrupt the running agent through the steering
|
|
1526
|
+
// channel (aborting in-flight tools at the next steering boundary); when the
|
|
1527
|
+
// loop has already yielded, triggerTurn resumes it so the advice is acted on
|
|
1528
|
+
// immediately rather than waiting for the next user prompt. After a deliberate
|
|
1529
|
+
// user interrupt that auto-resume is suppressed: the concern is recorded as
|
|
1530
|
+
// visible advice and re-enters context only when the user resumes. A plain nit
|
|
1531
|
+
// rides the non-interrupting YieldQueue aside.
|
|
1532
|
+
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1533
|
+
if (isInterruptingSeverity(severity)) {
|
|
1534
|
+
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1535
|
+
const content = formatAdvisorBatchContent(notes);
|
|
1536
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
1537
|
+
if (this.#advisorAutoResumeSuppressed) {
|
|
1538
|
+
this.#preserveAdvisorCard({
|
|
1539
|
+
role: "custom",
|
|
1540
|
+
customType: "advisor",
|
|
1541
|
+
content,
|
|
1542
|
+
display: true,
|
|
1543
|
+
attribution: "agent",
|
|
1544
|
+
details,
|
|
1545
|
+
timestamp: Date.now(),
|
|
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) }));
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
const adviseTool = new AdviseTool(enqueueAdvice);
|
|
1559
|
+
const advisorReadOnlyTools = this.#advisorReadOnlyTools ?? [];
|
|
1560
|
+
|
|
1561
|
+
const appendOnlyContext = new AppendOnlyContextManager();
|
|
1562
|
+
const advisorThinkingLevel = advisorSel.thinkingLevel ?? ThinkingLevel.Medium;
|
|
1563
|
+
const systemPrompt = [advisorSystemPrompt];
|
|
1564
|
+
if (this.#advisorWatchdogPrompt) {
|
|
1565
|
+
systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
1566
|
+
}
|
|
1567
|
+
const advisorAgent = new Agent({
|
|
1568
|
+
initialState: {
|
|
1569
|
+
systemPrompt,
|
|
1570
|
+
model: advisorSel.model,
|
|
1571
|
+
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
1572
|
+
tools: [adviseTool, ...advisorReadOnlyTools],
|
|
1573
|
+
},
|
|
1574
|
+
appendOnlyContext,
|
|
1575
|
+
sessionId: this.sessionId ? `${this.sessionId}-advisor` : undefined,
|
|
1576
|
+
getApiKey: async provider => {
|
|
1577
|
+
const key = await this.#modelRegistry.getApiKeyForProvider(
|
|
1578
|
+
provider,
|
|
1579
|
+
this.sessionId ? `${this.sessionId}-advisor` : undefined,
|
|
1580
|
+
);
|
|
1581
|
+
if (!key) throw new Error(`No API key for advisor provider "${provider}"`);
|
|
1582
|
+
return key;
|
|
1583
|
+
},
|
|
1584
|
+
intentTracing: false,
|
|
1585
|
+
});
|
|
1586
|
+
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
1587
|
+
|
|
1588
|
+
const advisorAgentFacade: AdvisorAgent = {
|
|
1589
|
+
prompt: input => advisorAgent.prompt(input),
|
|
1590
|
+
abort: reason => advisorAgent.abort(reason),
|
|
1591
|
+
reset: () => {
|
|
1592
|
+
advisorAgent.reset();
|
|
1593
|
+
appendOnlyContext.log.clear();
|
|
1594
|
+
},
|
|
1595
|
+
state: advisorAgent.state,
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
this.#advisorAgent = advisorAgent;
|
|
1599
|
+
this.#advisorRuntime = new AdvisorRuntime(advisorAgentFacade, {
|
|
1600
|
+
snapshotMessages: () => this.agent.state.messages,
|
|
1601
|
+
enqueueAdvice,
|
|
1602
|
+
maintainContext: incomingTokens => this.#maintainAdvisorContext(incomingTokens),
|
|
1603
|
+
});
|
|
1604
|
+
if (seedToCurrent) {
|
|
1605
|
+
this.#advisorRuntime.seedTo(this.agent.state.messages.length);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Batch non-blocking advisor notes into one injected custom message.
|
|
1609
|
+
this.#advisorYieldQueueUnsubscribe = this.yieldQueue.register<AdvisorNote>("advisor", {
|
|
1610
|
+
build: entries =>
|
|
1611
|
+
entries.length === 0
|
|
1612
|
+
? null
|
|
1613
|
+
: ({
|
|
1614
|
+
role: "custom",
|
|
1615
|
+
customType: "advisor",
|
|
1616
|
+
display: true,
|
|
1617
|
+
attribution: "agent",
|
|
1618
|
+
timestamp: Date.now(),
|
|
1619
|
+
content: formatAdvisorBatchContent(entries),
|
|
1620
|
+
details: { notes: entries } satisfies AdvisorMessageDetails,
|
|
1621
|
+
} satisfies CustomMessage),
|
|
1622
|
+
skipIdleFlush: true,
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
return true;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
#stopAdvisorRuntime(): void {
|
|
1629
|
+
if (this.#advisorRuntime) {
|
|
1630
|
+
this.#advisorRuntime.dispose();
|
|
1631
|
+
this.#advisorRuntime = undefined;
|
|
1632
|
+
}
|
|
1633
|
+
if (this.#advisorAgent) {
|
|
1634
|
+
this.#advisorAgent = undefined;
|
|
1635
|
+
}
|
|
1636
|
+
this.#advisorYieldQueueUnsubscribe?.();
|
|
1637
|
+
this.#advisorYieldQueueUnsubscribe = undefined;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
async #promoteAdvisorContextModel(currentModel: Model): Promise<boolean> {
|
|
1641
|
+
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
1642
|
+
if (!promotionSettings.enabled) return false;
|
|
1643
|
+
const contextWindow = currentModel.contextWindow ?? 0;
|
|
1644
|
+
if (contextWindow <= 0) return false;
|
|
1645
|
+
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
1646
|
+
if (!targetModel) return false;
|
|
1647
|
+
|
|
1648
|
+
const advisorSel = resolveRoleSelection(
|
|
1649
|
+
["advisor"],
|
|
1650
|
+
this.settings,
|
|
1651
|
+
this.#modelRegistry.getAvailable(),
|
|
1652
|
+
this.#modelRegistry,
|
|
1653
|
+
);
|
|
1654
|
+
const advisorThinkingLevel = advisorSel?.thinkingLevel ?? ThinkingLevel.Medium;
|
|
1655
|
+
|
|
1656
|
+
try {
|
|
1657
|
+
this.#advisorAgent?.setModel(targetModel);
|
|
1658
|
+
this.#advisorAgent?.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
1659
|
+
this.#advisorAgent?.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
1660
|
+
this.#advisorAgent?.appendOnlyContext?.invalidateForModelChange();
|
|
1661
|
+
logger.debug("Advisor context promotion switched model on overflow", {
|
|
1662
|
+
from: `${currentModel.provider}/${currentModel.id}`,
|
|
1663
|
+
to: `${targetModel.provider}/${targetModel.id}`,
|
|
1664
|
+
});
|
|
1665
|
+
return true;
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
logger.warn("Advisor context promotion failed", {
|
|
1668
|
+
from: `${currentModel.provider}/${currentModel.id}`,
|
|
1669
|
+
to: `${targetModel.provider}/${targetModel.id}`,
|
|
1670
|
+
error: String(error),
|
|
1671
|
+
});
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
async #maintainAdvisorContext(incomingTokens: number): Promise<boolean> {
|
|
1677
|
+
const advisor = this.#advisorAgent;
|
|
1678
|
+
if (!advisor) return false;
|
|
1679
|
+
|
|
1680
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
1681
|
+
if (compactionSettings.strategy === "off") return false;
|
|
1682
|
+
if (!compactionSettings.enabled) return false;
|
|
1683
|
+
|
|
1684
|
+
const advisorModel = advisor.state.model;
|
|
1685
|
+
const contextWindow = advisorModel.contextWindow ?? 0;
|
|
1686
|
+
if (contextWindow <= 0) return false;
|
|
1687
|
+
|
|
1688
|
+
const messages = advisor.state.messages;
|
|
1689
|
+
let contextTokens = incomingTokens;
|
|
1690
|
+
for (const message of messages) {
|
|
1691
|
+
contextTokens += estimateTokens(message);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) {
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// 1. Try promotion first
|
|
1699
|
+
if (await this.#promoteAdvisorContextModel(advisorModel)) {
|
|
1700
|
+
// Promotion succeeded, check if new model has enough space
|
|
1701
|
+
const newModel = advisor.state.model;
|
|
1702
|
+
const newWindow = newModel.contextWindow ?? 0;
|
|
1703
|
+
if (newWindow > 0) {
|
|
1704
|
+
const stillNeedsCompaction = shouldCompact(contextTokens, newWindow, compactionSettings);
|
|
1705
|
+
if (!stillNeedsCompaction) return false;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// 2. Run compaction on advisor messages
|
|
1710
|
+
const pathEntries: SessionEntry[] = messages.map((message, i) => {
|
|
1711
|
+
const id = `msg-${i}`;
|
|
1712
|
+
const parentId = i > 0 ? `msg-${i - 1}` : null;
|
|
1713
|
+
const timestamp = String(message.timestamp || Date.now());
|
|
1714
|
+
|
|
1715
|
+
if (message.role === "compactionSummary") {
|
|
1716
|
+
return {
|
|
1717
|
+
type: "compaction",
|
|
1718
|
+
id,
|
|
1719
|
+
parentId,
|
|
1720
|
+
timestamp,
|
|
1721
|
+
summary: message.summary,
|
|
1722
|
+
shortSummary: message.shortSummary,
|
|
1723
|
+
firstKeptEntryId: (message as any).firstKeptEntryId || `msg-${i + 1}`,
|
|
1724
|
+
tokensBefore: message.tokensBefore,
|
|
1725
|
+
} satisfies CompactionEntry;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
return {
|
|
1729
|
+
type: "message",
|
|
1730
|
+
id,
|
|
1731
|
+
parentId,
|
|
1732
|
+
timestamp,
|
|
1733
|
+
message,
|
|
1734
|
+
} satisfies SessionMessageEntry;
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
1738
|
+
if (!preparation) {
|
|
1739
|
+
// Cannot prepare compaction, fallback to re-prime
|
|
1740
|
+
return true;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const advisorCompactionThinkingLevel: ThinkingLevel | undefined = advisor.state.disableReasoning
|
|
1744
|
+
? ThinkingLevel.Off
|
|
1745
|
+
: advisor.state.thinkingLevel;
|
|
1746
|
+
|
|
1747
|
+
// Advisor state is in-memory-only, so snapcompact's frame archive has no
|
|
1748
|
+
// stable SessionEntry preserveData slot to carry across future advisor
|
|
1749
|
+
// maintenance runs. Use an LLM summary even when the primary session is
|
|
1750
|
+
// configured for snapcompact.
|
|
1751
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
1752
|
+
const candidates = this.#resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
1753
|
+
if (candidates.length === 0) {
|
|
1754
|
+
// No compaction candidates, fallback to re-prime
|
|
1755
|
+
return true;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
let compactResult: CompactionResult | undefined;
|
|
1759
|
+
let lastError: unknown;
|
|
1760
|
+
|
|
1761
|
+
for (const candidate of candidates) {
|
|
1762
|
+
const apiKey = await this.#modelRegistry.getApiKey(
|
|
1763
|
+
candidate,
|
|
1764
|
+
this.sessionId ? `${this.sessionId}-advisor` : undefined,
|
|
1765
|
+
);
|
|
1766
|
+
if (!apiKey) continue;
|
|
1767
|
+
|
|
1768
|
+
try {
|
|
1769
|
+
compactResult = await compact(
|
|
1770
|
+
preparation,
|
|
1771
|
+
candidate,
|
|
1772
|
+
this.#modelRegistry.resolver(candidate, this.sessionId ? `${this.sessionId}-advisor` : undefined),
|
|
1773
|
+
undefined,
|
|
1774
|
+
undefined,
|
|
1775
|
+
{
|
|
1776
|
+
thinkingLevel: advisorCompactionThinkingLevel,
|
|
1777
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
1778
|
+
},
|
|
1779
|
+
);
|
|
1780
|
+
break;
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
lastError = error;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (!compactResult) {
|
|
1787
|
+
logger.warn("Advisor compaction failed, falling back to re-prime", { error: String(lastError) });
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
const summary = compactResult.summary;
|
|
1792
|
+
const shortSummary = compactResult.shortSummary;
|
|
1793
|
+
const firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
1794
|
+
const tokensBefore = compactResult.tokensBefore;
|
|
1795
|
+
|
|
1796
|
+
// Rebuild messages with the compaction summary
|
|
1797
|
+
const summaryMessage = {
|
|
1798
|
+
...createCompactionSummaryMessage(summary, tokensBefore, new Date().toISOString(), shortSummary),
|
|
1799
|
+
firstKeptEntryId,
|
|
1800
|
+
} as CompactionSummaryMessage & { firstKeptEntryId?: string };
|
|
1801
|
+
|
|
1802
|
+
advisor.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1386
1805
|
|
|
1387
1806
|
/** Model registry for API key resolution and model discovery */
|
|
1388
1807
|
get modelRegistry(): ModelRegistry {
|
|
@@ -3213,6 +3632,7 @@ export class AgentSession {
|
|
|
3213
3632
|
this.#pendingIrcAsides = [];
|
|
3214
3633
|
this.yieldQueue.clear();
|
|
3215
3634
|
this.agent.setAsideMessageProvider(undefined);
|
|
3635
|
+
this.#stopAdvisorRuntime();
|
|
3216
3636
|
this.#evalExecutionDisposing = true;
|
|
3217
3637
|
}
|
|
3218
3638
|
|
|
@@ -4675,6 +5095,13 @@ export class AgentSession {
|
|
|
4675
5095
|
// agent-initiated turns never trigger them.
|
|
4676
5096
|
const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
|
|
4677
5097
|
|
|
5098
|
+
// A user-initiated prompt (typed message or the `.`/`c` continue shortcut)
|
|
5099
|
+
// re-enables advisor auto-resume that a prior user interrupt suppressed.
|
|
5100
|
+
// Agent-initiated synthetic prompts (auto-continue, plan, reminders) do not.
|
|
5101
|
+
if (options?.userInitiated ?? !options?.synthetic) {
|
|
5102
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
5103
|
+
}
|
|
5104
|
+
|
|
4678
5105
|
// If streaming, queue via steer() or followUp() based on option
|
|
4679
5106
|
if (this.isStreaming) {
|
|
4680
5107
|
if (!options?.streamingBehavior) {
|
|
@@ -5135,6 +5562,10 @@ export class AgentSession {
|
|
|
5135
5562
|
images: ImageContent[] | undefined,
|
|
5136
5563
|
mode: "steer" | "followUp",
|
|
5137
5564
|
): Promise<void> {
|
|
5565
|
+
// A queued user message (RPC/SDK/collab steer or follow-up, or a typed message
|
|
5566
|
+
// while streaming) is a deliberate resume; re-enable advisor auto-resume that
|
|
5567
|
+
// a user interrupt suppressed.
|
|
5568
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
5138
5569
|
const normalizedImages = await this.#normalizeImagesForModel(images);
|
|
5139
5570
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
5140
5571
|
if (normalizedImages?.length) {
|
|
@@ -5513,6 +5944,12 @@ export class AgentSession {
|
|
|
5513
5944
|
* abort. Omit it for internal/lifecycle aborts.
|
|
5514
5945
|
*/
|
|
5515
5946
|
async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
|
|
5947
|
+
const userInterrupt = options?.reason === USER_INTERRUPT_LABEL;
|
|
5948
|
+
if (userInterrupt) this.#advisorAutoResumeSuppressed = true;
|
|
5949
|
+
// Pull advisor concerns out of the steer/follow-up queues before any await so
|
|
5950
|
+
// the post-abort stranded-message drain can't auto-resume the run on them.
|
|
5951
|
+
// They are re-recorded as visible advice once the agent settles (below).
|
|
5952
|
+
const strandedAdvisorCards = userInterrupt ? this.#extractQueuedAdvisorCards() : [];
|
|
5516
5953
|
// Session switch/compact paths disconnect first; explicit aborts should
|
|
5517
5954
|
// leave any queued steer/follow-up visible for the user rather than
|
|
5518
5955
|
// auto-starting a fresh turn during cleanup.
|
|
@@ -5541,6 +5978,19 @@ export class AgentSession {
|
|
|
5541
5978
|
if (this.#toolChoiceQueue.hasInFlight) {
|
|
5542
5979
|
this.#toolChoiceQueue.reject("aborted");
|
|
5543
5980
|
}
|
|
5981
|
+
// Re-record advisor concerns the interrupt would otherwise strand, as
|
|
5982
|
+
// visible/persisted advice without triggering a turn (the agent is idle
|
|
5983
|
+
// now): cards steered into the queue before the user stopped, plus any
|
|
5984
|
+
// that arrived via enqueueAdvice mid-abort and were parked hidden in
|
|
5985
|
+
// #pendingNextTurnMessages while the turn was still tearing down. Other
|
|
5986
|
+
// deferred next-turn context (non-advisor) stays queued, in order.
|
|
5987
|
+
const parkedAdvisorCards = this.#pendingNextTurnMessages.filter(isAdvisorCard);
|
|
5988
|
+
if (parkedAdvisorCards.length > 0) {
|
|
5989
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(m => !isAdvisorCard(m));
|
|
5990
|
+
}
|
|
5991
|
+
for (const card of [...strandedAdvisorCards, ...parkedAdvisorCards]) {
|
|
5992
|
+
this.#preserveAdvisorCard(card);
|
|
5993
|
+
}
|
|
5544
5994
|
} finally {
|
|
5545
5995
|
this.#abortInProgress = false;
|
|
5546
5996
|
this.#drainStrandedQueuedMessages();
|
|
@@ -5617,6 +6067,7 @@ export class AgentSession {
|
|
|
5617
6067
|
this.#todoReminderAwaitingProgress = false;
|
|
5618
6068
|
this.#planReferenceSent = false;
|
|
5619
6069
|
this.#planReferencePath = "local://PLAN.md";
|
|
6070
|
+
this.#advisorRuntime?.reset();
|
|
5620
6071
|
this.#reconnectToAgent();
|
|
5621
6072
|
|
|
5622
6073
|
// Emit session_switch event with reason "new" to hooks
|
|
@@ -6234,6 +6685,7 @@ export class AgentSession {
|
|
|
6234
6685
|
await this.sessionManager.rewriteEntries();
|
|
6235
6686
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6236
6687
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6688
|
+
this.#advisorRuntime?.reset();
|
|
6237
6689
|
this.#syncTodoPhasesFromBranch();
|
|
6238
6690
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6239
6691
|
return result;
|
|
@@ -6263,9 +6715,9 @@ export class AgentSession {
|
|
|
6263
6715
|
return undefined;
|
|
6264
6716
|
}
|
|
6265
6717
|
|
|
6266
|
-
await this.sessionManager.rewriteEntries();
|
|
6267
6718
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6268
6719
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6720
|
+
this.#advisorRuntime?.reset();
|
|
6269
6721
|
this.#syncTodoPhasesFromBranch();
|
|
6270
6722
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6271
6723
|
return result;
|
|
@@ -6316,6 +6768,7 @@ export class AgentSession {
|
|
|
6316
6768
|
await this.sessionManager.rewriteEntries();
|
|
6317
6769
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6318
6770
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6771
|
+
this.#advisorRuntime?.reset();
|
|
6319
6772
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6320
6773
|
return { removed };
|
|
6321
6774
|
}
|
|
@@ -6366,6 +6819,7 @@ export class AgentSession {
|
|
|
6366
6819
|
await this.sessionManager.rewriteEntries();
|
|
6367
6820
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6368
6821
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6822
|
+
this.#advisorRuntime?.reset();
|
|
6369
6823
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6370
6824
|
|
|
6371
6825
|
return {
|
|
@@ -6582,6 +7036,7 @@ export class AgentSession {
|
|
|
6582
7036
|
const newEntries = this.sessionManager.getEntries();
|
|
6583
7037
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6584
7038
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7039
|
+
this.#advisorRuntime?.reset();
|
|
6585
7040
|
this.#syncTodoPhasesFromBranch();
|
|
6586
7041
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6587
7042
|
|
|
@@ -6801,6 +7256,7 @@ export class AgentSession {
|
|
|
6801
7256
|
// Rebuild agent messages from session
|
|
6802
7257
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6803
7258
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7259
|
+
this.#advisorRuntime?.reset();
|
|
6804
7260
|
this.#syncTodoPhasesFromBranch();
|
|
6805
7261
|
|
|
6806
7262
|
return { document: handoffText, savedPath };
|
|
@@ -6838,8 +7294,11 @@ export class AgentSession {
|
|
|
6838
7294
|
}
|
|
6839
7295
|
|
|
6840
7296
|
let tokens = currentUsage.tokens;
|
|
6841
|
-
|
|
6842
|
-
|
|
7297
|
+
const previousNonMessageTokens = currentEstimate.providerNonMessageTokens;
|
|
7298
|
+
if (previousNonMessageTokens !== undefined) {
|
|
7299
|
+
const currentNonMessageTokens = computeNonMessageTokens(this);
|
|
7300
|
+
const nonMessageTokenGrowth = Math.max(0, currentNonMessageTokens - previousNonMessageTokens);
|
|
7301
|
+
tokens += nonMessageTokenGrowth;
|
|
6843
7302
|
}
|
|
6844
7303
|
for (const message of messages) {
|
|
6845
7304
|
tokens += estimateTokens(message);
|
|
@@ -7235,6 +7694,7 @@ export class AgentSession {
|
|
|
7235
7694
|
}
|
|
7236
7695
|
const safeCount = Math.max(0, Math.min(checkpointState.checkpointMessageCount, this.agent.state.messages.length));
|
|
7237
7696
|
this.agent.replaceMessages(this.agent.state.messages.slice(0, safeCount));
|
|
7697
|
+
this.#advisorRuntime?.reset();
|
|
7238
7698
|
try {
|
|
7239
7699
|
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
7240
7700
|
startedAt: checkpointState.startedAt,
|
|
@@ -7843,6 +8303,10 @@ export class AgentSession {
|
|
|
7843
8303
|
}
|
|
7844
8304
|
|
|
7845
8305
|
#getCompactionModelCandidates(availableModels: Model[]): Model[] {
|
|
8306
|
+
return this.#resolveCompactionModelCandidates(this.model, availableModels);
|
|
8307
|
+
}
|
|
8308
|
+
|
|
8309
|
+
#resolveCompactionModelCandidates(preferredModel: Model | null | undefined, availableModels: Model[]): Model[] {
|
|
7846
8310
|
const candidates: Model[] = [];
|
|
7847
8311
|
const seen = new Set<string>();
|
|
7848
8312
|
|
|
@@ -7854,15 +8318,9 @@ export class AgentSession {
|
|
|
7854
8318
|
candidates.push(model);
|
|
7855
8319
|
};
|
|
7856
8320
|
|
|
7857
|
-
|
|
7858
|
-
// Prefer the active session's model: it's what the user is actively using,
|
|
7859
|
-
// and routing compaction to a different provider (e.g. an OpenAI default
|
|
7860
|
-
// model while the chat is on Anthropic) changes provider-specific behavior
|
|
7861
|
-
// like remote compaction endpoints. Role-based candidates only kick in
|
|
7862
|
-
// as auth fallbacks when the current model has no usable credentials.
|
|
7863
|
-
addCandidate(currentModel);
|
|
8321
|
+
addCandidate(preferredModel ?? undefined);
|
|
7864
8322
|
for (const role of MODEL_ROLE_IDS) {
|
|
7865
|
-
addCandidate(this.#resolveRoleModelFull(role, availableModels,
|
|
8323
|
+
addCandidate(this.#resolveRoleModelFull(role, availableModels, preferredModel ?? undefined).model);
|
|
7866
8324
|
}
|
|
7867
8325
|
|
|
7868
8326
|
const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
|
|
@@ -8420,6 +8878,7 @@ export class AgentSession {
|
|
|
8420
8878
|
const newEntries = this.sessionManager.getEntries();
|
|
8421
8879
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8422
8880
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8881
|
+
this.#advisorRuntime?.reset();
|
|
8423
8882
|
this.#syncTodoPhasesFromBranch();
|
|
8424
8883
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8425
8884
|
|
|
@@ -8690,11 +9149,22 @@ export class AgentSession {
|
|
|
8690
9149
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
8691
9150
|
|
|
8692
9151
|
if (this.#isClassifierRefusal(message)) return true;
|
|
9152
|
+
if (this.#streamInterruptedAfterObservableOutput(message)) return false;
|
|
8693
9153
|
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
8694
9154
|
|
|
8695
9155
|
const err = message.errorMessage;
|
|
8696
9156
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
8697
9157
|
}
|
|
9158
|
+
#streamInterruptedAfterObservableOutput(message: AssistantMessage): boolean {
|
|
9159
|
+
if (message.stopDetails?.type === STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL) return true;
|
|
9160
|
+
for (const block of message.content) {
|
|
9161
|
+
if (block.type === "toolCall") return true;
|
|
9162
|
+
if (block.type === "text" && block.text.length > 0) return true;
|
|
9163
|
+
if (block.type === "thinking" && block.thinking.length > 0) return true;
|
|
9164
|
+
if (block.type === "redactedThinking" && block.data.length > 0) return true;
|
|
9165
|
+
}
|
|
9166
|
+
return false;
|
|
9167
|
+
}
|
|
8698
9168
|
|
|
8699
9169
|
#isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
|
|
8700
9170
|
const currentApi = this.model?.api;
|
|
@@ -10065,6 +10535,7 @@ export class AgentSession {
|
|
|
10065
10535
|
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
10066
10536
|
this.agent.serviceTier = previousServiceTier;
|
|
10067
10537
|
this.#syncTodoPhasesFromBranch();
|
|
10538
|
+
this.#advisorRuntime?.reset();
|
|
10068
10539
|
this.#reconnectToAgent();
|
|
10069
10540
|
if (restoreMcpError) {
|
|
10070
10541
|
throw restoreMcpError;
|
|
@@ -10146,6 +10617,7 @@ export class AgentSession {
|
|
|
10146
10617
|
|
|
10147
10618
|
if (!skipConversationRestore) {
|
|
10148
10619
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10620
|
+
this.#advisorRuntime?.reset();
|
|
10149
10621
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10150
10622
|
}
|
|
10151
10623
|
|
|
@@ -10312,6 +10784,7 @@ export class AgentSession {
|
|
|
10312
10784
|
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
10313
10785
|
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
10314
10786
|
this.agent.replaceMessages(displayContext.messages);
|
|
10787
|
+
this.#advisorRuntime?.reset();
|
|
10315
10788
|
this.#syncTodoPhasesFromBranch();
|
|
10316
10789
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10317
10790
|
|
|
@@ -10808,7 +11281,10 @@ export class AgentSession {
|
|
|
10808
11281
|
* Format the entire session as plain text for clipboard export.
|
|
10809
11282
|
* Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
|
|
10810
11283
|
*/
|
|
10811
|
-
formatSessionAsText(): string {
|
|
11284
|
+
formatSessionAsText(options?: { compact?: boolean }): string {
|
|
11285
|
+
if (options?.compact) {
|
|
11286
|
+
return formatSessionHistoryMarkdown(this.messages);
|
|
11287
|
+
}
|
|
10812
11288
|
return formatSessionDumpText({
|
|
10813
11289
|
messages: this.messages,
|
|
10814
11290
|
systemPrompt: this.agent.state.systemPrompt,
|
|
@@ -10818,6 +11294,184 @@ export class AgentSession {
|
|
|
10818
11294
|
});
|
|
10819
11295
|
}
|
|
10820
11296
|
|
|
11297
|
+
/**
|
|
11298
|
+
* Enable or disable the advisor for this session. The setting is overridden for the session,
|
|
11299
|
+
* and the runtime is started or stopped to match.
|
|
11300
|
+
*
|
|
11301
|
+
* @returns true when the advisor is actively running after the call.
|
|
11302
|
+
*/
|
|
11303
|
+
setAdvisorEnabled(enabled: boolean): boolean {
|
|
11304
|
+
this.#advisorEnabled = enabled;
|
|
11305
|
+
if (enabled) {
|
|
11306
|
+
return this.#buildAdvisorRuntime(true);
|
|
11307
|
+
}
|
|
11308
|
+
this.#stopAdvisorRuntime();
|
|
11309
|
+
return false;
|
|
11310
|
+
}
|
|
11311
|
+
|
|
11312
|
+
/**
|
|
11313
|
+
* Toggle the advisor setting and start/stop the runtime accordingly.
|
|
11314
|
+
*
|
|
11315
|
+
* @returns true when the advisor is actively running after the call.
|
|
11316
|
+
*/
|
|
11317
|
+
toggleAdvisorEnabled(): boolean {
|
|
11318
|
+
return this.setAdvisorEnabled(!this.#advisorEnabled);
|
|
11319
|
+
}
|
|
11320
|
+
|
|
11321
|
+
/**
|
|
11322
|
+
* Whether the advisor setting is enabled for this session.
|
|
11323
|
+
*/
|
|
11324
|
+
isAdvisorEnabled(): boolean {
|
|
11325
|
+
return this.#advisorEnabled;
|
|
11326
|
+
}
|
|
11327
|
+
|
|
11328
|
+
/**
|
|
11329
|
+
* Whether a live advisor agent is attached to this session. True only when
|
|
11330
|
+
* `advisor.enabled` is set AND a model resolved for the `advisor` role AND
|
|
11331
|
+
* the advisor applies to this agent kind — i.e. the actual runtime exists,
|
|
11332
|
+
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
|
11333
|
+
*/
|
|
11334
|
+
isAdvisorActive(): boolean {
|
|
11335
|
+
return this.#advisorAgent !== undefined;
|
|
11336
|
+
}
|
|
11337
|
+
|
|
11338
|
+
/**
|
|
11339
|
+
* Return structured advisor stats for the status command and TUI panel.
|
|
11340
|
+
*/
|
|
11341
|
+
getAdvisorStats(): AdvisorStats {
|
|
11342
|
+
const configured = this.#advisorEnabled;
|
|
11343
|
+
const advisor = this.#advisorAgent;
|
|
11344
|
+
if (!advisor) {
|
|
11345
|
+
return {
|
|
11346
|
+
configured,
|
|
11347
|
+
active: false,
|
|
11348
|
+
contextWindow: 0,
|
|
11349
|
+
contextTokens: 0,
|
|
11350
|
+
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
11351
|
+
cost: 0,
|
|
11352
|
+
messages: { user: 0, assistant: 0, total: 0 },
|
|
11353
|
+
};
|
|
11354
|
+
}
|
|
11355
|
+
const model = advisor.state.model;
|
|
11356
|
+
const messages = advisor.state.messages;
|
|
11357
|
+
const contextTokens = this.#estimateAdvisorContextTokens(messages);
|
|
11358
|
+
let input = 0;
|
|
11359
|
+
let output = 0;
|
|
11360
|
+
let cacheRead = 0;
|
|
11361
|
+
let cacheWrite = 0;
|
|
11362
|
+
let cost = 0;
|
|
11363
|
+
let user = 0;
|
|
11364
|
+
let assistant = 0;
|
|
11365
|
+
for (const message of messages) {
|
|
11366
|
+
if (message.role === "user") user++;
|
|
11367
|
+
if (message.role === "assistant") {
|
|
11368
|
+
assistant++;
|
|
11369
|
+
const assistantMsg = message as AssistantMessage;
|
|
11370
|
+
input += assistantMsg.usage.input;
|
|
11371
|
+
output += assistantMsg.usage.output;
|
|
11372
|
+
cacheRead += assistantMsg.usage.cacheRead;
|
|
11373
|
+
cacheWrite += assistantMsg.usage.cacheWrite;
|
|
11374
|
+
cost += assistantMsg.usage.cost.total;
|
|
11375
|
+
}
|
|
11376
|
+
}
|
|
11377
|
+
return {
|
|
11378
|
+
configured,
|
|
11379
|
+
active: true,
|
|
11380
|
+
model,
|
|
11381
|
+
contextWindow: model.contextWindow ?? 0,
|
|
11382
|
+
contextTokens,
|
|
11383
|
+
tokens: {
|
|
11384
|
+
input,
|
|
11385
|
+
output,
|
|
11386
|
+
cacheRead,
|
|
11387
|
+
cacheWrite,
|
|
11388
|
+
total: input + output + cacheRead + cacheWrite,
|
|
11389
|
+
},
|
|
11390
|
+
cost,
|
|
11391
|
+
messages: { user, assistant, total: messages.length },
|
|
11392
|
+
};
|
|
11393
|
+
}
|
|
11394
|
+
|
|
11395
|
+
/**
|
|
11396
|
+
* Format a concise advisor status line for ACP/text output.
|
|
11397
|
+
*/
|
|
11398
|
+
formatAdvisorStatus(): string {
|
|
11399
|
+
const stats = this.getAdvisorStats();
|
|
11400
|
+
if (!stats.active) {
|
|
11401
|
+
return stats.configured
|
|
11402
|
+
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
11403
|
+
: "Advisor is disabled.";
|
|
11404
|
+
}
|
|
11405
|
+
const model = stats.model!;
|
|
11406
|
+
const contextLine =
|
|
11407
|
+
stats.contextWindow > 0
|
|
11408
|
+
? `Context: ${stats.contextTokens.toLocaleString()} / ${stats.contextWindow.toLocaleString()} tokens (${Math.round((stats.contextTokens / stats.contextWindow) * 100)}%)`
|
|
11409
|
+
: `Context: ${stats.contextTokens.toLocaleString()} tokens`;
|
|
11410
|
+
const spendParts = [
|
|
11411
|
+
`${stats.tokens.input.toLocaleString()} input`,
|
|
11412
|
+
`${stats.tokens.output.toLocaleString()} output`,
|
|
11413
|
+
];
|
|
11414
|
+
if (stats.tokens.cacheRead > 0) spendParts.push(`${stats.tokens.cacheRead.toLocaleString()} cache read`);
|
|
11415
|
+
if (stats.tokens.cacheWrite > 0) spendParts.push(`${stats.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
11416
|
+
const spendLine = `Spend: ${spendParts.join(", ")}, $${stats.cost.toFixed(4)}`;
|
|
11417
|
+
return `Advisor is enabled (${model.provider}/${model.id}). ${contextLine}. ${spendLine}.`;
|
|
11418
|
+
}
|
|
11419
|
+
|
|
11420
|
+
/**
|
|
11421
|
+
* Estimate the advisor's current context tokens. When the advisor has a
|
|
11422
|
+
* recent non-aborted assistant message with usage, use that prompt's token
|
|
11423
|
+
* count and add a trailing estimate for messages after it. Otherwise estimate
|
|
11424
|
+
* every message.
|
|
11425
|
+
*/
|
|
11426
|
+
#estimateAdvisorContextTokens(messages: AgentMessage[]): number {
|
|
11427
|
+
let lastUsageIndex: number | null = null;
|
|
11428
|
+
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
11429
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11430
|
+
const msg = messages[i];
|
|
11431
|
+
if (msg.role === "assistant") {
|
|
11432
|
+
const assistantMsg = msg as AssistantMessage;
|
|
11433
|
+
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
|
11434
|
+
lastUsage = assistantMsg.usage;
|
|
11435
|
+
lastUsageIndex = i;
|
|
11436
|
+
break;
|
|
11437
|
+
}
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11440
|
+
if (!lastUsage || lastUsageIndex === null) {
|
|
11441
|
+
let estimated = 0;
|
|
11442
|
+
for (const message of messages) {
|
|
11443
|
+
estimated += estimateTokens(message);
|
|
11444
|
+
}
|
|
11445
|
+
return estimated;
|
|
11446
|
+
}
|
|
11447
|
+
let trailingTokens = 0;
|
|
11448
|
+
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
11449
|
+
trailingTokens += estimateTokens(messages[i]);
|
|
11450
|
+
}
|
|
11451
|
+
return calculatePromptTokens(lastUsage) + trailingTokens;
|
|
11452
|
+
}
|
|
11453
|
+
|
|
11454
|
+
/**
|
|
11455
|
+
* Format the advisor agent's own transcript (its system prompt, config,
|
|
11456
|
+
* tools, and the markdown deltas it received plus its thinking/advise/read
|
|
11457
|
+
* calls) as plain text — the advisor-side equivalent of
|
|
11458
|
+
* {@link formatSessionAsText}. Returns null when no advisor is active.
|
|
11459
|
+
*/
|
|
11460
|
+
formatAdvisorHistoryAsText(options?: { compact?: boolean }): string | null {
|
|
11461
|
+
const advisor = this.#advisorAgent;
|
|
11462
|
+
if (!advisor) return null;
|
|
11463
|
+
if (options?.compact) {
|
|
11464
|
+
return formatSessionHistoryMarkdown(advisor.state.messages);
|
|
11465
|
+
}
|
|
11466
|
+
return formatSessionDumpText({
|
|
11467
|
+
messages: advisor.state.messages,
|
|
11468
|
+
systemPrompt: advisor.state.systemPrompt,
|
|
11469
|
+
model: advisor.state.model,
|
|
11470
|
+
thinkingLevel: advisor.state.thinkingLevel,
|
|
11471
|
+
tools: advisor.state.tools,
|
|
11472
|
+
});
|
|
11473
|
+
}
|
|
11474
|
+
|
|
10821
11475
|
// =========================================================================
|
|
10822
11476
|
// Extension System
|
|
10823
11477
|
// =========================================================================
|