@oh-my-pi/pi-coding-agent 15.13.2 → 16.0.0
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 +62 -0
- package/dist/cli.js +587 -499
- 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 +75 -5
- package/dist/types/eval/js/context-manager.d.ts +15 -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 +4 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +71 -2
- package/dist/types/session/session-history-format.d.ts +4 -0
- package/dist/types/session/unexpected-stop-classifier.d.ts +13 -0
- package/dist/types/session/yield-queue.d.ts +2 -0
- package/dist/types/stt/asr-client.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +1 -1
- 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/tts/tts-client.d.ts +1 -1
- package/dist/types/utils/thinking-display.d.ts +1 -17
- 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.ts +25 -12
- package/src/config/model-registry.ts +6 -2
- package/src/config/model-roles.ts +13 -1
- package/src/config/settings-schema.ts +67 -5
- package/src/eval/__tests__/agent-bridge.test.ts +106 -46
- package/src/eval/__tests__/js-context-manager.test.ts +12 -2
- package/src/eval/js/context-manager.ts +40 -3
- package/src/eval/js/worker-entry.ts +7 -0
- package/src/export/html/template.js +18 -22
- package/src/internal-urls/docs-index.generated.ts +8 -5
- package/src/main.ts +19 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/advisor-message.ts +99 -0
- package/src/modes/components/agent-hub.ts +38 -7
- package/src/modes/components/assistant-message.ts +110 -15
- package/src/modes/components/snapcompact-shape-preview-doc.md +2 -2
- package/src/modes/components/snapcompact-shape-preview.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -7
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/controllers/command-controller.ts +69 -2
- package/src/modes/controllers/event-controller.ts +3 -3
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/streaming-reveal.ts +4 -4
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/ui-helpers.ts +12 -3
- package/src/prompts/advisor/advise-tool.md +1 -0
- package/src/prompts/advisor/system.md +31 -0
- package/src/prompts/agents/oracle.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/unexpected-stop-classifier.md +17 -0
- package/src/prompts/system/unexpected-stop-retry.md +4 -0
- package/src/sdk.ts +52 -13
- package/src/session/agent-session.ts +722 -21
- package/src/session/session-dump-format.ts +15 -142
- package/src/session/session-history-format.ts +30 -11
- package/src/session/unexpected-stop-classifier.ts +129 -0
- package/src/session/yield-queue.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +102 -4
- package/src/stt/asr-client.ts +1 -1
- package/src/system-prompt.ts +1 -1
- package/src/tiny/title-client.ts +1 -1
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser/tab-worker-entry.ts +12 -4
- package/src/tools/job.ts +1 -0
- package/src/tools/path-utils.ts +33 -2
- package/src/tools/report-tool-issue.ts +2 -7
- package/src/tts/tts-client.ts +1 -1
- package/src/utils/thinking-display.ts +8 -34
- package/src/web/scrapers/docs-rs.ts +2 -3
|
@@ -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,6 +30,7 @@ import {
|
|
|
30
30
|
type AgentTool,
|
|
31
31
|
AppendOnlyContextManager,
|
|
32
32
|
type AsideMessage,
|
|
33
|
+
type CompactionSummaryMessage,
|
|
33
34
|
resolveTelemetry,
|
|
34
35
|
ThinkingLevel,
|
|
35
36
|
} from "@oh-my-pi/pi-agent-core";
|
|
@@ -54,6 +55,8 @@ import {
|
|
|
54
55
|
generateHandoff,
|
|
55
56
|
prepareCompaction,
|
|
56
57
|
resolveThresholdTokens,
|
|
58
|
+
type SessionEntry,
|
|
59
|
+
type SessionMessageEntry,
|
|
57
60
|
type ShakeConfig,
|
|
58
61
|
type ShakeRegion,
|
|
59
62
|
type SummaryOptions,
|
|
@@ -114,6 +117,16 @@ import {
|
|
|
114
117
|
Snowflake,
|
|
115
118
|
} from "@oh-my-pi/pi-utils";
|
|
116
119
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
120
|
+
import {
|
|
121
|
+
AdviseTool,
|
|
122
|
+
type AdvisorAgent,
|
|
123
|
+
type AdvisorMessageDetails,
|
|
124
|
+
type AdvisorNote,
|
|
125
|
+
AdvisorRuntime,
|
|
126
|
+
type AdvisorSeverity,
|
|
127
|
+
formatAdvisorBatchContent,
|
|
128
|
+
isInterruptingSeverity,
|
|
129
|
+
} from "../advisor";
|
|
117
130
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
118
131
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
119
132
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -129,6 +142,7 @@ import {
|
|
|
129
142
|
parseModelString,
|
|
130
143
|
type ResolvedModelRoleValue,
|
|
131
144
|
resolveModelRoleValue,
|
|
145
|
+
resolveRoleSelection,
|
|
132
146
|
} from "../config/model-resolver";
|
|
133
147
|
import { MODEL_ROLE_IDS } from "../config/model-roles";
|
|
134
148
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
@@ -189,6 +203,7 @@ import { computeNonMessageTokens } from "../modes/utils/context-usage";
|
|
|
189
203
|
import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
|
|
190
204
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
191
205
|
import type { PlanModeState } from "../plan-mode/state";
|
|
206
|
+
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
192
207
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
193
208
|
import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
|
|
194
209
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
@@ -202,6 +217,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
202
217
|
};
|
|
203
218
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
204
219
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
220
|
+
import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
|
|
205
221
|
import {
|
|
206
222
|
deobfuscateSessionContext,
|
|
207
223
|
obfuscateProviderContext,
|
|
@@ -267,9 +283,11 @@ import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-
|
|
|
267
283
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
268
284
|
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
|
|
269
285
|
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
|
|
286
|
+
import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
270
287
|
import type { SessionManager } from "./session-manager";
|
|
271
288
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
272
289
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
290
|
+
import { classifyUnexpectedStop, isUnexpectedStopCandidate } from "./unexpected-stop-classifier";
|
|
273
291
|
import { YieldQueue } from "./yield-queue";
|
|
274
292
|
|
|
275
293
|
/** Session-specific events that extend the core AgentEvent */
|
|
@@ -308,14 +326,16 @@ export type AgentSessionEvent =
|
|
|
308
326
|
resolved?: Effort;
|
|
309
327
|
}
|
|
310
328
|
| { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
|
|
311
|
-
|
|
312
329
|
/** Listener function for agent session events */
|
|
313
330
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
314
|
-
export type CommandMetadataChangedListener = () => void | Promise<void>;
|
|
315
|
-
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
316
331
|
|
|
332
|
+
const UNEXPECTED_STOP_MAX_RETRIES = 3;
|
|
333
|
+
const UNEXPECTED_STOP_TIMEOUT_MS = 4000;
|
|
317
334
|
const EMPTY_STOP_MAX_RETRIES = 3;
|
|
318
335
|
const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
336
|
+
export type CommandMetadataChangedListener = () => void | Promise<void>;
|
|
337
|
+
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
338
|
+
|
|
319
339
|
const RETRY_BACKOFF_JITTER_RATIO = 0.25;
|
|
320
340
|
/**
|
|
321
341
|
* Hysteresis band for the post-shake "did we actually create headroom?" check.
|
|
@@ -453,6 +473,15 @@ export interface AgentSessionConfig {
|
|
|
453
473
|
* so that credential sticky selection is consistent with the session's streaming calls.
|
|
454
474
|
*/
|
|
455
475
|
providerSessionId?: string;
|
|
476
|
+
/**
|
|
477
|
+
* Hard-isolated read-only tools (read/search/find) for the advisor agent,
|
|
478
|
+
* pre-built in `createAgentSession` against a distinct `ToolSession` so the
|
|
479
|
+
* advisor's reads never share the primary's snapshot/seen-lines/conflict
|
|
480
|
+
* caches. Undefined when the advisor is disabled.
|
|
481
|
+
*/
|
|
482
|
+
advisorReadOnlyTools?: AgentTool[];
|
|
483
|
+
/** Preloaded watchdog prompt content for the advisor. */
|
|
484
|
+
advisorWatchdogPrompt?: string;
|
|
456
485
|
}
|
|
457
486
|
|
|
458
487
|
/** Options for AgentSession.prompt() */
|
|
@@ -535,6 +564,28 @@ export interface SessionStats {
|
|
|
535
564
|
cost: number;
|
|
536
565
|
}
|
|
537
566
|
|
|
567
|
+
/** Advisor statistics for /advisor status command. */
|
|
568
|
+
export interface AdvisorStats {
|
|
569
|
+
configured: boolean;
|
|
570
|
+
active: boolean;
|
|
571
|
+
model?: Model;
|
|
572
|
+
contextWindow: number;
|
|
573
|
+
contextTokens: number;
|
|
574
|
+
tokens: {
|
|
575
|
+
input: number;
|
|
576
|
+
output: number;
|
|
577
|
+
cacheRead: number;
|
|
578
|
+
cacheWrite: number;
|
|
579
|
+
total: number;
|
|
580
|
+
};
|
|
581
|
+
cost: number;
|
|
582
|
+
messages: {
|
|
583
|
+
user: number;
|
|
584
|
+
assistant: number;
|
|
585
|
+
total: number;
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
538
589
|
export interface FreshSessionResult {
|
|
539
590
|
previousSessionId: string;
|
|
540
591
|
sessionId: string;
|
|
@@ -940,6 +991,12 @@ export class AgentSession {
|
|
|
940
991
|
#planModeState: PlanModeState | undefined;
|
|
941
992
|
#goalModeState: GoalModeState | undefined;
|
|
942
993
|
#goalRuntime: GoalRuntime;
|
|
994
|
+
#advisorRuntime?: AdvisorRuntime;
|
|
995
|
+
/** The advisor's own agent, retained so `/dump advisor` can serialize its transcript. Undefined when no advisor is active. */
|
|
996
|
+
#advisorAgent?: Agent;
|
|
997
|
+
#advisorReadOnlyTools?: AgentTool[];
|
|
998
|
+
#advisorWatchdogPrompt?: string;
|
|
999
|
+
#advisorYieldQueueUnsubscribe?: () => void;
|
|
943
1000
|
#goalTurnCounter = 0;
|
|
944
1001
|
#planReferenceSent = false;
|
|
945
1002
|
#planReferencePath = "local://PLAN.md";
|
|
@@ -1106,15 +1163,23 @@ export class AgentSession {
|
|
|
1106
1163
|
// `#emit(event)` that reaches external subscribers (rpc-mode stdout, ACP bridge,
|
|
1107
1164
|
// Cursor exec, TUI listeners) is held back. Without this, a client that resumes
|
|
1108
1165
|
// on `agent_end` can fire its next `prompt` before #promptWithMessage's finally
|
|
1109
|
-
|
|
1110
|
-
|
|
1166
|
+
#emptyStopRetryCount = 0;
|
|
1167
|
+
#unexpectedStopRetryCount = 0;
|
|
1168
|
+
#promptGeneration = 0;
|
|
1111
1169
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1170
|
+
#pendingProviderRequestNonMessageTokens: number | undefined = undefined;
|
|
1171
|
+
#lastProviderUsageNonMessage:
|
|
1172
|
+
| {
|
|
1173
|
+
provider: AssistantMessage["provider"];
|
|
1174
|
+
model: AssistantMessage["model"];
|
|
1175
|
+
timestamp: AssistantMessage["timestamp"];
|
|
1176
|
+
tokens: number;
|
|
1177
|
+
}
|
|
1178
|
+
| undefined;
|
|
1112
1179
|
#obfuscator: SecretObfuscator | undefined;
|
|
1113
1180
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1114
1181
|
#pendingRewindReport: string | undefined = undefined;
|
|
1115
1182
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1116
|
-
#emptyStopRetryCount = 0;
|
|
1117
|
-
#promptGeneration = 0;
|
|
1118
1183
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
1119
1184
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
1120
1185
|
readonly rawSseDebugBuffer: RawSseDebugBuffer;
|
|
@@ -1222,6 +1287,8 @@ export class AgentSession {
|
|
|
1222
1287
|
this.#customCommands = config.customCommands ?? [];
|
|
1223
1288
|
this.#skillsSettings = config.skillsSettings;
|
|
1224
1289
|
this.#modelRegistry = config.modelRegistry;
|
|
1290
|
+
this.#advisorReadOnlyTools = config.advisorReadOnlyTools;
|
|
1291
|
+
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1225
1292
|
this.#validateRetryFallbackChains();
|
|
1226
1293
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
1227
1294
|
this.#requestedToolNames = config.requestedToolNames;
|
|
@@ -1254,6 +1321,17 @@ export class AgentSession {
|
|
|
1254
1321
|
};
|
|
1255
1322
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
1256
1323
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1324
|
+
this.agent.setOnTurnEnd(async (messages, signal) => {
|
|
1325
|
+
if (signal?.aborted) return;
|
|
1326
|
+
if (this.#advisorRuntime && !this.#advisorRuntime.disposed) {
|
|
1327
|
+
this.#advisorRuntime.onTurnEnd(messages);
|
|
1328
|
+
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
1329
|
+
if (syncBacklog !== "off") {
|
|
1330
|
+
const threshold = parseInt(syncBacklog, 10);
|
|
1331
|
+
await this.#advisorRuntime.waitForCatchup(30000, threshold, signal);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1257
1335
|
this.yieldQueue = new YieldQueue({
|
|
1258
1336
|
isStreaming: () => this.isStreaming,
|
|
1259
1337
|
injectIdle: async messages => {
|
|
@@ -1365,12 +1443,304 @@ export class AgentSession {
|
|
|
1365
1443
|
},
|
|
1366
1444
|
});
|
|
1367
1445
|
|
|
1446
|
+
if (this.settings.get("advisor.enabled")) this.#buildAdvisorRuntime();
|
|
1447
|
+
|
|
1368
1448
|
// Always subscribe to agent events for internal handling
|
|
1369
1449
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
1370
1450
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
1371
1451
|
// Re-evaluate append-only context mode when the setting changes at runtime.
|
|
1372
1452
|
this.#unsubscribeAppendOnly = onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model));
|
|
1373
1453
|
}
|
|
1454
|
+
// -------------------------------------------------------------------------
|
|
1455
|
+
// Advisor runtime lifecycle
|
|
1456
|
+
// -------------------------------------------------------------------------
|
|
1457
|
+
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1458
|
+
if (this.#isDisposed) return false;
|
|
1459
|
+
if (this.#advisorRuntime) return true;
|
|
1460
|
+
if (!this.settings.get("advisor.enabled")) return false;
|
|
1461
|
+
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1462
|
+
|
|
1463
|
+
const advisorSel = resolveRoleSelection(
|
|
1464
|
+
["advisor"],
|
|
1465
|
+
this.settings,
|
|
1466
|
+
this.#modelRegistry.getAvailable(),
|
|
1467
|
+
this.#modelRegistry,
|
|
1468
|
+
);
|
|
1469
|
+
if (!advisorSel) {
|
|
1470
|
+
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive");
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Concern and blocker interrupt the running agent through the steering
|
|
1475
|
+
// channel (aborting in-flight tools at the next steering boundary); when
|
|
1476
|
+
// the loop has already yielded, triggerTurn resumes it so the advice is
|
|
1477
|
+
// acted on immediately rather than waiting for the next user prompt. A
|
|
1478
|
+
// plain nit rides the non-interrupting YieldQueue aside.
|
|
1479
|
+
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1480
|
+
if (isInterruptingSeverity(severity)) {
|
|
1481
|
+
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1482
|
+
void this.sendCustomMessage(
|
|
1483
|
+
{
|
|
1484
|
+
customType: "advisor",
|
|
1485
|
+
content: formatAdvisorBatchContent(notes),
|
|
1486
|
+
display: true,
|
|
1487
|
+
attribution: "agent",
|
|
1488
|
+
details: { notes } satisfies AdvisorMessageDetails,
|
|
1489
|
+
},
|
|
1490
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
1491
|
+
).catch(err => logger.debug("advisor steer failed", { err: String(err) }));
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
const adviseTool = new AdviseTool(enqueueAdvice);
|
|
1498
|
+
const advisorReadOnlyTools = this.#advisorReadOnlyTools ?? [];
|
|
1499
|
+
|
|
1500
|
+
const appendOnlyContext = new AppendOnlyContextManager();
|
|
1501
|
+
const advisorThinkingLevel = advisorSel.thinkingLevel ?? ThinkingLevel.Medium;
|
|
1502
|
+
const systemPrompt = [advisorSystemPrompt];
|
|
1503
|
+
if (this.#advisorWatchdogPrompt) {
|
|
1504
|
+
systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
1505
|
+
}
|
|
1506
|
+
const advisorAgent = new Agent({
|
|
1507
|
+
initialState: {
|
|
1508
|
+
systemPrompt,
|
|
1509
|
+
model: advisorSel.model,
|
|
1510
|
+
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
1511
|
+
tools: [adviseTool, ...advisorReadOnlyTools],
|
|
1512
|
+
},
|
|
1513
|
+
appendOnlyContext,
|
|
1514
|
+
sessionId: this.sessionId ? `${this.sessionId}-advisor` : undefined,
|
|
1515
|
+
getApiKey: async provider => {
|
|
1516
|
+
const key = await this.#modelRegistry.getApiKeyForProvider(
|
|
1517
|
+
provider,
|
|
1518
|
+
this.sessionId ? `${this.sessionId}-advisor` : undefined,
|
|
1519
|
+
);
|
|
1520
|
+
if (!key) throw new Error(`No API key for advisor provider "${provider}"`);
|
|
1521
|
+
return key;
|
|
1522
|
+
},
|
|
1523
|
+
intentTracing: false,
|
|
1524
|
+
});
|
|
1525
|
+
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
1526
|
+
|
|
1527
|
+
const advisorAgentFacade: AdvisorAgent = {
|
|
1528
|
+
prompt: input => advisorAgent.prompt(input),
|
|
1529
|
+
abort: reason => advisorAgent.abort(reason),
|
|
1530
|
+
reset: () => {
|
|
1531
|
+
advisorAgent.reset();
|
|
1532
|
+
appendOnlyContext.log.clear();
|
|
1533
|
+
},
|
|
1534
|
+
state: advisorAgent.state,
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
this.#advisorAgent = advisorAgent;
|
|
1538
|
+
this.#advisorRuntime = new AdvisorRuntime(advisorAgentFacade, {
|
|
1539
|
+
snapshotMessages: () => this.agent.state.messages,
|
|
1540
|
+
enqueueAdvice,
|
|
1541
|
+
maintainContext: incomingTokens => this.#maintainAdvisorContext(incomingTokens),
|
|
1542
|
+
});
|
|
1543
|
+
if (seedToCurrent) {
|
|
1544
|
+
this.#advisorRuntime.seedTo(this.agent.state.messages.length);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Batch non-blocking advisor notes into one injected custom message.
|
|
1548
|
+
this.#advisorYieldQueueUnsubscribe = this.yieldQueue.register<AdvisorNote>("advisor", {
|
|
1549
|
+
build: entries =>
|
|
1550
|
+
entries.length === 0
|
|
1551
|
+
? null
|
|
1552
|
+
: ({
|
|
1553
|
+
role: "custom",
|
|
1554
|
+
customType: "advisor",
|
|
1555
|
+
display: true,
|
|
1556
|
+
attribution: "agent",
|
|
1557
|
+
timestamp: Date.now(),
|
|
1558
|
+
content: formatAdvisorBatchContent(entries),
|
|
1559
|
+
details: { notes: entries } satisfies AdvisorMessageDetails,
|
|
1560
|
+
} satisfies CustomMessage),
|
|
1561
|
+
skipIdleFlush: true,
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
return true;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
#stopAdvisorRuntime(): void {
|
|
1568
|
+
if (this.#advisorRuntime) {
|
|
1569
|
+
this.#advisorRuntime.dispose();
|
|
1570
|
+
this.#advisorRuntime = undefined;
|
|
1571
|
+
}
|
|
1572
|
+
if (this.#advisorAgent) {
|
|
1573
|
+
this.#advisorAgent = undefined;
|
|
1574
|
+
}
|
|
1575
|
+
this.#advisorYieldQueueUnsubscribe?.();
|
|
1576
|
+
this.#advisorYieldQueueUnsubscribe = undefined;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
async #promoteAdvisorContextModel(currentModel: Model): Promise<boolean> {
|
|
1580
|
+
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
1581
|
+
if (!promotionSettings.enabled) return false;
|
|
1582
|
+
const contextWindow = currentModel.contextWindow ?? 0;
|
|
1583
|
+
if (contextWindow <= 0) return false;
|
|
1584
|
+
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
1585
|
+
if (!targetModel) return false;
|
|
1586
|
+
|
|
1587
|
+
const advisorSel = resolveRoleSelection(
|
|
1588
|
+
["advisor"],
|
|
1589
|
+
this.settings,
|
|
1590
|
+
this.#modelRegistry.getAvailable(),
|
|
1591
|
+
this.#modelRegistry,
|
|
1592
|
+
);
|
|
1593
|
+
const advisorThinkingLevel = advisorSel?.thinkingLevel ?? ThinkingLevel.Medium;
|
|
1594
|
+
|
|
1595
|
+
try {
|
|
1596
|
+
this.#advisorAgent?.setModel(targetModel);
|
|
1597
|
+
this.#advisorAgent?.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
1598
|
+
this.#advisorAgent?.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
1599
|
+
this.#advisorAgent?.appendOnlyContext?.invalidateForModelChange();
|
|
1600
|
+
logger.debug("Advisor context promotion switched model on overflow", {
|
|
1601
|
+
from: `${currentModel.provider}/${currentModel.id}`,
|
|
1602
|
+
to: `${targetModel.provider}/${targetModel.id}`,
|
|
1603
|
+
});
|
|
1604
|
+
return true;
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
logger.warn("Advisor context promotion failed", {
|
|
1607
|
+
from: `${currentModel.provider}/${currentModel.id}`,
|
|
1608
|
+
to: `${targetModel.provider}/${targetModel.id}`,
|
|
1609
|
+
error: String(error),
|
|
1610
|
+
});
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
async #maintainAdvisorContext(incomingTokens: number): Promise<boolean> {
|
|
1616
|
+
const advisor = this.#advisorAgent;
|
|
1617
|
+
if (!advisor) return false;
|
|
1618
|
+
|
|
1619
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
1620
|
+
if (compactionSettings.strategy === "off") return false;
|
|
1621
|
+
if (!compactionSettings.enabled) return false;
|
|
1622
|
+
|
|
1623
|
+
const advisorModel = advisor.state.model;
|
|
1624
|
+
const contextWindow = advisorModel.contextWindow ?? 0;
|
|
1625
|
+
if (contextWindow <= 0) return false;
|
|
1626
|
+
|
|
1627
|
+
const messages = advisor.state.messages;
|
|
1628
|
+
let contextTokens = incomingTokens;
|
|
1629
|
+
for (const message of messages) {
|
|
1630
|
+
contextTokens += estimateTokens(message);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) {
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// 1. Try promotion first
|
|
1638
|
+
if (await this.#promoteAdvisorContextModel(advisorModel)) {
|
|
1639
|
+
// Promotion succeeded, check if new model has enough space
|
|
1640
|
+
const newModel = advisor.state.model;
|
|
1641
|
+
const newWindow = newModel.contextWindow ?? 0;
|
|
1642
|
+
if (newWindow > 0) {
|
|
1643
|
+
const stillNeedsCompaction = shouldCompact(contextTokens, newWindow, compactionSettings);
|
|
1644
|
+
if (!stillNeedsCompaction) return false;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// 2. Run compaction on advisor messages
|
|
1649
|
+
const pathEntries: SessionEntry[] = messages.map((message, i) => {
|
|
1650
|
+
const id = `msg-${i}`;
|
|
1651
|
+
const parentId = i > 0 ? `msg-${i - 1}` : null;
|
|
1652
|
+
const timestamp = String(message.timestamp || Date.now());
|
|
1653
|
+
|
|
1654
|
+
if (message.role === "compactionSummary") {
|
|
1655
|
+
return {
|
|
1656
|
+
type: "compaction",
|
|
1657
|
+
id,
|
|
1658
|
+
parentId,
|
|
1659
|
+
timestamp,
|
|
1660
|
+
summary: message.summary,
|
|
1661
|
+
shortSummary: message.shortSummary,
|
|
1662
|
+
firstKeptEntryId: (message as any).firstKeptEntryId || `msg-${i + 1}`,
|
|
1663
|
+
tokensBefore: message.tokensBefore,
|
|
1664
|
+
} satisfies CompactionEntry;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
return {
|
|
1668
|
+
type: "message",
|
|
1669
|
+
id,
|
|
1670
|
+
parentId,
|
|
1671
|
+
timestamp,
|
|
1672
|
+
message,
|
|
1673
|
+
} satisfies SessionMessageEntry;
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
1677
|
+
if (!preparation) {
|
|
1678
|
+
// Cannot prepare compaction, fallback to re-prime
|
|
1679
|
+
return true;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
const advisorCompactionThinkingLevel: ThinkingLevel | undefined = advisor.state.disableReasoning
|
|
1683
|
+
? ThinkingLevel.Off
|
|
1684
|
+
: advisor.state.thinkingLevel;
|
|
1685
|
+
|
|
1686
|
+
// Advisor state is in-memory-only, so snapcompact's frame archive has no
|
|
1687
|
+
// stable SessionEntry preserveData slot to carry across future advisor
|
|
1688
|
+
// maintenance runs. Use an LLM summary even when the primary session is
|
|
1689
|
+
// configured for snapcompact.
|
|
1690
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
1691
|
+
const candidates = this.#resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
1692
|
+
if (candidates.length === 0) {
|
|
1693
|
+
// No compaction candidates, fallback to re-prime
|
|
1694
|
+
return true;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
let compactResult: CompactionResult | undefined;
|
|
1698
|
+
let lastError: unknown;
|
|
1699
|
+
|
|
1700
|
+
for (const candidate of candidates) {
|
|
1701
|
+
const apiKey = await this.#modelRegistry.getApiKey(
|
|
1702
|
+
candidate,
|
|
1703
|
+
this.sessionId ? `${this.sessionId}-advisor` : undefined,
|
|
1704
|
+
);
|
|
1705
|
+
if (!apiKey) continue;
|
|
1706
|
+
|
|
1707
|
+
try {
|
|
1708
|
+
compactResult = await compact(
|
|
1709
|
+
preparation,
|
|
1710
|
+
candidate,
|
|
1711
|
+
this.#modelRegistry.resolver(candidate, this.sessionId ? `${this.sessionId}-advisor` : undefined),
|
|
1712
|
+
undefined,
|
|
1713
|
+
undefined,
|
|
1714
|
+
{
|
|
1715
|
+
thinkingLevel: advisorCompactionThinkingLevel,
|
|
1716
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
1717
|
+
},
|
|
1718
|
+
);
|
|
1719
|
+
break;
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
lastError = error;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (!compactResult) {
|
|
1726
|
+
logger.warn("Advisor compaction failed, falling back to re-prime", { error: String(lastError) });
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const summary = compactResult.summary;
|
|
1731
|
+
const shortSummary = compactResult.shortSummary;
|
|
1732
|
+
const firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
1733
|
+
const tokensBefore = compactResult.tokensBefore;
|
|
1734
|
+
|
|
1735
|
+
// Rebuild messages with the compaction summary
|
|
1736
|
+
const summaryMessage = {
|
|
1737
|
+
...createCompactionSummaryMessage(summary, tokensBefore, new Date().toISOString(), shortSummary),
|
|
1738
|
+
firstKeptEntryId,
|
|
1739
|
+
} as CompactionSummaryMessage & { firstKeptEntryId?: string };
|
|
1740
|
+
|
|
1741
|
+
advisor.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1374
1744
|
|
|
1375
1745
|
/** Model registry for API key resolution and model discovery */
|
|
1376
1746
|
get modelRegistry(): ModelRegistry {
|
|
@@ -1619,8 +1989,34 @@ export class AgentSession {
|
|
|
1619
1989
|
// Track last assistant message for auto-compaction check
|
|
1620
1990
|
#lastAssistantMessage: AssistantMessage | undefined = undefined;
|
|
1621
1991
|
|
|
1622
|
-
/** Internal handler for agent events - shared by subscribe and reconnect
|
|
1992
|
+
/** Internal handler for agent events - shared by subscribe and reconnect.
|
|
1993
|
+
*
|
|
1994
|
+
* `agent_end` handling schedules deferred post-prompt recovery work
|
|
1995
|
+
* (compaction/handoff, context-promotion continuations). It is invoked
|
|
1996
|
+
* fire-and-forget by the agent's synchronous `#emit`, and only reaches
|
|
1997
|
+
* `#checkCompaction` after several internal awaits. `prompt()` runs
|
|
1998
|
+
* `#waitForPostPromptRecovery()` the instant `agent.prompt()` resolves — which
|
|
1999
|
+
* can land BEFORE the handler registers its tasks, so the wait would observe an
|
|
2000
|
+
* empty task set and return early, letting a deferred handoff/promotion race
|
|
2001
|
+
* prompt completion. Tracking the `agent_end` handler as a post-prompt task
|
|
2002
|
+
* that is registered SYNCHRONOUSLY (before the first await) closes that window:
|
|
2003
|
+
* `#postPromptTasksPromise` is set the moment `#emit` invokes this handler, so
|
|
2004
|
+
* the recovery wait always sees the in-flight handler and blocks until it — and
|
|
2005
|
+
* everything it schedules — settles. */
|
|
1623
2006
|
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
2007
|
+
if (event.type !== "agent_end") {
|
|
2008
|
+
return this.#processAgentEvent(event);
|
|
2009
|
+
}
|
|
2010
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
2011
|
+
this.#trackPostPromptTask(promise);
|
|
2012
|
+
try {
|
|
2013
|
+
await this.#processAgentEvent(event);
|
|
2014
|
+
} finally {
|
|
2015
|
+
resolve();
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
#processAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
1624
2020
|
// Plan-mode → compaction transition: stamp `SILENT_ABORT_MARKER` on the
|
|
1625
2021
|
// persisted message BEFORE the obfuscator's display-side copy below.
|
|
1626
2022
|
// Invariant (must hold across refactors): this branch precedes the
|
|
@@ -1787,6 +2183,14 @@ export class AgentSession {
|
|
|
1787
2183
|
if (event.message.role === "assistant") {
|
|
1788
2184
|
this.#lastAssistantMessage = event.message;
|
|
1789
2185
|
const assistantMsg = event.message as AssistantMessage;
|
|
2186
|
+
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
|
2187
|
+
this.#lastProviderUsageNonMessage = {
|
|
2188
|
+
provider: assistantMsg.provider,
|
|
2189
|
+
model: assistantMsg.model,
|
|
2190
|
+
timestamp: assistantMsg.timestamp,
|
|
2191
|
+
tokens: this.#pendingProviderRequestNonMessageTokens ?? computeNonMessageTokens(this),
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
1790
2194
|
const currentGrantsAnthropicPriority =
|
|
1791
2195
|
this.serviceTier === "priority" || this.serviceTier === "claude-only";
|
|
1792
2196
|
if (assistantMsg.disabledFeatures?.includes("priority") && currentGrantsAnthropicPriority) {
|
|
@@ -1933,6 +2337,9 @@ export class AgentSession {
|
|
|
1933
2337
|
if (await this.#handleEmptyAssistantStop(msg)) {
|
|
1934
2338
|
return;
|
|
1935
2339
|
}
|
|
2340
|
+
if (await this.#handleUnexpectedAssistantStop(msg)) {
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
1936
2343
|
|
|
1937
2344
|
// A deliberate abort should settle the current turn, not trigger queued continuations.
|
|
1938
2345
|
if (msg.stopReason === "aborted") {
|
|
@@ -3164,6 +3571,7 @@ export class AgentSession {
|
|
|
3164
3571
|
this.#pendingIrcAsides = [];
|
|
3165
3572
|
this.yieldQueue.clear();
|
|
3166
3573
|
this.agent.setAsideMessageProvider(undefined);
|
|
3574
|
+
this.#stopAdvisorRuntime();
|
|
3167
3575
|
this.#evalExecutionDisposing = true;
|
|
3168
3576
|
}
|
|
3169
3577
|
|
|
@@ -4765,6 +5173,7 @@ export class AgentSession {
|
|
|
4765
5173
|
this.#todoReminderCount = 0;
|
|
4766
5174
|
this.#todoReminderAwaitingProgress = false;
|
|
4767
5175
|
this.#emptyStopRetryCount = 0;
|
|
5176
|
+
this.#unexpectedStopRetryCount = 0;
|
|
4768
5177
|
|
|
4769
5178
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
4770
5179
|
|
|
@@ -4905,7 +5314,12 @@ export class AgentSession {
|
|
|
4905
5314
|
}
|
|
4906
5315
|
|
|
4907
5316
|
const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
|
|
4908
|
-
|
|
5317
|
+
this.#pendingProviderRequestNonMessageTokens = computeNonMessageTokens(this);
|
|
5318
|
+
try {
|
|
5319
|
+
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
|
|
5320
|
+
} finally {
|
|
5321
|
+
this.#pendingProviderRequestNonMessageTokens = undefined;
|
|
5322
|
+
}
|
|
4909
5323
|
if (!options?.skipPostPromptRecoveryWait) {
|
|
4910
5324
|
await this.#waitForPostPromptRecovery(generation);
|
|
4911
5325
|
}
|
|
@@ -5562,6 +5976,7 @@ export class AgentSession {
|
|
|
5562
5976
|
this.#todoReminderAwaitingProgress = false;
|
|
5563
5977
|
this.#planReferenceSent = false;
|
|
5564
5978
|
this.#planReferencePath = "local://PLAN.md";
|
|
5979
|
+
this.#advisorRuntime?.reset();
|
|
5565
5980
|
this.#reconnectToAgent();
|
|
5566
5981
|
|
|
5567
5982
|
// Emit session_switch event with reason "new" to hooks
|
|
@@ -6179,6 +6594,7 @@ export class AgentSession {
|
|
|
6179
6594
|
await this.sessionManager.rewriteEntries();
|
|
6180
6595
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6181
6596
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6597
|
+
this.#advisorRuntime?.reset();
|
|
6182
6598
|
this.#syncTodoPhasesFromBranch();
|
|
6183
6599
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6184
6600
|
return result;
|
|
@@ -6208,9 +6624,9 @@ export class AgentSession {
|
|
|
6208
6624
|
return undefined;
|
|
6209
6625
|
}
|
|
6210
6626
|
|
|
6211
|
-
await this.sessionManager.rewriteEntries();
|
|
6212
6627
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6213
6628
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6629
|
+
this.#advisorRuntime?.reset();
|
|
6214
6630
|
this.#syncTodoPhasesFromBranch();
|
|
6215
6631
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6216
6632
|
return result;
|
|
@@ -6261,6 +6677,7 @@ export class AgentSession {
|
|
|
6261
6677
|
await this.sessionManager.rewriteEntries();
|
|
6262
6678
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6263
6679
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6680
|
+
this.#advisorRuntime?.reset();
|
|
6264
6681
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6265
6682
|
return { removed };
|
|
6266
6683
|
}
|
|
@@ -6311,6 +6728,7 @@ export class AgentSession {
|
|
|
6311
6728
|
await this.sessionManager.rewriteEntries();
|
|
6312
6729
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6313
6730
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6731
|
+
this.#advisorRuntime?.reset();
|
|
6314
6732
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6315
6733
|
|
|
6316
6734
|
return {
|
|
@@ -6527,6 +6945,7 @@ export class AgentSession {
|
|
|
6527
6945
|
const newEntries = this.sessionManager.getEntries();
|
|
6528
6946
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6529
6947
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6948
|
+
this.#advisorRuntime?.reset();
|
|
6530
6949
|
this.#syncTodoPhasesFromBranch();
|
|
6531
6950
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6532
6951
|
|
|
@@ -6746,6 +7165,7 @@ export class AgentSession {
|
|
|
6746
7165
|
// Rebuild agent messages from session
|
|
6747
7166
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6748
7167
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7168
|
+
this.#advisorRuntime?.reset();
|
|
6749
7169
|
this.#syncTodoPhasesFromBranch();
|
|
6750
7170
|
|
|
6751
7171
|
return { document: handoffText, savedPath };
|
|
@@ -6771,13 +7191,37 @@ export class AgentSession {
|
|
|
6771
7191
|
return tokens;
|
|
6772
7192
|
}
|
|
6773
7193
|
|
|
7194
|
+
#estimatePrePromptContextTokens(messages: AgentMessage[], contextWindow: number): number {
|
|
7195
|
+
const currentUsage = this.getContextUsage({ contextWindow });
|
|
7196
|
+
if (typeof currentUsage?.tokens !== "number" || !Number.isFinite(currentUsage.tokens)) {
|
|
7197
|
+
return this.#estimatePendingPromptTokens(messages);
|
|
7198
|
+
}
|
|
7199
|
+
|
|
7200
|
+
const currentEstimate = this.#estimateContextTokens();
|
|
7201
|
+
if (!currentEstimate.providerAnchored) {
|
|
7202
|
+
return this.#estimatePendingPromptTokens(messages);
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
let tokens = currentUsage.tokens;
|
|
7206
|
+
const previousNonMessageTokens = currentEstimate.providerNonMessageTokens;
|
|
7207
|
+
if (previousNonMessageTokens !== undefined) {
|
|
7208
|
+
const currentNonMessageTokens = computeNonMessageTokens(this);
|
|
7209
|
+
const nonMessageTokenGrowth = Math.max(0, currentNonMessageTokens - previousNonMessageTokens);
|
|
7210
|
+
tokens += nonMessageTokenGrowth;
|
|
7211
|
+
}
|
|
7212
|
+
for (const message of messages) {
|
|
7213
|
+
tokens += estimateTokens(message);
|
|
7214
|
+
}
|
|
7215
|
+
return tokens;
|
|
7216
|
+
}
|
|
7217
|
+
|
|
6774
7218
|
async #runPrePromptCompactionIfNeeded(messages: AgentMessage[]): Promise<void> {
|
|
6775
7219
|
const model = this.model;
|
|
6776
7220
|
if (!model) return;
|
|
6777
7221
|
const contextWindow = model.contextWindow ?? 0;
|
|
6778
7222
|
if (contextWindow <= 0) return;
|
|
6779
7223
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6780
|
-
const contextTokens = this.#
|
|
7224
|
+
const contextTokens = this.#estimatePrePromptContextTokens(messages, contextWindow);
|
|
6781
7225
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
|
|
6782
7226
|
|
|
6783
7227
|
// Auto-promote first: switching to a larger-context model avoids compacting
|
|
@@ -7027,6 +7471,71 @@ export class AgentSession {
|
|
|
7027
7471
|
maxRetries: EMPTY_STOP_MAX_RETRIES,
|
|
7028
7472
|
});
|
|
7029
7473
|
}
|
|
7474
|
+
async #handleUnexpectedAssistantStop(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
7475
|
+
if (!this.settings.get("features.unexpectedStopDetection")) {
|
|
7476
|
+
return false;
|
|
7477
|
+
}
|
|
7478
|
+
if (!isUnexpectedStopCandidate(assistantMessage)) {
|
|
7479
|
+
this.#unexpectedStopRetryCount = 0;
|
|
7480
|
+
return false;
|
|
7481
|
+
}
|
|
7482
|
+
|
|
7483
|
+
const text = assistantMessage.content
|
|
7484
|
+
.filter((content): content is TextContent => content.type === "text")
|
|
7485
|
+
.map(content => content.text)
|
|
7486
|
+
.join("\n");
|
|
7487
|
+
if (!/\S/.test(text)) {
|
|
7488
|
+
this.#unexpectedStopRetryCount = 0;
|
|
7489
|
+
return false;
|
|
7490
|
+
}
|
|
7491
|
+
|
|
7492
|
+
const controller = new AbortController();
|
|
7493
|
+
const timeout = setTimeout(() => controller.abort(), UNEXPECTED_STOP_TIMEOUT_MS);
|
|
7494
|
+
let classification: boolean | undefined;
|
|
7495
|
+
try {
|
|
7496
|
+
classification = await classifyUnexpectedStop(text, {
|
|
7497
|
+
settings: this.settings,
|
|
7498
|
+
registry: this.#modelRegistry,
|
|
7499
|
+
sessionId: this.sessionId,
|
|
7500
|
+
metadataResolver: (provider: string) => this.agent.metadataForProvider(provider),
|
|
7501
|
+
signal: controller.signal,
|
|
7502
|
+
});
|
|
7503
|
+
} finally {
|
|
7504
|
+
clearTimeout(timeout);
|
|
7505
|
+
}
|
|
7506
|
+
|
|
7507
|
+
if (classification !== true) {
|
|
7508
|
+
this.#unexpectedStopRetryCount = 0;
|
|
7509
|
+
return false;
|
|
7510
|
+
}
|
|
7511
|
+
|
|
7512
|
+
this.#unexpectedStopRetryCount++;
|
|
7513
|
+
if (this.#unexpectedStopRetryCount > UNEXPECTED_STOP_MAX_RETRIES) {
|
|
7514
|
+
logger.warn("Assistant returned unexpected stop after retry cap", {
|
|
7515
|
+
attempts: this.#unexpectedStopRetryCount - 1,
|
|
7516
|
+
model: assistantMessage.model,
|
|
7517
|
+
provider: assistantMessage.provider,
|
|
7518
|
+
});
|
|
7519
|
+
this.#unexpectedStopRetryCount = 0;
|
|
7520
|
+
return false;
|
|
7521
|
+
}
|
|
7522
|
+
|
|
7523
|
+
this.agent.appendMessage({
|
|
7524
|
+
role: "developer",
|
|
7525
|
+
content: [{ type: "text", text: this.#unexpectedStopRetryReminder() }],
|
|
7526
|
+
attribution: "agent",
|
|
7527
|
+
timestamp: Date.now(),
|
|
7528
|
+
});
|
|
7529
|
+
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
7530
|
+
return true;
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7533
|
+
#unexpectedStopRetryReminder(): string {
|
|
7534
|
+
return prompt.render(unexpectedStopRetryTemplate, {
|
|
7535
|
+
retryCount: this.#unexpectedStopRetryCount,
|
|
7536
|
+
maxRetries: UNEXPECTED_STOP_MAX_RETRIES,
|
|
7537
|
+
});
|
|
7538
|
+
}
|
|
7030
7539
|
|
|
7031
7540
|
#removeEmptyStopFromActiveContext(assistantMessage: AssistantMessage): void {
|
|
7032
7541
|
const messages = this.agent.state.messages;
|
|
@@ -7094,6 +7603,7 @@ export class AgentSession {
|
|
|
7094
7603
|
}
|
|
7095
7604
|
const safeCount = Math.max(0, Math.min(checkpointState.checkpointMessageCount, this.agent.state.messages.length));
|
|
7096
7605
|
this.agent.replaceMessages(this.agent.state.messages.slice(0, safeCount));
|
|
7606
|
+
this.#advisorRuntime?.reset();
|
|
7097
7607
|
try {
|
|
7098
7608
|
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
7099
7609
|
startedAt: checkpointState.startedAt,
|
|
@@ -7702,6 +8212,10 @@ export class AgentSession {
|
|
|
7702
8212
|
}
|
|
7703
8213
|
|
|
7704
8214
|
#getCompactionModelCandidates(availableModels: Model[]): Model[] {
|
|
8215
|
+
return this.#resolveCompactionModelCandidates(this.model, availableModels);
|
|
8216
|
+
}
|
|
8217
|
+
|
|
8218
|
+
#resolveCompactionModelCandidates(preferredModel: Model | null | undefined, availableModels: Model[]): Model[] {
|
|
7705
8219
|
const candidates: Model[] = [];
|
|
7706
8220
|
const seen = new Set<string>();
|
|
7707
8221
|
|
|
@@ -7713,15 +8227,9 @@ export class AgentSession {
|
|
|
7713
8227
|
candidates.push(model);
|
|
7714
8228
|
};
|
|
7715
8229
|
|
|
7716
|
-
|
|
7717
|
-
// Prefer the active session's model: it's what the user is actively using,
|
|
7718
|
-
// and routing compaction to a different provider (e.g. an OpenAI default
|
|
7719
|
-
// model while the chat is on Anthropic) changes provider-specific behavior
|
|
7720
|
-
// like remote compaction endpoints. Role-based candidates only kick in
|
|
7721
|
-
// as auth fallbacks when the current model has no usable credentials.
|
|
7722
|
-
addCandidate(currentModel);
|
|
8230
|
+
addCandidate(preferredModel ?? undefined);
|
|
7723
8231
|
for (const role of MODEL_ROLE_IDS) {
|
|
7724
|
-
addCandidate(this.#resolveRoleModelFull(role, availableModels,
|
|
8232
|
+
addCandidate(this.#resolveRoleModelFull(role, availableModels, preferredModel ?? undefined).model);
|
|
7725
8233
|
}
|
|
7726
8234
|
|
|
7727
8235
|
const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
|
|
@@ -8279,6 +8787,7 @@ export class AgentSession {
|
|
|
8279
8787
|
const newEntries = this.sessionManager.getEntries();
|
|
8280
8788
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8281
8789
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8790
|
+
this.#advisorRuntime?.reset();
|
|
8282
8791
|
this.#syncTodoPhasesFromBranch();
|
|
8283
8792
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8284
8793
|
|
|
@@ -9924,6 +10433,7 @@ export class AgentSession {
|
|
|
9924
10433
|
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
9925
10434
|
this.agent.serviceTier = previousServiceTier;
|
|
9926
10435
|
this.#syncTodoPhasesFromBranch();
|
|
10436
|
+
this.#advisorRuntime?.reset();
|
|
9927
10437
|
this.#reconnectToAgent();
|
|
9928
10438
|
if (restoreMcpError) {
|
|
9929
10439
|
throw restoreMcpError;
|
|
@@ -10005,6 +10515,7 @@ export class AgentSession {
|
|
|
10005
10515
|
|
|
10006
10516
|
if (!skipConversationRestore) {
|
|
10007
10517
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10518
|
+
this.#advisorRuntime?.reset();
|
|
10008
10519
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10009
10520
|
}
|
|
10010
10521
|
|
|
@@ -10171,6 +10682,7 @@ export class AgentSession {
|
|
|
10171
10682
|
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
10172
10683
|
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
10173
10684
|
this.agent.replaceMessages(displayContext.messages);
|
|
10685
|
+
this.#advisorRuntime?.reset();
|
|
10174
10686
|
this.#syncTodoPhasesFromBranch();
|
|
10175
10687
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10176
10688
|
|
|
@@ -10528,6 +11040,8 @@ export class AgentSession {
|
|
|
10528
11040
|
*/
|
|
10529
11041
|
#estimateContextTokens(): {
|
|
10530
11042
|
tokens: number;
|
|
11043
|
+
providerAnchored: boolean;
|
|
11044
|
+
providerNonMessageTokens?: number;
|
|
10531
11045
|
} {
|
|
10532
11046
|
const messages = this.messages;
|
|
10533
11047
|
|
|
@@ -10554,10 +11068,19 @@ export class AgentSession {
|
|
|
10554
11068
|
}
|
|
10555
11069
|
return {
|
|
10556
11070
|
tokens: estimated,
|
|
11071
|
+
providerAnchored: false,
|
|
10557
11072
|
};
|
|
10558
11073
|
}
|
|
10559
11074
|
|
|
10560
11075
|
const usageTokens = calculatePromptTokens(lastUsage);
|
|
11076
|
+
const providerNonMessage =
|
|
11077
|
+
this.#lastProviderUsageNonMessage &&
|
|
11078
|
+
messages[lastUsageIndex]?.role === "assistant" &&
|
|
11079
|
+
this.#lastProviderUsageNonMessage.provider === (messages[lastUsageIndex] as AssistantMessage).provider &&
|
|
11080
|
+
this.#lastProviderUsageNonMessage.model === (messages[lastUsageIndex] as AssistantMessage).model &&
|
|
11081
|
+
this.#lastProviderUsageNonMessage.timestamp === (messages[lastUsageIndex] as AssistantMessage).timestamp
|
|
11082
|
+
? this.#lastProviderUsageNonMessage.tokens
|
|
11083
|
+
: undefined;
|
|
10561
11084
|
let trailingTokens = 0;
|
|
10562
11085
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
10563
11086
|
trailingTokens += estimateTokens(messages[i]);
|
|
@@ -10565,6 +11088,8 @@ export class AgentSession {
|
|
|
10565
11088
|
|
|
10566
11089
|
return {
|
|
10567
11090
|
tokens: usageTokens + trailingTokens,
|
|
11091
|
+
providerAnchored: true,
|
|
11092
|
+
providerNonMessageTokens: providerNonMessage,
|
|
10568
11093
|
};
|
|
10569
11094
|
}
|
|
10570
11095
|
|
|
@@ -10654,7 +11179,10 @@ export class AgentSession {
|
|
|
10654
11179
|
* Format the entire session as plain text for clipboard export.
|
|
10655
11180
|
* Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
|
|
10656
11181
|
*/
|
|
10657
|
-
formatSessionAsText(): string {
|
|
11182
|
+
formatSessionAsText(options?: { compact?: boolean }): string {
|
|
11183
|
+
if (options?.compact) {
|
|
11184
|
+
return formatSessionHistoryMarkdown(this.messages);
|
|
11185
|
+
}
|
|
10658
11186
|
return formatSessionDumpText({
|
|
10659
11187
|
messages: this.messages,
|
|
10660
11188
|
systemPrompt: this.agent.state.systemPrompt,
|
|
@@ -10664,6 +11192,179 @@ export class AgentSession {
|
|
|
10664
11192
|
});
|
|
10665
11193
|
}
|
|
10666
11194
|
|
|
11195
|
+
/**
|
|
11196
|
+
* Enable or disable the advisor for this session. The setting is persisted,
|
|
11197
|
+
* and the runtime is started or stopped to match.
|
|
11198
|
+
*
|
|
11199
|
+
* @returns true when the advisor is actively running after the call.
|
|
11200
|
+
*/
|
|
11201
|
+
setAdvisorEnabled(enabled: boolean): boolean {
|
|
11202
|
+
if (enabled) {
|
|
11203
|
+
this.settings.clearOverride("advisor.enabled");
|
|
11204
|
+
this.settings.set("advisor.enabled", true);
|
|
11205
|
+
return this.#buildAdvisorRuntime(true);
|
|
11206
|
+
}
|
|
11207
|
+
this.settings.set("advisor.enabled", false);
|
|
11208
|
+
this.#stopAdvisorRuntime();
|
|
11209
|
+
return false;
|
|
11210
|
+
}
|
|
11211
|
+
|
|
11212
|
+
/**
|
|
11213
|
+
* Toggle the advisor setting and start/stop the runtime accordingly.
|
|
11214
|
+
*
|
|
11215
|
+
* @returns true when the advisor is actively running after the call.
|
|
11216
|
+
*/
|
|
11217
|
+
toggleAdvisorEnabled(): boolean {
|
|
11218
|
+
return this.setAdvisorEnabled(!this.settings.get("advisor.enabled"));
|
|
11219
|
+
}
|
|
11220
|
+
|
|
11221
|
+
/**
|
|
11222
|
+
* Whether a live advisor agent is attached to this session. True only when
|
|
11223
|
+
* `advisor.enabled` is set AND a model resolved for the `advisor` role AND
|
|
11224
|
+
* the advisor applies to this agent kind — i.e. the actual runtime exists,
|
|
11225
|
+
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
|
11226
|
+
*/
|
|
11227
|
+
isAdvisorActive(): boolean {
|
|
11228
|
+
return this.#advisorAgent !== undefined;
|
|
11229
|
+
}
|
|
11230
|
+
|
|
11231
|
+
/**
|
|
11232
|
+
* Return structured advisor stats for the status command and TUI panel.
|
|
11233
|
+
*/
|
|
11234
|
+
getAdvisorStats(): AdvisorStats {
|
|
11235
|
+
const configured = this.settings.get("advisor.enabled") as boolean;
|
|
11236
|
+
const advisor = this.#advisorAgent;
|
|
11237
|
+
if (!advisor) {
|
|
11238
|
+
return {
|
|
11239
|
+
configured,
|
|
11240
|
+
active: false,
|
|
11241
|
+
contextWindow: 0,
|
|
11242
|
+
contextTokens: 0,
|
|
11243
|
+
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
11244
|
+
cost: 0,
|
|
11245
|
+
messages: { user: 0, assistant: 0, total: 0 },
|
|
11246
|
+
};
|
|
11247
|
+
}
|
|
11248
|
+
const model = advisor.state.model;
|
|
11249
|
+
const messages = advisor.state.messages;
|
|
11250
|
+
const contextTokens = this.#estimateAdvisorContextTokens(messages);
|
|
11251
|
+
let input = 0;
|
|
11252
|
+
let output = 0;
|
|
11253
|
+
let cacheRead = 0;
|
|
11254
|
+
let cacheWrite = 0;
|
|
11255
|
+
let cost = 0;
|
|
11256
|
+
let user = 0;
|
|
11257
|
+
let assistant = 0;
|
|
11258
|
+
for (const message of messages) {
|
|
11259
|
+
if (message.role === "user") user++;
|
|
11260
|
+
if (message.role === "assistant") {
|
|
11261
|
+
assistant++;
|
|
11262
|
+
const assistantMsg = message as AssistantMessage;
|
|
11263
|
+
input += assistantMsg.usage.input;
|
|
11264
|
+
output += assistantMsg.usage.output;
|
|
11265
|
+
cacheRead += assistantMsg.usage.cacheRead;
|
|
11266
|
+
cacheWrite += assistantMsg.usage.cacheWrite;
|
|
11267
|
+
cost += assistantMsg.usage.cost.total;
|
|
11268
|
+
}
|
|
11269
|
+
}
|
|
11270
|
+
return {
|
|
11271
|
+
configured,
|
|
11272
|
+
active: true,
|
|
11273
|
+
model,
|
|
11274
|
+
contextWindow: model.contextWindow ?? 0,
|
|
11275
|
+
contextTokens,
|
|
11276
|
+
tokens: {
|
|
11277
|
+
input,
|
|
11278
|
+
output,
|
|
11279
|
+
cacheRead,
|
|
11280
|
+
cacheWrite,
|
|
11281
|
+
total: input + output + cacheRead + cacheWrite,
|
|
11282
|
+
},
|
|
11283
|
+
cost,
|
|
11284
|
+
messages: { user, assistant, total: messages.length },
|
|
11285
|
+
};
|
|
11286
|
+
}
|
|
11287
|
+
|
|
11288
|
+
/**
|
|
11289
|
+
* Format a concise advisor status line for ACP/text output.
|
|
11290
|
+
*/
|
|
11291
|
+
formatAdvisorStatus(): string {
|
|
11292
|
+
const stats = this.getAdvisorStats();
|
|
11293
|
+
if (!stats.active) {
|
|
11294
|
+
return stats.configured
|
|
11295
|
+
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
11296
|
+
: "Advisor is disabled.";
|
|
11297
|
+
}
|
|
11298
|
+
const model = stats.model!;
|
|
11299
|
+
const contextLine =
|
|
11300
|
+
stats.contextWindow > 0
|
|
11301
|
+
? `Context: ${stats.contextTokens.toLocaleString()} / ${stats.contextWindow.toLocaleString()} tokens (${Math.round((stats.contextTokens / stats.contextWindow) * 100)}%)`
|
|
11302
|
+
: `Context: ${stats.contextTokens.toLocaleString()} tokens`;
|
|
11303
|
+
const spendParts = [
|
|
11304
|
+
`${stats.tokens.input.toLocaleString()} input`,
|
|
11305
|
+
`${stats.tokens.output.toLocaleString()} output`,
|
|
11306
|
+
];
|
|
11307
|
+
if (stats.tokens.cacheRead > 0) spendParts.push(`${stats.tokens.cacheRead.toLocaleString()} cache read`);
|
|
11308
|
+
if (stats.tokens.cacheWrite > 0) spendParts.push(`${stats.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
11309
|
+
const spendLine = `Spend: ${spendParts.join(", ")}, $${stats.cost.toFixed(4)}`;
|
|
11310
|
+
return `Advisor is enabled (${model.provider}/${model.id}). ${contextLine}. ${spendLine}.`;
|
|
11311
|
+
}
|
|
11312
|
+
|
|
11313
|
+
/**
|
|
11314
|
+
* Estimate the advisor's current context tokens. When the advisor has a
|
|
11315
|
+
* recent non-aborted assistant message with usage, use that prompt's token
|
|
11316
|
+
* count and add a trailing estimate for messages after it. Otherwise estimate
|
|
11317
|
+
* every message.
|
|
11318
|
+
*/
|
|
11319
|
+
#estimateAdvisorContextTokens(messages: AgentMessage[]): number {
|
|
11320
|
+
let lastUsageIndex: number | null = null;
|
|
11321
|
+
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
11322
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11323
|
+
const msg = messages[i];
|
|
11324
|
+
if (msg.role === "assistant") {
|
|
11325
|
+
const assistantMsg = msg as AssistantMessage;
|
|
11326
|
+
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
|
11327
|
+
lastUsage = assistantMsg.usage;
|
|
11328
|
+
lastUsageIndex = i;
|
|
11329
|
+
break;
|
|
11330
|
+
}
|
|
11331
|
+
}
|
|
11332
|
+
}
|
|
11333
|
+
if (!lastUsage || lastUsageIndex === null) {
|
|
11334
|
+
let estimated = 0;
|
|
11335
|
+
for (const message of messages) {
|
|
11336
|
+
estimated += estimateTokens(message);
|
|
11337
|
+
}
|
|
11338
|
+
return estimated;
|
|
11339
|
+
}
|
|
11340
|
+
let trailingTokens = 0;
|
|
11341
|
+
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
11342
|
+
trailingTokens += estimateTokens(messages[i]);
|
|
11343
|
+
}
|
|
11344
|
+
return calculatePromptTokens(lastUsage) + trailingTokens;
|
|
11345
|
+
}
|
|
11346
|
+
|
|
11347
|
+
/**
|
|
11348
|
+
* Format the advisor agent's own transcript (its system prompt, config,
|
|
11349
|
+
* tools, and the markdown deltas it received plus its thinking/advise/read
|
|
11350
|
+
* calls) as plain text — the advisor-side equivalent of
|
|
11351
|
+
* {@link formatSessionAsText}. Returns null when no advisor is active.
|
|
11352
|
+
*/
|
|
11353
|
+
formatAdvisorHistoryAsText(options?: { compact?: boolean }): string | null {
|
|
11354
|
+
const advisor = this.#advisorAgent;
|
|
11355
|
+
if (!advisor) return null;
|
|
11356
|
+
if (options?.compact) {
|
|
11357
|
+
return formatSessionHistoryMarkdown(advisor.state.messages);
|
|
11358
|
+
}
|
|
11359
|
+
return formatSessionDumpText({
|
|
11360
|
+
messages: advisor.state.messages,
|
|
11361
|
+
systemPrompt: advisor.state.systemPrompt,
|
|
11362
|
+
model: advisor.state.model,
|
|
11363
|
+
thinkingLevel: advisor.state.thinkingLevel,
|
|
11364
|
+
tools: advisor.state.tools,
|
|
11365
|
+
});
|
|
11366
|
+
}
|
|
11367
|
+
|
|
10667
11368
|
// =========================================================================
|
|
10668
11369
|
// Extension System
|
|
10669
11370
|
// =========================================================================
|