@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/cli.js +3624 -3568
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/settings-schema.d.ts +81 -0
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/session/agent-session.d.ts +58 -10
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +26 -0
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +242 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +12 -2
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/settings-schema.ts +77 -0
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/edit/hashline/diff.ts +57 -4
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/manager.ts +2 -1
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +144 -5
- package/src/modes/components/status-line/segments.ts +46 -21
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +106 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -5
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +28 -24
- package/src/session/agent-session.ts +867 -428
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +172 -9
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +95 -1
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/text.ts +23 -0
- package/src/tools/ask.ts +55 -4
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
|
@@ -130,18 +130,22 @@ import {
|
|
|
130
130
|
} from "@oh-my-pi/pi-utils";
|
|
131
131
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
132
132
|
import {
|
|
133
|
+
ADVISOR_DEFAULT_TOOL_NAMES,
|
|
133
134
|
AdviseTool,
|
|
134
135
|
type AdvisorAgent,
|
|
136
|
+
type AdvisorConfig,
|
|
135
137
|
AdvisorEmissionGuard,
|
|
136
138
|
type AdvisorMessageDetails,
|
|
137
139
|
type AdvisorNote,
|
|
138
140
|
AdvisorRuntime,
|
|
139
141
|
type AdvisorSeverity,
|
|
140
142
|
AdvisorTranscriptRecorder,
|
|
143
|
+
advisorTranscriptFilename,
|
|
141
144
|
formatAdvisorBatchContent,
|
|
142
145
|
isAdvisorInterruptImmuneTurnActive,
|
|
143
146
|
isInterruptingSeverity,
|
|
144
147
|
resolveAdvisorDeliveryChannel,
|
|
148
|
+
slugifyAdvisorName,
|
|
145
149
|
} from "../advisor";
|
|
146
150
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
147
151
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
@@ -232,6 +236,7 @@ import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text
|
|
|
232
236
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
233
237
|
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
234
238
|
import geminiToolReminderTemplate from "../prompts/system/gemini-tool-call-reminder.md" with { type: "text" };
|
|
239
|
+
import interruptedThinkingTemplate from "../prompts/system/interrupted-thinking.md" with { type: "text" };
|
|
235
240
|
import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
|
|
236
241
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
237
242
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
@@ -261,6 +266,7 @@ import {
|
|
|
261
266
|
shouldDisableReasoning,
|
|
262
267
|
toReasoningEffort,
|
|
263
268
|
} from "../thinking";
|
|
269
|
+
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/text";
|
|
264
270
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
265
271
|
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
266
272
|
import {
|
|
@@ -288,6 +294,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
288
294
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
289
295
|
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
290
296
|
import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallback";
|
|
297
|
+
import { generateSessionTitle } from "../utils/title-generator";
|
|
291
298
|
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
|
292
299
|
import type { AuthStorage } from "./auth-storage";
|
|
293
300
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
@@ -303,6 +310,10 @@ import {
|
|
|
303
310
|
type BashExecutionMessage,
|
|
304
311
|
type CustomMessage,
|
|
305
312
|
convertToLlm,
|
|
313
|
+
demoteInterruptedThinking,
|
|
314
|
+
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
315
|
+
type InterruptedThinkingDetails,
|
|
316
|
+
isUserInterruptAbort,
|
|
306
317
|
type PythonExecutionMessage,
|
|
307
318
|
readQueueChipText,
|
|
308
319
|
SILENT_ABORT_MARKER,
|
|
@@ -599,20 +610,29 @@ export interface AgentSessionConfig {
|
|
|
599
610
|
*/
|
|
600
611
|
providerSessionId?: string;
|
|
601
612
|
/**
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* advisor's
|
|
605
|
-
*
|
|
613
|
+
* Full advisor toolset, pre-built in `createAgentSession` against a distinct,
|
|
614
|
+
* advisor-scoped `ToolSession` (its own `-advisor` session/agent id) so the
|
|
615
|
+
* advisor's tool state stays isolated from the primary. The advisor is a full
|
|
616
|
+
* agent; its config `tools` selects a subset (default read/grep/glob). Undefined
|
|
617
|
+
* when the advisor is disabled.
|
|
606
618
|
*/
|
|
607
|
-
|
|
619
|
+
advisorTools?: AgentTool[];
|
|
608
620
|
/** Preloaded watchdog prompt content for the advisor. */
|
|
609
621
|
advisorWatchdogPrompt?: string;
|
|
622
|
+
/** Preloaded YAML top-level `instructions` shared baseline, kept separate from
|
|
623
|
+
* `advisorWatchdogPrompt` so `/advisor configure` can swap it live. */
|
|
624
|
+
advisorSharedInstructions?: string;
|
|
610
625
|
/**
|
|
611
626
|
* Preloaded project context files (AGENTS.md, etc.) rendered as a system-prompt
|
|
612
627
|
* block for the advisor — the same standing instructions the primary agent
|
|
613
628
|
* receives, so the reviewer holds the agent to them.
|
|
614
629
|
*/
|
|
615
630
|
advisorContextPrompt?: string;
|
|
631
|
+
/**
|
|
632
|
+
* Advisors discovered from `WATCHDOG.yml`. Empty/undefined runs a single
|
|
633
|
+
* legacy advisor on the `advisor` role (byte-for-byte the pre-config path).
|
|
634
|
+
*/
|
|
635
|
+
advisorConfigs?: AdvisorConfig[];
|
|
616
636
|
/**
|
|
617
637
|
* Strip tool descriptions from provider-bound tool specs on side requests
|
|
618
638
|
* (handoff). Must match the session-start value used to build the system
|
|
@@ -717,6 +737,7 @@ export interface SessionStats {
|
|
|
717
737
|
tokens: {
|
|
718
738
|
input: number;
|
|
719
739
|
output: number;
|
|
740
|
+
reasoning: number;
|
|
720
741
|
cacheRead: number;
|
|
721
742
|
cacheWrite: number;
|
|
722
743
|
total: number;
|
|
@@ -735,6 +756,7 @@ export interface AdvisorStats {
|
|
|
735
756
|
tokens: {
|
|
736
757
|
input: number;
|
|
737
758
|
output: number;
|
|
759
|
+
reasoning: number;
|
|
738
760
|
cacheRead: number;
|
|
739
761
|
cacheWrite: number;
|
|
740
762
|
total: number;
|
|
@@ -745,6 +767,43 @@ export interface AdvisorStats {
|
|
|
745
767
|
assistant: number;
|
|
746
768
|
total: number;
|
|
747
769
|
};
|
|
770
|
+
/** Per-advisor breakdown; one entry per active advisor (single-advisor sessions have one). */
|
|
771
|
+
advisors: PerAdvisorStat[];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** One advisor's slice of {@link AdvisorStats}, surfaced for the multi-advisor status panel. */
|
|
775
|
+
export interface PerAdvisorStat {
|
|
776
|
+
name: string;
|
|
777
|
+
model: Model;
|
|
778
|
+
contextWindow: number;
|
|
779
|
+
contextTokens: number;
|
|
780
|
+
tokens: AdvisorStats["tokens"];
|
|
781
|
+
cost: number;
|
|
782
|
+
messages: AdvisorStats["messages"];
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* One live advisor instance: its own agent/runtime/tools/recorder plus a
|
|
787
|
+
* per-advisor emission guard and identity. The session holds an array of these;
|
|
788
|
+
* primary-scoped state (turn counters, interrupt latches, the shared yield
|
|
789
|
+
* channel) stays on the session.
|
|
790
|
+
*/
|
|
791
|
+
interface ActiveAdvisor {
|
|
792
|
+
/** Display name from config ("default" for the legacy no-YAML advisor). */
|
|
793
|
+
name: string;
|
|
794
|
+
/** Slug for the transcript filename/session id; "" → `__advisor.jsonl`. */
|
|
795
|
+
slug: string;
|
|
796
|
+
agent: Agent;
|
|
797
|
+
runtime: AdvisorRuntime;
|
|
798
|
+
adviseTool: AdviseTool;
|
|
799
|
+
emissionGuard: AdvisorEmissionGuard;
|
|
800
|
+
recorder: AdvisorTranscriptRecorder;
|
|
801
|
+
/** Latest recorder close, awaited by dispose() so the final turn lands on disk. */
|
|
802
|
+
recorderClosed: Promise<void>;
|
|
803
|
+
/** Unsubscribe for the advisor agent's event stream feeding the recorder. */
|
|
804
|
+
agentUnsubscribe?: () => void;
|
|
805
|
+
model: Model;
|
|
806
|
+
thinkingLevel: ThinkingLevel;
|
|
748
807
|
}
|
|
749
808
|
|
|
750
809
|
export interface FreshSessionResult {
|
|
@@ -1189,6 +1248,60 @@ type MessageEndPersistenceSlot = {
|
|
|
1189
1248
|
release: () => void;
|
|
1190
1249
|
};
|
|
1191
1250
|
|
|
1251
|
+
const REPLAN_TITLE_CONTEXT_TURN_LIMIT = 6;
|
|
1252
|
+
|
|
1253
|
+
type SessionTitleSource = "auto" | "user";
|
|
1254
|
+
type SessionNameTrigger = "replan";
|
|
1255
|
+
type SetSessionNameWithTrigger = (
|
|
1256
|
+
name: string,
|
|
1257
|
+
source?: SessionTitleSource,
|
|
1258
|
+
trigger?: SessionNameTrigger,
|
|
1259
|
+
) => Promise<boolean>;
|
|
1260
|
+
|
|
1261
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1262
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function textFromContent(content: unknown): string {
|
|
1266
|
+
if (typeof content === "string") return content.trim();
|
|
1267
|
+
if (!Array.isArray(content)) return "";
|
|
1268
|
+
const parts: string[] = [];
|
|
1269
|
+
for (const block of content) {
|
|
1270
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") continue;
|
|
1271
|
+
const text = block.text.trim();
|
|
1272
|
+
if (text) parts.push(text);
|
|
1273
|
+
}
|
|
1274
|
+
return parts.join("\n\n");
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function thinkingFromContent(content: unknown): string {
|
|
1278
|
+
if (!Array.isArray(content)) return "";
|
|
1279
|
+
const parts: string[] = [];
|
|
1280
|
+
for (const block of content) {
|
|
1281
|
+
if (!isRecord(block) || block.type !== "thinking" || typeof block.thinking !== "string") continue;
|
|
1282
|
+
const thinking = block.thinking.trim();
|
|
1283
|
+
if (thinking) parts.push(thinking);
|
|
1284
|
+
}
|
|
1285
|
+
return parts.join("\n\n");
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function toolCallOpFromMessage(message: AgentMessage, toolCallId: string): string | undefined {
|
|
1289
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return undefined;
|
|
1290
|
+
for (const block of message.content) {
|
|
1291
|
+
if (!isRecord(block) || block.type !== "toolCall" || block.id !== toolCallId) continue;
|
|
1292
|
+
return isRecord(block.arguments) ? getStringProperty(block.arguments, "op") : undefined;
|
|
1293
|
+
}
|
|
1294
|
+
return undefined;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function titleConversationTurnFromMessage(message: AgentMessage): TitleConversationTurn | undefined {
|
|
1298
|
+
if (message.role !== "user" && message.role !== "assistant") return undefined;
|
|
1299
|
+
const text = textFromContent(message.content);
|
|
1300
|
+
const thinking = message.role === "assistant" ? thinkingFromContent(message.content) : undefined;
|
|
1301
|
+
if (!text && !thinking) return undefined;
|
|
1302
|
+
return { role: message.role, ...(text ? { text } : {}), ...(thinking ? { thinking } : {}) };
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1192
1305
|
export class AgentSession {
|
|
1193
1306
|
readonly agent: Agent;
|
|
1194
1307
|
readonly sessionManager: SessionManager;
|
|
@@ -1229,29 +1342,21 @@ export class AgentSession {
|
|
|
1229
1342
|
#advisorAutoResumeSuppressed = false;
|
|
1230
1343
|
#advisorPrimaryTurnsCompleted = 0;
|
|
1231
1344
|
#advisorInterruptImmuneTurnStart: number | undefined;
|
|
1232
|
-
/** Dedupe + per-update rate-limit + content-free-phrase filter applied to
|
|
1233
|
-
* every accepted advisor `advise()` call. Owned by the session because the
|
|
1234
|
-
* session is what routes accepted notes back to the primary transcript.
|
|
1235
|
-
* Reset on advisor reset (compaction, session switch, `/new`). */
|
|
1236
|
-
readonly #advisorEmissionGuard = new AdvisorEmissionGuard();
|
|
1237
1345
|
#planModeState: PlanModeState | undefined;
|
|
1238
1346
|
#goalModeState: GoalModeState | undefined;
|
|
1239
1347
|
#goalRuntime: GoalRuntime;
|
|
1240
|
-
#advisorRuntime?: AdvisorRuntime;
|
|
1241
1348
|
#advisorEnabled = false;
|
|
1242
|
-
|
|
1243
|
-
#advisorAgent?: Agent;
|
|
1244
|
-
#advisorAdviseTool?: AdviseTool;
|
|
1245
|
-
#advisorReadOnlyTools?: AgentTool[];
|
|
1349
|
+
#advisorTools?: AgentTool[];
|
|
1246
1350
|
#advisorWatchdogPrompt?: string;
|
|
1351
|
+
#advisorSharedInstructions?: string;
|
|
1247
1352
|
#advisorContextPrompt?: string;
|
|
1248
1353
|
#advisorYieldQueueUnsubscribe?: () => void;
|
|
1249
|
-
/**
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1354
|
+
/** Live advisors. Empty when no advisor is active. */
|
|
1355
|
+
#advisors: ActiveAdvisor[] = [];
|
|
1356
|
+
/** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
|
|
1357
|
+
#advisorConfigs?: AdvisorConfig[];
|
|
1358
|
+
/** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
|
|
1359
|
+
* used as the open barrier for the next build so two writers never share a file. */
|
|
1255
1360
|
#advisorRecorderClosed: Promise<void> = Promise.resolve();
|
|
1256
1361
|
#goalTurnCounter = 0;
|
|
1257
1362
|
#planReferenceSent = false;
|
|
@@ -1290,6 +1395,7 @@ export class AgentSession {
|
|
|
1290
1395
|
*/
|
|
1291
1396
|
#todoReminderAwaitingProgress = false;
|
|
1292
1397
|
#todoPhases: TodoPhase[] = [];
|
|
1398
|
+
#replanTitleRefreshInFlight: Promise<void> | undefined = undefined;
|
|
1293
1399
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
1294
1400
|
|
|
1295
1401
|
// Bash execution state
|
|
@@ -1669,9 +1775,11 @@ export class AgentSession {
|
|
|
1669
1775
|
// toggle scopes priority to Fireworks alone, without mutating the shared
|
|
1670
1776
|
// session `serviceTier` that drives `/fast` and OpenAI/Anthropic priority.
|
|
1671
1777
|
this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
|
|
1672
|
-
this.#
|
|
1778
|
+
this.#advisorTools = config.advisorTools;
|
|
1673
1779
|
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1780
|
+
this.#advisorSharedInstructions = config.advisorSharedInstructions;
|
|
1674
1781
|
this.#advisorContextPrompt = config.advisorContextPrompt;
|
|
1782
|
+
this.#advisorConfigs = config.advisorConfigs;
|
|
1675
1783
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
1676
1784
|
this.#validateRetryFallbackChains();
|
|
1677
1785
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
@@ -1716,12 +1824,15 @@ export class AgentSession {
|
|
|
1716
1824
|
await this.#applyRewind(rewindReport, messages);
|
|
1717
1825
|
}
|
|
1718
1826
|
this.#advisorPrimaryTurnsCompleted++;
|
|
1719
|
-
if (this.#
|
|
1720
|
-
this.#
|
|
1827
|
+
if (this.#advisors.length > 0) {
|
|
1828
|
+
for (const a of this.#advisors) {
|
|
1829
|
+
if (!a.runtime.disposed) a.runtime.onTurnEnd(messages);
|
|
1830
|
+
}
|
|
1721
1831
|
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
1722
1832
|
if (syncBacklog !== "off") {
|
|
1723
1833
|
const threshold = parseInt(syncBacklog, 10);
|
|
1724
|
-
|
|
1834
|
+
// Parallel so the 30s catch-up budget is shared across advisors, not summed.
|
|
1835
|
+
await Promise.all(this.#advisors.map(a => a.runtime.waitForCatchup(30000, threshold, signal)));
|
|
1725
1836
|
}
|
|
1726
1837
|
}
|
|
1727
1838
|
await this.#maintainContextMidRun(messages, signal, context);
|
|
@@ -1891,12 +2002,14 @@ export class AgentSession {
|
|
|
1891
2002
|
// Mute the recorder across the re-prime: AdvisorRuntime.reset() aborts the advisor
|
|
1892
2003
|
// loop, and that abort can emit an `aborted` message_end we must not attribute to
|
|
1893
2004
|
// either session's transcript. Detach, reset, then re-attach the live agent's feed.
|
|
1894
|
-
this.#
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2005
|
+
for (const a of this.#advisors) {
|
|
2006
|
+
a.agentUnsubscribe?.();
|
|
2007
|
+
a.agentUnsubscribe = undefined;
|
|
2008
|
+
a.runtime.reset();
|
|
2009
|
+
a.adviseTool.resetDeliveredNotes();
|
|
2010
|
+
a.emissionGuard.reset();
|
|
2011
|
+
this.#attachAdvisorRecorderFeed(a);
|
|
2012
|
+
}
|
|
1900
2013
|
this.#advisorPrimaryTurnsCompleted = 0;
|
|
1901
2014
|
this.#advisorInterruptImmuneTurnStart = undefined;
|
|
1902
2015
|
this.#advisorAutoResumeSuppressed = false;
|
|
@@ -1909,261 +2022,298 @@ export class AgentSession {
|
|
|
1909
2022
|
|
|
1910
2023
|
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1911
2024
|
if (this.#isDisposed) return false;
|
|
1912
|
-
if (this.#
|
|
2025
|
+
if (this.#advisors.length > 0) return true;
|
|
1913
2026
|
if (!this.#advisorEnabled) return false;
|
|
1914
2027
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1915
2028
|
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
);
|
|
1921
|
-
if (!advisorSel) {
|
|
1922
|
-
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive");
|
|
1923
|
-
return false;
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
// Concern and blocker interrupt the running agent through the steering
|
|
1927
|
-
// channel (aborting in-flight tools at the next steering boundary); when the
|
|
1928
|
-
// loop has already yielded, triggerTurn resumes it so the advice is acted on
|
|
1929
|
-
// immediately rather than waiting for the next user prompt. After a deliberate
|
|
1930
|
-
// user interrupt the auto-resume is suppressed — but only while the agent is
|
|
1931
|
-
// idle or still tearing the interrupted turn down: a concern is then recorded
|
|
1932
|
-
// as a visible card and re-enters context when the user resumes. Once a turn
|
|
1933
|
-
// is streaming again (a resume the user already drove) it is steered in live,
|
|
1934
|
-
// since steering an active run auto-resumes nothing; parking it there would
|
|
1935
|
-
// strand the advice and dump the backlog as one burst at the next prompt. A
|
|
1936
|
-
// plain nit always rides the non-interrupting YieldQueue aside.
|
|
1937
|
-
// Apply the per-session emission policy (one-advise-per-update gate,
|
|
1938
|
-
// exact-text dedupe, content-free phrase filter) before any routing.
|
|
1939
|
-
// Suppression here means the advisor model called `advise()` but the call
|
|
1940
|
-
// is dropped silently — the model still sees `Recorded.` from the tool, so
|
|
1941
|
-
// telling it "suppressed" doesn't tempt it into rephrasing the same useless
|
|
1942
|
-
// note to bypass the dedupe.
|
|
1943
|
-
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1944
|
-
if (!this.#advisorEmissionGuard.accept(note)) {
|
|
1945
|
-
logger.debug("advisor advice suppressed by emission guard", { severity });
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
const interrupting = isInterruptingSeverity(severity);
|
|
1949
|
-
const channel = resolveAdvisorDeliveryChannel({
|
|
1950
|
-
severity,
|
|
1951
|
-
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
1952
|
-
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
1953
|
-
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
1954
|
-
// loop will consume a steer at its next boundary; steering into the unwind
|
|
1955
|
-
// window would strand the card and let #drainStrandedQueuedMessages
|
|
1956
|
-
// auto-resume it despite the user's interrupt.
|
|
1957
|
-
streaming: this.agent.state.isStreaming,
|
|
1958
|
-
aborting: this.#abortInProgress,
|
|
1959
|
-
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
1960
|
-
});
|
|
1961
|
-
if (channel === "aside") {
|
|
1962
|
-
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1966
|
-
const content = formatAdvisorBatchContent(notes);
|
|
1967
|
-
const details = { notes } satisfies AdvisorMessageDetails;
|
|
1968
|
-
if (channel === "preserve") {
|
|
1969
|
-
this.#preserveAdvisorCard({
|
|
1970
|
-
role: "custom",
|
|
1971
|
-
customType: "advisor",
|
|
1972
|
-
content,
|
|
1973
|
-
display: true,
|
|
1974
|
-
attribution: "agent",
|
|
1975
|
-
details,
|
|
1976
|
-
timestamp: Date.now(),
|
|
1977
|
-
});
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
this.#recordAdvisorInterruptDelivered();
|
|
1981
|
-
void this.sendCustomMessage(
|
|
1982
|
-
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1983
|
-
{ deliverAs: "steer", triggerTurn: true },
|
|
1984
|
-
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1985
|
-
};
|
|
1986
|
-
|
|
1987
|
-
const adviseTool = new AdviseTool(enqueueAdvice);
|
|
1988
|
-
this.#advisorAdviseTool = adviseTool;
|
|
1989
|
-
const advisorReadOnlyTools = this.#advisorReadOnlyTools ?? [];
|
|
1990
|
-
|
|
1991
|
-
const appendOnlyContext = new AppendOnlyContextManager();
|
|
1992
|
-
const advisorThinkingLevel = advisorSel.thinkingLevel ?? ThinkingLevel.Medium;
|
|
1993
|
-
const systemPrompt = [advisorSystemPrompt];
|
|
1994
|
-
if (this.#advisorContextPrompt) {
|
|
1995
|
-
systemPrompt.push(this.#advisorContextPrompt);
|
|
1996
|
-
}
|
|
1997
|
-
if (this.#advisorWatchdogPrompt) {
|
|
1998
|
-
systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
1999
|
-
}
|
|
2000
|
-
const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
|
|
2029
|
+
// The lone implicit "default" advisor (slug "") reproduces the legacy
|
|
2030
|
+
// single-advisor path: the `advisor` role model + `__advisor.jsonl`.
|
|
2031
|
+
const legacy = !this.#advisorConfigs?.length;
|
|
2032
|
+
const roster: AdvisorConfig[] = legacy ? [{ name: "default" }] : this.#advisorConfigs!;
|
|
2001
2033
|
|
|
2002
2034
|
// Advisor service tier (`serviceTierAdvisor`): "none" (default) runs the
|
|
2003
2035
|
// advisor on standard processing; "inherit" tracks the session's live tier
|
|
2004
2036
|
// per request (like the main agent, including /fast toggles) via a resolver;
|
|
2005
|
-
// a concrete value pins the advisor to that tier
|
|
2037
|
+
// a concrete value pins the advisor to that tier. One value for all advisors.
|
|
2006
2038
|
const advisorTierSetting = this.settings.get("serviceTierAdvisor");
|
|
2007
2039
|
const advisorServiceTier =
|
|
2008
2040
|
advisorTierSetting === "inherit" ? undefined : resolveServiceTierSetting(advisorTierSetting, undefined);
|
|
2009
2041
|
const advisorServiceTierResolver =
|
|
2010
2042
|
advisorTierSetting === "inherit" ? (model: Model) => this.#effectiveServiceTier(model) : undefined;
|
|
2011
2043
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2044
|
+
const usedSlugs = new Set<string>();
|
|
2045
|
+
for (const config of roster) {
|
|
2046
|
+
let slug = legacy ? "" : slugifyAdvisorName(config.name);
|
|
2047
|
+
if (slug) {
|
|
2048
|
+
let candidate = slug;
|
|
2049
|
+
let n = 2;
|
|
2050
|
+
while (usedSlugs.has(candidate)) candidate = `${slug}-${n++}`;
|
|
2051
|
+
slug = candidate;
|
|
2052
|
+
usedSlugs.add(slug);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Resolve the advisor's model: an explicit `model` override wins; else the
|
|
2056
|
+
// `advisor` role chain. A model that fails to resolve skips just this advisor.
|
|
2057
|
+
let model: Model | undefined;
|
|
2058
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
2059
|
+
if (config.model) {
|
|
2060
|
+
const resolved = resolveModelOverride([config.model], this.#modelRegistry, this.settings);
|
|
2061
|
+
model = resolved.model;
|
|
2062
|
+
thinkingLevel = resolved.thinkingLevel;
|
|
2063
|
+
if (!model) {
|
|
2064
|
+
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2065
|
+
continue;
|
|
2027
2066
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
// - `promptCacheKey` pins OpenAI Responses (and any provider that reads
|
|
2040
|
-
// `prompt_cache_key`) to the advisor session id so consecutive advisor
|
|
2041
|
-
// turns land on the same cache shard.
|
|
2042
|
-
// - `transformProviderContext` applies snapcompact, secret obfuscation,
|
|
2043
|
-
// and image clamping to advisor requests like the main turn.
|
|
2044
|
-
//
|
|
2045
|
-
// Without this parity, OpenRouter advisor calls bypassed the variant
|
|
2046
|
-
// suffix and prompt-cache key and produced inconsistent cache hits
|
|
2047
|
-
// (see can1357/oh-my-pi#3639).
|
|
2048
|
-
const advisorAgent = new Agent({
|
|
2049
|
-
initialState: {
|
|
2050
|
-
systemPrompt,
|
|
2051
|
-
model: advisorSel.model,
|
|
2052
|
-
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
2053
|
-
tools: [adviseTool, ...advisorReadOnlyTools],
|
|
2054
|
-
},
|
|
2055
|
-
appendOnlyContext,
|
|
2056
|
-
sessionId: advisorSessionId,
|
|
2057
|
-
promptCacheKey: advisorSessionId,
|
|
2058
|
-
providerSessionState: this.#providerSessionState,
|
|
2059
|
-
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
2060
|
-
streamFn: this.#advisorStreamFn,
|
|
2061
|
-
onPayload: this.#onPayload,
|
|
2062
|
-
onResponse: this.#onResponse,
|
|
2063
|
-
onSseEvent: this.#onSseEvent,
|
|
2064
|
-
transformProviderContext: this.#transformProviderContext,
|
|
2065
|
-
intentTracing: false,
|
|
2066
|
-
telemetry: advisorTelemetry,
|
|
2067
|
-
serviceTier: advisorServiceTier,
|
|
2068
|
-
serviceTierResolver: advisorServiceTierResolver,
|
|
2069
|
-
});
|
|
2070
|
-
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2071
|
-
|
|
2072
|
-
const advisorAgentFacade: AdvisorAgent = {
|
|
2073
|
-
prompt: input => advisorAgent.prompt(input),
|
|
2074
|
-
abort: reason => advisorAgent.abort(reason),
|
|
2075
|
-
reset: () => {
|
|
2076
|
-
advisorAgent.reset();
|
|
2077
|
-
appendOnlyContext.log.clear();
|
|
2078
|
-
},
|
|
2079
|
-
rollbackTo: count => {
|
|
2080
|
-
// Drop the failed user batch + synthetic assistant-error turn
|
|
2081
|
-
// `Agent.#runLoop` appended for a turn ending in `stopReason: "error"`.
|
|
2082
|
-
// The append-only context auto-resyncs on the next prompt's
|
|
2083
|
-
// `syncMessages` shrink path, but reset the sync cursor so the log
|
|
2084
|
-
// can't carry the dropped tail forward if no further turn ever runs.
|
|
2085
|
-
const messages = advisorAgent.state.messages;
|
|
2086
|
-
if (count < messages.length) {
|
|
2087
|
-
messages.length = count;
|
|
2067
|
+
} else {
|
|
2068
|
+
const sel = resolveAdvisorRoleSelection(
|
|
2069
|
+
this.settings,
|
|
2070
|
+
this.#modelRegistry.getAvailable(),
|
|
2071
|
+
this.#modelRegistry,
|
|
2072
|
+
);
|
|
2073
|
+
if (!sel) {
|
|
2074
|
+
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2075
|
+
advisor: config.name,
|
|
2076
|
+
});
|
|
2077
|
+
continue;
|
|
2088
2078
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2079
|
+
model = sel.model;
|
|
2080
|
+
thinkingLevel = sel.thinkingLevel;
|
|
2081
|
+
}
|
|
2082
|
+
const advisorModel = model;
|
|
2083
|
+
const advisorName = config.name;
|
|
2084
|
+
const advisorThinkingLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
2085
|
+
|
|
2086
|
+
const emissionGuard = new AdvisorEmissionGuard();
|
|
2087
|
+
const adviseTool = new AdviseTool((note, severity) => this.#routeAdvice(advisorRef, note, severity));
|
|
2088
|
+
|
|
2089
|
+
// `#advisorWatchdogPrompt` already carries WATCHDOG.md + YAML shared
|
|
2090
|
+
// instructions; `config.instructions` adds this advisor's specialization.
|
|
2091
|
+
const systemPrompt = [advisorSystemPrompt];
|
|
2092
|
+
if (this.#advisorContextPrompt) systemPrompt.push(this.#advisorContextPrompt);
|
|
2093
|
+
if (this.#advisorWatchdogPrompt) systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
2094
|
+
if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
|
|
2095
|
+
if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
|
|
2096
|
+
|
|
2097
|
+
const names = config.tools?.length ? new Set(config.tools) : ADVISOR_DEFAULT_TOOL_NAMES;
|
|
2098
|
+
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
2099
|
+
|
|
2100
|
+
const advisorSessionId = this.#advisorSessionId(slug);
|
|
2101
|
+
const appendOnlyContext = new AppendOnlyContextManager();
|
|
2102
|
+
|
|
2103
|
+
// Thread the primary's telemetry into the advisor loop so the advisor
|
|
2104
|
+
// model's GenAI spans + usage/cost hooks fire stamped with the advisor's
|
|
2105
|
+
// own identity. `conversationId` is cleared so the advisor loop falls back
|
|
2106
|
+
// to its own session id; undefined telemetry stays undefined.
|
|
2107
|
+
const advisorTelemetry = this.agent.telemetry
|
|
2108
|
+
? {
|
|
2109
|
+
...this.agent.telemetry,
|
|
2110
|
+
agent: {
|
|
2111
|
+
id: advisorSessionId,
|
|
2112
|
+
name: slug ? `${MODEL_ROLES.advisor.name}: ${advisorName}` : MODEL_ROLES.advisor.name,
|
|
2113
|
+
description: formatModelString(advisorModel),
|
|
2114
|
+
},
|
|
2115
|
+
conversationId: undefined,
|
|
2116
|
+
}
|
|
2117
|
+
: undefined;
|
|
2118
|
+
// Mirror the SDK's provider-shaping options (streamFn/onPayload/...,
|
|
2119
|
+
// providerSessionState, promptCacheKey, transformProviderContext) so each
|
|
2120
|
+
// advisor's requests cache, route, and obfuscate like the main turn.
|
|
2121
|
+
const advisorAgent = new Agent({
|
|
2122
|
+
initialState: {
|
|
2123
|
+
systemPrompt,
|
|
2124
|
+
model: advisorModel,
|
|
2125
|
+
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
2126
|
+
tools: [adviseTool, ...tools],
|
|
2127
|
+
},
|
|
2128
|
+
appendOnlyContext,
|
|
2129
|
+
sessionId: advisorSessionId,
|
|
2130
|
+
promptCacheKey: advisorSessionId,
|
|
2131
|
+
providerSessionState: this.#providerSessionState,
|
|
2132
|
+
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
2133
|
+
streamFn: this.#advisorStreamFn,
|
|
2134
|
+
onPayload: this.#onPayload,
|
|
2135
|
+
onResponse: this.#onResponse,
|
|
2136
|
+
onSseEvent: this.#onSseEvent,
|
|
2137
|
+
transformProviderContext: this.#transformProviderContext,
|
|
2138
|
+
intentTracing: false,
|
|
2139
|
+
telemetry: advisorTelemetry,
|
|
2140
|
+
serviceTier: advisorServiceTier,
|
|
2141
|
+
serviceTierResolver: advisorServiceTierResolver,
|
|
2142
|
+
});
|
|
2143
|
+
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2144
|
+
|
|
2145
|
+
const advisorAgentFacade: AdvisorAgent = {
|
|
2146
|
+
prompt: input => advisorAgent.prompt(input),
|
|
2147
|
+
abort: reason => advisorAgent.abort(reason),
|
|
2148
|
+
reset: () => {
|
|
2149
|
+
advisorAgent.reset();
|
|
2150
|
+
appendOnlyContext.log.clear();
|
|
2151
|
+
},
|
|
2152
|
+
rollbackTo: count => {
|
|
2153
|
+
// Drop the failed user batch + synthetic assistant-error turn
|
|
2154
|
+
// `Agent.#runLoop` appended for a turn ending in `stopReason: "error"`.
|
|
2155
|
+
const messages = advisorAgent.state.messages;
|
|
2156
|
+
if (count < messages.length) {
|
|
2157
|
+
messages.length = count;
|
|
2158
|
+
}
|
|
2159
|
+
appendOnlyContext.resetSyncCursor();
|
|
2160
|
+
advisorAgent.state.error = undefined;
|
|
2161
|
+
},
|
|
2162
|
+
state: advisorAgent.state,
|
|
2163
|
+
};
|
|
2094
2164
|
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2165
|
+
// Persist this advisor's turns to `<session>/__advisor[.<slug>].jsonl`
|
|
2166
|
+
// (resolved lazily so it follows session switches) for stats attribution
|
|
2167
|
+
// and Agent Hub observability, without registering it as a peer.
|
|
2168
|
+
const recorder = new AdvisorTranscriptRecorder(
|
|
2169
|
+
() => this.sessionManager.getSessionFile(),
|
|
2170
|
+
() => this.sessionManager.getCwd(),
|
|
2171
|
+
advisorTranscriptFilename(slug),
|
|
2172
|
+
// On the advisor on→off→on toggle, wait for the prior recorders' closes
|
|
2173
|
+
// so two SessionManagers never hold the same file at once.
|
|
2174
|
+
this.#advisorRecorderClosed,
|
|
2175
|
+
);
|
|
2176
|
+
const runtime = new AdvisorRuntime(advisorAgentFacade, {
|
|
2177
|
+
snapshotMessages: () => this.agent.state.messages,
|
|
2178
|
+
enqueueAdvice: (note, severity) => this.#routeAdvice(advisorRef, note, severity),
|
|
2179
|
+
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
2180
|
+
obfuscator: this.#obfuscator,
|
|
2181
|
+
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
2182
|
+
notifyFailure: error => {
|
|
2183
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2184
|
+
this.emitNotice(
|
|
2185
|
+
"warning",
|
|
2186
|
+
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(advisorModel)}: ${message}`,
|
|
2187
|
+
"advisor",
|
|
2188
|
+
);
|
|
2189
|
+
},
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
const advisorRef: ActiveAdvisor = {
|
|
2193
|
+
name: advisorName,
|
|
2194
|
+
slug,
|
|
2195
|
+
agent: advisorAgent,
|
|
2196
|
+
runtime,
|
|
2197
|
+
adviseTool,
|
|
2198
|
+
emissionGuard,
|
|
2199
|
+
recorder,
|
|
2200
|
+
recorderClosed: Promise.resolve(),
|
|
2201
|
+
model: advisorModel,
|
|
2202
|
+
thinkingLevel: advisorThinkingLevel,
|
|
2203
|
+
};
|
|
2204
|
+
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
2205
|
+
if (seedToCurrent) runtime.seedTo(this.agent.state.messages.length);
|
|
2206
|
+
this.#advisors.push(advisorRef);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// One shared non-blocking aside channel for all advisors; the build callback
|
|
2210
|
+
// aggregates every advisor's queued nits into one card (each entry already
|
|
2211
|
+
// carries its own `advisor` name).
|
|
2212
|
+
if (this.#advisors.length > 0 && !this.#advisorYieldQueueUnsubscribe) {
|
|
2213
|
+
this.#advisorYieldQueueUnsubscribe = this.yieldQueue.register<AdvisorNote>("advisor", {
|
|
2214
|
+
build: entries =>
|
|
2215
|
+
entries.length === 0
|
|
2216
|
+
? null
|
|
2217
|
+
: ({
|
|
2218
|
+
role: "custom",
|
|
2219
|
+
customType: "advisor",
|
|
2220
|
+
display: true,
|
|
2221
|
+
attribution: "agent",
|
|
2222
|
+
timestamp: Date.now(),
|
|
2223
|
+
content: formatAdvisorBatchContent(entries),
|
|
2224
|
+
details: { notes: entries } satisfies AdvisorMessageDetails,
|
|
2225
|
+
} satisfies CustomMessage),
|
|
2226
|
+
skipIdleFlush: true,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return this.#advisors.length > 0;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/** Provider/session id for an advisor's loop. The slug suffix MUST match the
|
|
2234
|
+
* advisor's transcript filename so stats/telemetry attribute the same advisor. */
|
|
2235
|
+
#advisorSessionId(slug: string): string | undefined {
|
|
2236
|
+
if (!this.sessionId) return undefined;
|
|
2237
|
+
return slug ? `${this.sessionId}-advisor-${slug}` : `${this.sessionId}-advisor`;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
/**
|
|
2241
|
+
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
2242
|
+
* blocker interrupt the running agent through the steering channel; once the
|
|
2243
|
+
* loop has yielded, `triggerTurn` resumes it. After a deliberate user interrupt
|
|
2244
|
+
* auto-resume is suppressed while idle/unwinding (the note becomes a preserved
|
|
2245
|
+
* card re-entering on resume); a live-streaming turn is steered in directly. A
|
|
2246
|
+
* plain nit always rides the non-interrupting YieldQueue aside. Suppression by
|
|
2247
|
+
* the per-advisor emission guard drops the note silently — the model still saw
|
|
2248
|
+
* `Recorded.`, so it isn't tempted to rephrase the same note past the dedupe.
|
|
2249
|
+
*/
|
|
2250
|
+
#routeAdvice(advisor: ActiveAdvisor, note: string, severity?: AdvisorSeverity): void {
|
|
2251
|
+
if (!advisor.emissionGuard.accept(note)) {
|
|
2252
|
+
logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
// The implicit single ("default") advisor stamps no source name, so its
|
|
2256
|
+
// agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
|
|
2257
|
+
const source = advisor.slug ? advisor.name : undefined;
|
|
2258
|
+
const interrupting = isInterruptingSeverity(severity);
|
|
2259
|
+
const channel = resolveAdvisorDeliveryChannel({
|
|
2260
|
+
severity,
|
|
2261
|
+
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
2262
|
+
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
2263
|
+
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
2264
|
+
// loop consumes a steer at its next boundary.
|
|
2265
|
+
streaming: this.agent.state.isStreaming,
|
|
2266
|
+
aborting: this.#abortInProgress,
|
|
2267
|
+
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
2142
2268
|
});
|
|
2269
|
+
if (channel === "aside") {
|
|
2270
|
+
this.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const notes: AdvisorNote[] = [{ note, severity, advisor: source }];
|
|
2274
|
+
const content = formatAdvisorBatchContent(notes);
|
|
2275
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
2276
|
+
if (channel === "preserve") {
|
|
2277
|
+
this.#preserveAdvisorCard({
|
|
2278
|
+
role: "custom",
|
|
2279
|
+
customType: "advisor",
|
|
2280
|
+
content,
|
|
2281
|
+
display: true,
|
|
2282
|
+
attribution: "agent",
|
|
2283
|
+
details,
|
|
2284
|
+
timestamp: Date.now(),
|
|
2285
|
+
});
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
this.#recordAdvisorInterruptDelivered();
|
|
2289
|
+
void this.sendCustomMessage(
|
|
2290
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
2291
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
2292
|
+
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
2293
|
+
}
|
|
2143
2294
|
|
|
2144
|
-
|
|
2295
|
+
/** Re-prime every advisor's transcript view (compaction/shake/rewind) without the
|
|
2296
|
+
* session-level latch reset {@link #resetAdvisorSessionState} performs. */
|
|
2297
|
+
#resetAllAdvisorRuntimes(): void {
|
|
2298
|
+
for (const a of this.#advisors) a.runtime.reset();
|
|
2145
2299
|
}
|
|
2146
2300
|
|
|
2147
2301
|
#stopAdvisorRuntime(): void {
|
|
2148
|
-
// Detach
|
|
2302
|
+
// Detach each recorder feed BEFORE aborting its advisor agent: dispose() aborts
|
|
2149
2303
|
// the loop, and an abort emits a final `message_end` we must not enqueue against
|
|
2150
2304
|
// a closing recorder (it would reopen and resurrect an already-released file).
|
|
2151
|
-
|
|
2152
|
-
this.#
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
if (this.#advisorTranscriptRecorder) {
|
|
2158
|
-
// Capture the close so dispose()/`/drop` can await the queued open+append+close —
|
|
2305
|
+
const closes: Promise<void>[] = [];
|
|
2306
|
+
for (const a of this.#advisors) {
|
|
2307
|
+
a.agentUnsubscribe?.();
|
|
2308
|
+
a.agentUnsubscribe = undefined;
|
|
2309
|
+
a.runtime.dispose();
|
|
2310
|
+
// Capture each close so dispose()/`/drop` can await the queued open+append+close —
|
|
2159
2311
|
// the last advisor turn would otherwise be lost on a fast process exit.
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
}
|
|
2163
|
-
if (this.#advisorAgent) {
|
|
2164
|
-
this.#advisorAgent = undefined;
|
|
2312
|
+
a.recorderClosed = a.recorder.close();
|
|
2313
|
+
closes.push(a.recorderClosed);
|
|
2165
2314
|
}
|
|
2166
|
-
this.#
|
|
2315
|
+
this.#advisorRecorderClosed = Promise.all(closes).then(() => {});
|
|
2316
|
+
this.#advisors = [];
|
|
2167
2317
|
this.#advisorYieldQueueUnsubscribe?.();
|
|
2168
2318
|
this.#advisorYieldQueueUnsubscribe = undefined;
|
|
2169
2319
|
}
|
|
@@ -2171,16 +2321,13 @@ export class AgentSession {
|
|
|
2171
2321
|
/** Subscribe the advisor agent's finalized messages into the transcript recorder.
|
|
2172
2322
|
* Idempotent-by-replacement: callers detach the prior feed first. Kept separate
|
|
2173
2323
|
* so the re-prime path can mute the feed across an abort-driven reset. */
|
|
2174
|
-
#attachAdvisorRecorderFeed(): void {
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
if (!agent || !recorder) return;
|
|
2178
|
-
this.#advisorAgentUnsubscribe = agent.subscribe(event => {
|
|
2179
|
-
if (event.type === "message_end") recorder.record(event.message);
|
|
2324
|
+
#attachAdvisorRecorderFeed(advisor: ActiveAdvisor): void {
|
|
2325
|
+
advisor.agentUnsubscribe = advisor.agent.subscribe(event => {
|
|
2326
|
+
if (event.type === "message_end") advisor.recorder.record(event.message);
|
|
2180
2327
|
});
|
|
2181
2328
|
}
|
|
2182
2329
|
|
|
2183
|
-
async #promoteAdvisorContextModel(currentModel: Model): Promise<boolean> {
|
|
2330
|
+
async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
|
|
2184
2331
|
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
2185
2332
|
if (!promotionSettings.enabled) return false;
|
|
2186
2333
|
const contextWindow = currentModel.contextWindow ?? 0;
|
|
@@ -2188,25 +2335,23 @@ export class AgentSession {
|
|
|
2188
2335
|
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
2189
2336
|
if (!targetModel) return false;
|
|
2190
2337
|
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
this.#modelRegistry,
|
|
2195
|
-
);
|
|
2196
|
-
const advisorThinkingLevel = advisorSel?.thinkingLevel ?? ThinkingLevel.Medium;
|
|
2197
|
-
|
|
2338
|
+
// Preserve this advisor's own thinking level (a configured `model:...:high`
|
|
2339
|
+
// keeps its suffix across a promotion); only the model changes.
|
|
2340
|
+
const advisorThinkingLevel = advisor.thinkingLevel;
|
|
2198
2341
|
try {
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2342
|
+
advisor.agent.setModel(targetModel);
|
|
2343
|
+
advisor.agent.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
2344
|
+
advisor.agent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2345
|
+
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
2203
2346
|
logger.debug("Advisor context promotion switched model on overflow", {
|
|
2347
|
+
advisor: advisor.name,
|
|
2204
2348
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2205
2349
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2206
2350
|
});
|
|
2207
2351
|
return true;
|
|
2208
2352
|
} catch (error) {
|
|
2209
2353
|
logger.warn("Advisor context promotion failed", {
|
|
2354
|
+
advisor: advisor.name,
|
|
2210
2355
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2211
2356
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2212
2357
|
error: String(error),
|
|
@@ -2215,19 +2360,18 @@ export class AgentSession {
|
|
|
2215
2360
|
}
|
|
2216
2361
|
}
|
|
2217
2362
|
|
|
2218
|
-
async #maintainAdvisorContext(incomingTokens: number): Promise<boolean> {
|
|
2219
|
-
const
|
|
2220
|
-
if (!advisor) return false;
|
|
2363
|
+
async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
|
|
2364
|
+
const agent = advisor.agent;
|
|
2221
2365
|
|
|
2222
2366
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
2223
2367
|
if (compactionSettings.strategy === "off") return false;
|
|
2224
2368
|
if (!compactionSettings.enabled) return false;
|
|
2225
2369
|
|
|
2226
|
-
const advisorModel =
|
|
2370
|
+
const advisorModel = agent.state.model;
|
|
2227
2371
|
const contextWindow = advisorModel.contextWindow ?? 0;
|
|
2228
2372
|
if (contextWindow <= 0) return false;
|
|
2229
2373
|
|
|
2230
|
-
const messages =
|
|
2374
|
+
const messages = agent.state.messages;
|
|
2231
2375
|
let contextTokens = incomingTokens;
|
|
2232
2376
|
for (const message of messages) {
|
|
2233
2377
|
contextTokens += estimateTokens(message);
|
|
@@ -2238,9 +2382,9 @@ export class AgentSession {
|
|
|
2238
2382
|
}
|
|
2239
2383
|
|
|
2240
2384
|
// 1. Try promotion first
|
|
2241
|
-
if (await this.#promoteAdvisorContextModel(advisorModel)) {
|
|
2385
|
+
if (await this.#promoteAdvisorContextModel(advisor, advisorModel)) {
|
|
2242
2386
|
// Promotion succeeded, check if new model has enough space
|
|
2243
|
-
const newModel =
|
|
2387
|
+
const newModel = agent.state.model;
|
|
2244
2388
|
const newWindow = newModel.contextWindow ?? 0;
|
|
2245
2389
|
if (newWindow > 0) {
|
|
2246
2390
|
const stillNeedsCompaction = shouldCompact(contextTokens, newWindow, compactionSettings);
|
|
@@ -2262,7 +2406,9 @@ export class AgentSession {
|
|
|
2262
2406
|
timestamp,
|
|
2263
2407
|
summary: message.summary,
|
|
2264
2408
|
shortSummary: message.shortSummary,
|
|
2265
|
-
firstKeptEntryId:
|
|
2409
|
+
firstKeptEntryId:
|
|
2410
|
+
(message as CompactionSummaryMessage & { firstKeptEntryId?: string }).firstKeptEntryId ||
|
|
2411
|
+
`msg-${i + 1}`,
|
|
2266
2412
|
tokensBefore: message.tokensBefore,
|
|
2267
2413
|
} satisfies CompactionEntry;
|
|
2268
2414
|
}
|
|
@@ -2276,33 +2422,37 @@ export class AgentSession {
|
|
|
2276
2422
|
} satisfies SessionMessageEntry;
|
|
2277
2423
|
});
|
|
2278
2424
|
|
|
2279
|
-
const
|
|
2425
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
2426
|
+
const candidates = this.#resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
2427
|
+
if (candidates.length === 0) {
|
|
2428
|
+
// No compaction candidates, fallback to re-prime
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
const advisorSessionId = this.#advisorSessionId(advisor.slug);
|
|
2432
|
+
const preparation = prepareCompaction(
|
|
2433
|
+
pathEntries,
|
|
2434
|
+
compactionSettings,
|
|
2435
|
+
await this.#runnableCompactionCandidates(candidates, advisorSessionId),
|
|
2436
|
+
);
|
|
2280
2437
|
if (!preparation) {
|
|
2281
2438
|
// Cannot prepare compaction, fallback to re-prime
|
|
2282
2439
|
return true;
|
|
2283
2440
|
}
|
|
2284
2441
|
|
|
2285
|
-
const advisorCompactionThinkingLevel: ThinkingLevel | undefined =
|
|
2442
|
+
const advisorCompactionThinkingLevel: ThinkingLevel | undefined = agent.state.disableReasoning
|
|
2286
2443
|
? ThinkingLevel.Off
|
|
2287
|
-
:
|
|
2444
|
+
: agent.state.thinkingLevel;
|
|
2288
2445
|
|
|
2289
2446
|
// Advisor state is in-memory-only, so snapcompact's frame archive has no
|
|
2290
2447
|
// stable SessionEntry preserveData slot to carry across future advisor
|
|
2291
2448
|
// maintenance runs. Use an LLM summary even when the primary session is
|
|
2292
2449
|
// configured for snapcompact.
|
|
2293
|
-
const availableModels = this.#modelRegistry.getAvailable();
|
|
2294
|
-
const candidates = this.#resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
2295
|
-
if (candidates.length === 0) {
|
|
2296
|
-
// No compaction candidates, fallback to re-prime
|
|
2297
|
-
return true;
|
|
2298
|
-
}
|
|
2299
2450
|
|
|
2300
2451
|
let compactResult: CompactionResult | undefined;
|
|
2301
2452
|
let lastError: unknown;
|
|
2302
|
-
const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
|
|
2303
2453
|
// Instrument the advisor's overflow-compaction one-shot like the primary
|
|
2304
2454
|
// compaction path so the advisor model's maintenance call also emits spans.
|
|
2305
|
-
const telemetry = resolveTelemetry(
|
|
2455
|
+
const telemetry = resolveTelemetry(agent.telemetry, advisorSessionId);
|
|
2306
2456
|
|
|
2307
2457
|
for (const candidate of candidates) {
|
|
2308
2458
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorSessionId);
|
|
@@ -2319,6 +2469,9 @@ export class AgentSession {
|
|
|
2319
2469
|
thinkingLevel: advisorCompactionThinkingLevel,
|
|
2320
2470
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
2321
2471
|
telemetry,
|
|
2472
|
+
tools: agent.state.tools,
|
|
2473
|
+
sessionId: advisorSessionId,
|
|
2474
|
+
promptCacheKey: advisorSessionId,
|
|
2322
2475
|
},
|
|
2323
2476
|
);
|
|
2324
2477
|
break;
|
|
@@ -2343,7 +2496,7 @@ export class AgentSession {
|
|
|
2343
2496
|
firstKeptEntryId,
|
|
2344
2497
|
} as CompactionSummaryMessage & { firstKeptEntryId?: string };
|
|
2345
2498
|
|
|
2346
|
-
|
|
2499
|
+
agent.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
2347
2500
|
return false;
|
|
2348
2501
|
}
|
|
2349
2502
|
|
|
@@ -2776,6 +2929,37 @@ export class AgentSession {
|
|
|
2776
2929
|
}
|
|
2777
2930
|
}
|
|
2778
2931
|
|
|
2932
|
+
/**
|
|
2933
|
+
* On a user-interrupted (`Esc`) abort, copy the trailing thinking run into a
|
|
2934
|
+
* hidden `display: false` continuity message for the next turn WITHOUT
|
|
2935
|
+
* mutating the assistant message. The original thinking stays on the message
|
|
2936
|
+
* so live render, reload, and Ctrl+L rebuilds keep showing it; `convertToLlm`
|
|
2937
|
+
* strips the run from the provider request (incomplete/unsigned thinking is
|
|
2938
|
+
* rejected on resend) when this continuity message follows the assistant turn.
|
|
2939
|
+
*/
|
|
2940
|
+
#demoteInterruptedThinkingOnUserInterrupt(
|
|
2941
|
+
message: AssistantMessage,
|
|
2942
|
+
): CustomMessage<InterruptedThinkingDetails> | undefined {
|
|
2943
|
+
if (message.stopReason !== "aborted" || !isUserInterruptAbort(message)) return undefined;
|
|
2944
|
+
const demoted = demoteInterruptedThinking(message);
|
|
2945
|
+
if (!demoted) return undefined;
|
|
2946
|
+
const interruptedAt = Date.now();
|
|
2947
|
+
return {
|
|
2948
|
+
role: "custom",
|
|
2949
|
+
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
2950
|
+
content: prompt.render(interruptedThinkingTemplate, { reasoning: demoted.reasoning }),
|
|
2951
|
+
display: false,
|
|
2952
|
+
details: {
|
|
2953
|
+
interruptedAt,
|
|
2954
|
+
provider: message.provider,
|
|
2955
|
+
model: message.model,
|
|
2956
|
+
blockCount: demoted.blockCount,
|
|
2957
|
+
},
|
|
2958
|
+
attribution: "agent",
|
|
2959
|
+
timestamp: interruptedAt,
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2779
2963
|
async #persistTurnMessagesForMidRunCompaction(context: AgentTurnEndContext | undefined): Promise<boolean> {
|
|
2780
2964
|
if (!context) return true;
|
|
2781
2965
|
const turnMessages = [context.message, ...context.toolResults];
|
|
@@ -2843,6 +3027,17 @@ export class AgentSession {
|
|
|
2843
3027
|
}
|
|
2844
3028
|
}
|
|
2845
3029
|
|
|
3030
|
+
const interruptedThinkingMessage =
|
|
3031
|
+
event.type === "message_end" && event.message.role === "assistant"
|
|
3032
|
+
? this.#demoteInterruptedThinkingOnUserInterrupt(event.message as AssistantMessage)
|
|
3033
|
+
: undefined;
|
|
3034
|
+
// `message_end` handling is fire-and-forget from agent-core. Make the
|
|
3035
|
+
// hidden continuity turn visible to the next prompt before any awaited
|
|
3036
|
+
// extension delivery or persistence can stall this handler.
|
|
3037
|
+
if (interruptedThinkingMessage) {
|
|
3038
|
+
this.agent.appendMessage(interruptedThinkingMessage);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
2846
3041
|
const messageEndPersistence =
|
|
2847
3042
|
event.type === "message_end" ? this.#createMessageEndPersistenceSlot(event.message) : undefined;
|
|
2848
3043
|
|
|
@@ -2985,6 +3180,15 @@ export class AgentSession {
|
|
|
2985
3180
|
} else {
|
|
2986
3181
|
persistMessageEnd();
|
|
2987
3182
|
}
|
|
3183
|
+
if (interruptedThinkingMessage) {
|
|
3184
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
3185
|
+
interruptedThinkingMessage.customType,
|
|
3186
|
+
interruptedThinkingMessage.content,
|
|
3187
|
+
interruptedThinkingMessage.display,
|
|
3188
|
+
interruptedThinkingMessage.details,
|
|
3189
|
+
interruptedThinkingMessage.attribution,
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
2988
3192
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
2989
3193
|
|
|
2990
3194
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
@@ -3041,9 +3245,10 @@ export class AgentSession {
|
|
|
3041
3245
|
}
|
|
3042
3246
|
}
|
|
3043
3247
|
if (event.message.role === "toolResult") {
|
|
3044
|
-
const { toolName, details, isError, content } = event.message as {
|
|
3248
|
+
const { toolName, toolCallId, details, isError, content } = event.message as {
|
|
3249
|
+
toolCallId?: string;
|
|
3045
3250
|
toolName?: string;
|
|
3046
|
-
details?: { path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
|
|
3251
|
+
details?: { op?: string; path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
|
|
3047
3252
|
isError?: boolean;
|
|
3048
3253
|
content?: Array<TextContent | ImageContent>;
|
|
3049
3254
|
};
|
|
@@ -3057,6 +3262,9 @@ export class AgentSession {
|
|
|
3057
3262
|
}
|
|
3058
3263
|
if (toolName === "todo" && !isError && Array.isArray(details?.phases)) {
|
|
3059
3264
|
this.setTodoPhases(details.phases);
|
|
3265
|
+
if (this.#isTodoInitResult(details, toolCallId)) {
|
|
3266
|
+
this.#scheduleReplanTitleRefresh();
|
|
3267
|
+
}
|
|
3060
3268
|
}
|
|
3061
3269
|
if (toolName === "todo" && isError) {
|
|
3062
3270
|
const errorText = content?.find(part => part.type === "text")?.text;
|
|
@@ -6526,7 +6734,10 @@ export class AgentSession {
|
|
|
6526
6734
|
|
|
6527
6735
|
async promptCustomMessage<T = unknown>(
|
|
6528
6736
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
6529
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & {
|
|
6737
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & {
|
|
6738
|
+
queueChipText?: string;
|
|
6739
|
+
queueOnly?: boolean;
|
|
6740
|
+
},
|
|
6530
6741
|
): Promise<void> {
|
|
6531
6742
|
const textContent =
|
|
6532
6743
|
typeof message.content === "string"
|
|
@@ -6546,6 +6757,16 @@ export class AgentSession {
|
|
|
6546
6757
|
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
6547
6758
|
}
|
|
6548
6759
|
|
|
6760
|
+
if (options?.queueOnly) {
|
|
6761
|
+
if (!options.streamingBehavior) {
|
|
6762
|
+
throw new AgentBusyError();
|
|
6763
|
+
}
|
|
6764
|
+
for (const notice of keywordNotices) {
|
|
6765
|
+
await this.#queueCustomMessage(notice, options.streamingBehavior);
|
|
6766
|
+
}
|
|
6767
|
+
await this.#queueCustomMessage(message, options.streamingBehavior, options.queueChipText);
|
|
6768
|
+
return;
|
|
6769
|
+
}
|
|
6549
6770
|
if (this.isStreaming) {
|
|
6550
6771
|
if (!options?.streamingBehavior) {
|
|
6551
6772
|
throw new AgentBusyError();
|
|
@@ -7111,6 +7332,40 @@ export class AgentSession {
|
|
|
7111
7332
|
}
|
|
7112
7333
|
}
|
|
7113
7334
|
|
|
7335
|
+
/** Queue a custom message without starting a turn, matching steer/follow-up delivery. */
|
|
7336
|
+
async #queueCustomMessage<T = unknown>(
|
|
7337
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
7338
|
+
deliverAs: "steer" | "followUp",
|
|
7339
|
+
queueChipText?: string,
|
|
7340
|
+
): Promise<void> {
|
|
7341
|
+
const details =
|
|
7342
|
+
queueChipText !== undefined
|
|
7343
|
+
? ({
|
|
7344
|
+
...((message.details && typeof message.details === "object" ? message.details : {}) as Record<
|
|
7345
|
+
string,
|
|
7346
|
+
unknown
|
|
7347
|
+
>),
|
|
7348
|
+
__queueChipText: queueChipText,
|
|
7349
|
+
} as T)
|
|
7350
|
+
: message.details;
|
|
7351
|
+
const appMessage: CustomMessage<T> = {
|
|
7352
|
+
role: "custom",
|
|
7353
|
+
customType: message.customType,
|
|
7354
|
+
content: message.content,
|
|
7355
|
+
display: message.display,
|
|
7356
|
+
details,
|
|
7357
|
+
attribution: message.attribution ?? "agent",
|
|
7358
|
+
timestamp: Date.now(),
|
|
7359
|
+
};
|
|
7360
|
+
const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
|
|
7361
|
+
if (deliverAs === "followUp") {
|
|
7362
|
+
this.agent.followUp(normalizedAppMessage);
|
|
7363
|
+
} else {
|
|
7364
|
+
this.agent.steer(normalizedAppMessage);
|
|
7365
|
+
}
|
|
7366
|
+
this.#scheduleIdleQueueDrain();
|
|
7367
|
+
}
|
|
7368
|
+
|
|
7114
7369
|
/**
|
|
7115
7370
|
* Send a custom message to the session. Creates a CustomMessageEntry.
|
|
7116
7371
|
*
|
|
@@ -7353,6 +7608,74 @@ export class AgentSession {
|
|
|
7353
7608
|
this.#todoPhases = this.#cloneTodoPhases(phases);
|
|
7354
7609
|
}
|
|
7355
7610
|
|
|
7611
|
+
#isTodoInitResult(details: Record<string, unknown>, toolCallId: string | undefined): boolean {
|
|
7612
|
+
const detailOp = getStringProperty(details, "op");
|
|
7613
|
+
if (detailOp) return detailOp === "init";
|
|
7614
|
+
if (!toolCallId) return false;
|
|
7615
|
+
for (let i = this.agent.state.messages.length - 1; i >= 0; i--) {
|
|
7616
|
+
const message = this.agent.state.messages[i];
|
|
7617
|
+
if (!message) continue;
|
|
7618
|
+
const op = toolCallOpFromMessage(message, toolCallId);
|
|
7619
|
+
if (op) return op === "init";
|
|
7620
|
+
}
|
|
7621
|
+
return false;
|
|
7622
|
+
}
|
|
7623
|
+
|
|
7624
|
+
#buildReplanTitleContext(): string {
|
|
7625
|
+
const turns: TitleConversationTurn[] = [];
|
|
7626
|
+
for (
|
|
7627
|
+
let i = this.agent.state.messages.length - 1;
|
|
7628
|
+
i >= 0 && turns.length < REPLAN_TITLE_CONTEXT_TURN_LIMIT;
|
|
7629
|
+
i--
|
|
7630
|
+
) {
|
|
7631
|
+
const message = this.agent.state.messages[i];
|
|
7632
|
+
if (!message) continue;
|
|
7633
|
+
const turn = titleConversationTurnFromMessage(message);
|
|
7634
|
+
if (turn) turns.push(turn);
|
|
7635
|
+
}
|
|
7636
|
+
turns.reverse();
|
|
7637
|
+
return formatTitleConversationContext(turns);
|
|
7638
|
+
}
|
|
7639
|
+
|
|
7640
|
+
#scheduleReplanTitleRefresh(): void {
|
|
7641
|
+
if (this.#replanTitleRefreshInFlight) return;
|
|
7642
|
+
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
7643
|
+
if (this.sessionManager.titleSource === "user") return;
|
|
7644
|
+
const context = this.#buildReplanTitleContext();
|
|
7645
|
+
if (!context) return;
|
|
7646
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
7647
|
+
const refresh = this.#refreshTitleAfterReplan(context, sessionId)
|
|
7648
|
+
.catch(err => {
|
|
7649
|
+
logger.warn("title-generator: replan refresh failed", {
|
|
7650
|
+
sessionId,
|
|
7651
|
+
error: err instanceof Error ? err.message : String(err),
|
|
7652
|
+
});
|
|
7653
|
+
})
|
|
7654
|
+
.finally(() => {
|
|
7655
|
+
if (this.#replanTitleRefreshInFlight === refresh) {
|
|
7656
|
+
this.#replanTitleRefreshInFlight = undefined;
|
|
7657
|
+
}
|
|
7658
|
+
});
|
|
7659
|
+
this.#replanTitleRefreshInFlight = refresh;
|
|
7660
|
+
}
|
|
7661
|
+
|
|
7662
|
+
async #refreshTitleAfterReplan(context: string, sessionId: string): Promise<void> {
|
|
7663
|
+
const title = await generateSessionTitle(
|
|
7664
|
+
context,
|
|
7665
|
+
this.#modelRegistry,
|
|
7666
|
+
this.settings,
|
|
7667
|
+
sessionId,
|
|
7668
|
+
this.model,
|
|
7669
|
+
provider => this.agent.metadataForProvider(provider),
|
|
7670
|
+
);
|
|
7671
|
+
if (!title) return;
|
|
7672
|
+
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
7673
|
+
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
7674
|
+
if (this.sessionManager.titleSource === "user") return;
|
|
7675
|
+
const setSessionName = this.sessionManager.setSessionName as SetSessionNameWithTrigger;
|
|
7676
|
+
await setSessionName.call(this.sessionManager, title, "auto", "replan");
|
|
7677
|
+
}
|
|
7678
|
+
|
|
7356
7679
|
#syncTodoPhasesFromBranch(): void {
|
|
7357
7680
|
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
7358
7681
|
// Strip completed/abandoned tasks — they were done in a previous run,
|
|
@@ -7495,9 +7818,11 @@ export class AgentSession {
|
|
|
7495
7818
|
// running advisor turn could otherwise finish, emit `message_end`, and recreate
|
|
7496
7819
|
// `<old>/__advisor.jsonl`. #resetAdvisorSessionState (after newSession) re-primes
|
|
7497
7820
|
// the advisor and re-attaches the feed at the new session's path.
|
|
7498
|
-
this.#
|
|
7499
|
-
|
|
7500
|
-
|
|
7821
|
+
for (const a of this.#advisors) {
|
|
7822
|
+
a.agentUnsubscribe?.();
|
|
7823
|
+
a.agentUnsubscribe = undefined;
|
|
7824
|
+
await a.recorder.close();
|
|
7825
|
+
}
|
|
7501
7826
|
try {
|
|
7502
7827
|
await this.sessionManager.dropSession(previousSessionFile);
|
|
7503
7828
|
} catch (err) {
|
|
@@ -7551,8 +7876,9 @@ export class AgentSession {
|
|
|
7551
7876
|
/**
|
|
7552
7877
|
* Set a display name for the current session.
|
|
7553
7878
|
*/
|
|
7554
|
-
setSessionName(name: string, source: "auto" | "user" = "auto"): Promise<boolean> {
|
|
7555
|
-
|
|
7879
|
+
setSessionName(name: string, source: "auto" | "user" = "auto", trigger?: SessionNameTrigger): Promise<boolean> {
|
|
7880
|
+
const setSessionName = this.sessionManager.setSessionName as SetSessionNameWithTrigger;
|
|
7881
|
+
return setSessionName.call(this.sessionManager, name, source, trigger);
|
|
7556
7882
|
}
|
|
7557
7883
|
|
|
7558
7884
|
/**
|
|
@@ -7630,16 +7956,24 @@ export class AgentSession {
|
|
|
7630
7956
|
/**
|
|
7631
7957
|
* Set model directly.
|
|
7632
7958
|
* Validates that a credential source is configured (synchronously, without
|
|
7633
|
-
* refreshing OAuth or running command-backed key programs)
|
|
7634
|
-
*
|
|
7635
|
-
*
|
|
7959
|
+
* refreshing OAuth or running command-backed key programs). The active
|
|
7960
|
+
* session switches by default; when `currentContextTokens` is provided and
|
|
7961
|
+
* exceeds the refreshed candidate's context window, the live switch is
|
|
7962
|
+
* skipped while role persistence still runs. Returns whether the active
|
|
7963
|
+
* model actually switched, computed against the refreshed metadata so
|
|
7964
|
+
* dynamic providers (e.g. llama.cpp) honor their post-load contextWindow.
|
|
7636
7965
|
* @throws Error if no API key available for the model
|
|
7637
7966
|
*/
|
|
7638
7967
|
async setModel(
|
|
7639
7968
|
model: Model,
|
|
7640
7969
|
role: string = "default",
|
|
7641
|
-
options?: {
|
|
7642
|
-
|
|
7970
|
+
options?: {
|
|
7971
|
+
selector?: string;
|
|
7972
|
+
thinkingLevel?: ThinkingLevel;
|
|
7973
|
+
persist?: boolean;
|
|
7974
|
+
currentContextTokens?: number;
|
|
7975
|
+
},
|
|
7976
|
+
): Promise<{ switched: boolean }> {
|
|
7643
7977
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
7644
7978
|
if (!this.#modelRegistry.hasConfiguredAuth(model)) {
|
|
7645
7979
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -7647,21 +7981,35 @@ export class AgentSession {
|
|
|
7647
7981
|
|
|
7648
7982
|
const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
|
|
7649
7983
|
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7984
|
+
const currentContextTokens = options?.currentContextTokens ?? 0;
|
|
7985
|
+
const targetContextWindow = targetModel.contextWindow ?? 0;
|
|
7986
|
+
const switched = !(
|
|
7987
|
+
currentContextTokens > 0 &&
|
|
7988
|
+
targetContextWindow > 0 &&
|
|
7989
|
+
currentContextTokens > targetContextWindow
|
|
7990
|
+
);
|
|
7991
|
+
|
|
7992
|
+
if (switched) {
|
|
7993
|
+
this.#clearActiveRetryFallback();
|
|
7994
|
+
this.#setModelWithProviderSessionReset(targetModel);
|
|
7995
|
+
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
7996
|
+
}
|
|
7653
7997
|
if (options?.persist) {
|
|
7654
7998
|
this.settings.setModelRole(
|
|
7655
7999
|
role,
|
|
7656
8000
|
this.#formatRoleModelValue(role, targetModel, options.selector, options.thinkingLevel),
|
|
7657
8001
|
);
|
|
7658
8002
|
}
|
|
8003
|
+
if (!switched) {
|
|
8004
|
+
return { switched: false };
|
|
8005
|
+
}
|
|
7659
8006
|
this.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
7660
8007
|
|
|
7661
8008
|
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
7662
8009
|
// configured defaultLevel; otherwise preserve the current level (or auto).
|
|
7663
8010
|
this.#reapplyThinkingLevel(targetModel.thinking?.defaultLevel);
|
|
7664
8011
|
await this.#syncAfterModelChange(previousEditMode);
|
|
8012
|
+
return { switched: true };
|
|
7665
8013
|
}
|
|
7666
8014
|
|
|
7667
8015
|
/**
|
|
@@ -8186,7 +8534,7 @@ export class AgentSession {
|
|
|
8186
8534
|
await this.sessionManager.rewriteEntries();
|
|
8187
8535
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8188
8536
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8189
|
-
this.#
|
|
8537
|
+
this.#resetAllAdvisorRuntimes();
|
|
8190
8538
|
this.#syncTodoPhasesFromBranch();
|
|
8191
8539
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8192
8540
|
return result;
|
|
@@ -8223,7 +8571,7 @@ export class AgentSession {
|
|
|
8223
8571
|
|
|
8224
8572
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8225
8573
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8226
|
-
this.#
|
|
8574
|
+
this.#resetAllAdvisorRuntimes();
|
|
8227
8575
|
this.#syncTodoPhasesFromBranch();
|
|
8228
8576
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8229
8577
|
return result;
|
|
@@ -8274,7 +8622,7 @@ export class AgentSession {
|
|
|
8274
8622
|
await this.sessionManager.rewriteEntries();
|
|
8275
8623
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8276
8624
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8277
|
-
this.#
|
|
8625
|
+
this.#resetAllAdvisorRuntimes();
|
|
8278
8626
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8279
8627
|
return { removed };
|
|
8280
8628
|
}
|
|
@@ -8331,7 +8679,7 @@ export class AgentSession {
|
|
|
8331
8679
|
await this.sessionManager.rewriteEntries();
|
|
8332
8680
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8333
8681
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8334
|
-
this.#
|
|
8682
|
+
this.#resetAllAdvisorRuntimes();
|
|
8335
8683
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8336
8684
|
|
|
8337
8685
|
return {
|
|
@@ -8427,7 +8775,11 @@ export class AgentSession {
|
|
|
8427
8775
|
compactionCandidates = this.#getCompactionModelCandidates(availableModels);
|
|
8428
8776
|
}
|
|
8429
8777
|
const pathEntries = this.sessionManager.getBranch();
|
|
8430
|
-
const preparation = prepareCompaction(
|
|
8778
|
+
const preparation = prepareCompaction(
|
|
8779
|
+
pathEntries,
|
|
8780
|
+
effectiveSettings,
|
|
8781
|
+
await this.#runnableCompactionCandidates(compactionCandidates, this.sessionId),
|
|
8782
|
+
);
|
|
8431
8783
|
if (!preparation) {
|
|
8432
8784
|
// Check why we can't compact
|
|
8433
8785
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -8617,7 +8969,7 @@ export class AgentSession {
|
|
|
8617
8969
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
8618
8970
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
8619
8971
|
this.#planReferenceSent = false;
|
|
8620
|
-
this.#
|
|
8972
|
+
this.#resetAllAdvisorRuntimes();
|
|
8621
8973
|
this.#syncTodoPhasesFromBranch();
|
|
8622
8974
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8623
8975
|
|
|
@@ -8881,7 +9233,7 @@ export class AgentSession {
|
|
|
8881
9233
|
// Rebuild agent messages from session
|
|
8882
9234
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8883
9235
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8884
|
-
this.#
|
|
9236
|
+
this.#resetAllAdvisorRuntimes();
|
|
8885
9237
|
this.#syncTodoPhasesFromBranch();
|
|
8886
9238
|
|
|
8887
9239
|
return { document: handoffText, savedPath };
|
|
@@ -10201,6 +10553,17 @@ export class AgentSession {
|
|
|
10201
10553
|
return this.#resolveCompactionModelCandidates(this.model, availableModels, filter);
|
|
10202
10554
|
}
|
|
10203
10555
|
|
|
10556
|
+
/**
|
|
10557
|
+
* Compaction candidates that can actually run — those with a resolvable API
|
|
10558
|
+
* key, matching the per-candidate getApiKey gate the execution loop applies.
|
|
10559
|
+
* Re-expansion reusability (prepareCompaction) must judge remote-preserve
|
|
10560
|
+
* reuse against these, not against candidates the loop would skip at runtime.
|
|
10561
|
+
*/
|
|
10562
|
+
async #runnableCompactionCandidates(candidates: readonly Model[], sessionId: string | undefined): Promise<Model[]> {
|
|
10563
|
+
const keys = await Promise.all(candidates.map(model => this.#modelRegistry.getApiKey(model, sessionId)));
|
|
10564
|
+
return candidates.filter((_, index) => keys[index] !== undefined);
|
|
10565
|
+
}
|
|
10566
|
+
|
|
10204
10567
|
#resolveCompactionModelCandidates(
|
|
10205
10568
|
preferredModel: Model | null | undefined,
|
|
10206
10569
|
availableModels: Model[],
|
|
@@ -10297,6 +10660,9 @@ export class AgentSession {
|
|
|
10297
10660
|
// via resolveCompactionEffort so unsupported-effort models
|
|
10298
10661
|
// (xai-oauth/grok-build) don't trip requireSupportedEffort.
|
|
10299
10662
|
thinkingLevel: this.thinkingLevel,
|
|
10663
|
+
tools: this.agent.state.tools,
|
|
10664
|
+
sessionId: this.sessionId,
|
|
10665
|
+
promptCacheKey: this.sessionId,
|
|
10300
10666
|
},
|
|
10301
10667
|
);
|
|
10302
10668
|
} catch (error) {
|
|
@@ -10616,15 +10982,21 @@ export class AgentSession {
|
|
|
10616
10982
|
|
|
10617
10983
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
10618
10984
|
// LLM call would hit the same overflow. "incomplete" is an output-side problem,
|
|
10619
|
-
// so a handoff request on the existing context is still viable.
|
|
10620
|
-
// a local-only strategy: if it cannot run, report the local blocker instead of
|
|
10621
|
-
// silently swapping in a provider-backed summary.
|
|
10985
|
+
// so a handoff request on the existing context is still viable.
|
|
10622
10986
|
let action: "context-full" | "handoff" | "snapcompact" =
|
|
10623
10987
|
compactionSettings.strategy === "snapcompact"
|
|
10624
10988
|
? "snapcompact"
|
|
10625
10989
|
: compactionSettings.strategy === "handoff" && reason !== "overflow" && !suppressHandoff
|
|
10626
10990
|
? "handoff"
|
|
10627
10991
|
: "context-full";
|
|
10992
|
+
if (action === "snapcompact" && this.model && !this.model.input.includes("image")) {
|
|
10993
|
+
this.emitNotice(
|
|
10994
|
+
"warning",
|
|
10995
|
+
`snapcompact needs a vision-capable active model (${this.model.id} is text-only); using context-full auto-compaction instead.`,
|
|
10996
|
+
"compaction",
|
|
10997
|
+
);
|
|
10998
|
+
action = "context-full";
|
|
10999
|
+
}
|
|
10628
11000
|
// Abort any older auto-compaction before installing this run's controller.
|
|
10629
11001
|
this.#autoCompactionAbortController?.abort();
|
|
10630
11002
|
const autoCompactionAbortController = new AbortController();
|
|
@@ -10703,7 +11075,11 @@ export class AgentSession {
|
|
|
10703
11075
|
|
|
10704
11076
|
const pathEntries = this.sessionManager.getBranch();
|
|
10705
11077
|
|
|
10706
|
-
const
|
|
11078
|
+
const autoCompactionCandidates = await this.#runnableCompactionCandidates(
|
|
11079
|
+
this.#getCompactionModelCandidates(availableModels),
|
|
11080
|
+
this.sessionId,
|
|
11081
|
+
);
|
|
11082
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, autoCompactionCandidates);
|
|
10707
11083
|
if (!preparation) {
|
|
10708
11084
|
await this.#emitSessionEvent({
|
|
10709
11085
|
type: "auto_compaction_end",
|
|
@@ -10766,21 +11142,15 @@ export class AgentSession {
|
|
|
10766
11142
|
// + a summary message carrying the imaged archive at FRAME_TOKEN_ESTIMATE
|
|
10767
11143
|
// per frame; #computeSnapcompactMaxFrames sizes the frame cap from the
|
|
10768
11144
|
// live window so we don't run snapcompact just to overflow every threshold
|
|
10769
|
-
// tick. Any local blocker
|
|
10770
|
-
//
|
|
11145
|
+
// tick. Any local blocker (non-ASCII transcript, kept-history too large,
|
|
11146
|
+
// post-render overflow) downgrades auto maintenance to a context-full LLM
|
|
11147
|
+
// summary instead of wedging the session (#3659) — auto runs the default
|
|
11148
|
+
// strategy on the user's behalf, so a fallback that lets the session keep
|
|
11149
|
+
// running is the right behavior. Manual `/compact snapcompact` keeps the
|
|
11150
|
+
// local-only contract (#3599): the user explicitly picked it.
|
|
10771
11151
|
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
|
11152
|
+
let snapcompactBlocker: string | undefined;
|
|
10772
11153
|
if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
|
|
10773
|
-
if (!this.model?.input.includes("image")) {
|
|
10774
|
-
logger.warn("Snapcompact compaction requires a vision-capable model", {
|
|
10775
|
-
model: this.model?.id,
|
|
10776
|
-
});
|
|
10777
|
-
this.emitNotice(
|
|
10778
|
-
"warning",
|
|
10779
|
-
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only). No LLM fallback was attempted.`,
|
|
10780
|
-
"compaction",
|
|
10781
|
-
);
|
|
10782
|
-
throw new Error(`snapcompact cannot run locally: ${this.model?.id ?? "unknown"} is text-only.`);
|
|
10783
|
-
}
|
|
10784
11154
|
const text = snapcompact.serializeConversation(
|
|
10785
11155
|
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
|
10786
11156
|
);
|
|
@@ -10791,55 +11161,46 @@ export class AgentSession {
|
|
|
10791
11161
|
model: this.model?.id,
|
|
10792
11162
|
unrenderableRatio: renderScan.unrenderableRatio,
|
|
10793
11163
|
});
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
throw new Error(
|
|
10800
|
-
`snapcompact cannot render this conversation locally: high non-ASCII rate detected (${percent}%).`,
|
|
10801
|
-
);
|
|
10802
|
-
}
|
|
10803
|
-
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
|
|
10804
|
-
if (maxFrames < 1) {
|
|
10805
|
-
logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
|
|
10806
|
-
model: this.model?.id,
|
|
10807
|
-
});
|
|
10808
|
-
this.emitNotice(
|
|
10809
|
-
"warning",
|
|
10810
|
-
"snapcompact: kept history alone exceeds the context budget. No LLM fallback was attempted.",
|
|
10811
|
-
"compaction",
|
|
10812
|
-
);
|
|
10813
|
-
throw new Error("snapcompact cannot run locally: kept history alone exceeds the context budget.");
|
|
10814
|
-
}
|
|
10815
|
-
snapcompactResult = await snapcompact.compact(preparation, {
|
|
10816
|
-
convertToLlm,
|
|
10817
|
-
model: this.model,
|
|
10818
|
-
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
10819
|
-
maxFrames,
|
|
10820
|
-
});
|
|
10821
|
-
|
|
10822
|
-
if (snapcompactResult) {
|
|
10823
|
-
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
10824
|
-
const budget =
|
|
10825
|
-
ctxWindow > 0
|
|
10826
|
-
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
|
10827
|
-
: Number.POSITIVE_INFINITY;
|
|
10828
|
-
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
|
10829
|
-
if (projected > budget) {
|
|
10830
|
-
logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
|
|
11164
|
+
snapcompactBlocker = `snapcompact disabled: high non-ASCII rate detected (${percent}%); using context-full auto-compaction instead.`;
|
|
11165
|
+
} else {
|
|
11166
|
+
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
|
|
11167
|
+
if (maxFrames < 1) {
|
|
11168
|
+
logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
|
|
10831
11169
|
model: this.model?.id,
|
|
10832
|
-
projected,
|
|
10833
|
-
budget,
|
|
10834
11170
|
});
|
|
10835
|
-
|
|
10836
|
-
"
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
11171
|
+
snapcompactBlocker =
|
|
11172
|
+
"snapcompact: kept history alone exceeds the context budget; using context-full auto-compaction instead.";
|
|
11173
|
+
} else {
|
|
11174
|
+
snapcompactResult = await snapcompact.compact(preparation, {
|
|
11175
|
+
convertToLlm,
|
|
11176
|
+
model: this.model,
|
|
11177
|
+
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
11178
|
+
maxFrames,
|
|
11179
|
+
});
|
|
11180
|
+
if (snapcompactResult) {
|
|
11181
|
+
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
11182
|
+
const budget =
|
|
11183
|
+
ctxWindow > 0
|
|
11184
|
+
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
|
11185
|
+
: Number.POSITIVE_INFINITY;
|
|
11186
|
+
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
|
11187
|
+
if (projected > budget) {
|
|
11188
|
+
logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
|
|
11189
|
+
model: this.model?.id,
|
|
11190
|
+
projected,
|
|
11191
|
+
budget,
|
|
11192
|
+
});
|
|
11193
|
+
snapcompactBlocker =
|
|
11194
|
+
"snapcompact could not bring the context under the limit; using context-full auto-compaction instead.";
|
|
11195
|
+
snapcompactResult = undefined;
|
|
11196
|
+
}
|
|
11197
|
+
}
|
|
10841
11198
|
}
|
|
10842
11199
|
}
|
|
11200
|
+
if (snapcompactBlocker) {
|
|
11201
|
+
this.emitNotice("warning", snapcompactBlocker, "compaction");
|
|
11202
|
+
action = "context-full";
|
|
11203
|
+
}
|
|
10843
11204
|
}
|
|
10844
11205
|
|
|
10845
11206
|
if (compactionPrep.kind === "fromHook") {
|
|
@@ -10891,6 +11252,9 @@ export class AgentSession {
|
|
|
10891
11252
|
// site. Clamped per-model inside compact() via
|
|
10892
11253
|
// resolveCompactionEffort.
|
|
10893
11254
|
thinkingLevel: this.thinkingLevel,
|
|
11255
|
+
tools: this.agent.state.tools,
|
|
11256
|
+
sessionId: this.sessionId,
|
|
11257
|
+
promptCacheKey: this.sessionId,
|
|
10894
11258
|
},
|
|
10895
11259
|
);
|
|
10896
11260
|
break;
|
|
@@ -11007,7 +11371,7 @@ export class AgentSession {
|
|
|
11007
11371
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
11008
11372
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
11009
11373
|
this.#planReferenceSent = false;
|
|
11010
|
-
this.#
|
|
11374
|
+
this.#resetAllAdvisorRuntimes();
|
|
11011
11375
|
this.#syncTodoPhasesFromBranch();
|
|
11012
11376
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
11013
11377
|
|
|
@@ -12853,7 +13217,7 @@ export class AgentSession {
|
|
|
12853
13217
|
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
12854
13218
|
this.agent.serviceTier = previousServiceTier;
|
|
12855
13219
|
this.#syncTodoPhasesFromBranch();
|
|
12856
|
-
this.#
|
|
13220
|
+
this.#resetAllAdvisorRuntimes();
|
|
12857
13221
|
this.#reconnectToAgent();
|
|
12858
13222
|
if (restoreMcpError) {
|
|
12859
13223
|
throw restoreMcpError;
|
|
@@ -13255,10 +13619,11 @@ export class AgentSession {
|
|
|
13255
13619
|
let totalInput = 0;
|
|
13256
13620
|
let totalOutput = 0;
|
|
13257
13621
|
let totalCacheRead = 0;
|
|
13622
|
+
let totalReasoning = 0;
|
|
13258
13623
|
let totalCacheWrite = 0;
|
|
13259
13624
|
let totalCost = 0;
|
|
13260
|
-
|
|
13261
13625
|
let totalPremiumRequests = 0;
|
|
13626
|
+
|
|
13262
13627
|
const getTaskToolUsage = (details: unknown): Usage | undefined => {
|
|
13263
13628
|
if (!details || typeof details !== "object") return undefined;
|
|
13264
13629
|
const record = details as Record<string, unknown>;
|
|
@@ -13273,6 +13638,7 @@ export class AgentSession {
|
|
|
13273
13638
|
toolCalls += assistantMsg.content.filter(c => c.type === "toolCall").length;
|
|
13274
13639
|
totalInput += assistantMsg.usage.input;
|
|
13275
13640
|
totalOutput += assistantMsg.usage.output;
|
|
13641
|
+
totalReasoning += assistantMsg.usage.reasoningTokens ?? 0;
|
|
13276
13642
|
totalCacheRead += assistantMsg.usage.cacheRead;
|
|
13277
13643
|
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
|
13278
13644
|
totalPremiumRequests += assistantMsg.usage.premiumRequests ?? 0;
|
|
@@ -13284,6 +13650,7 @@ export class AgentSession {
|
|
|
13284
13650
|
if (usage) {
|
|
13285
13651
|
totalInput += usage.input;
|
|
13286
13652
|
totalOutput += usage.output;
|
|
13653
|
+
totalReasoning += usage.reasoningTokens ?? 0;
|
|
13287
13654
|
totalCacheRead += usage.cacheRead;
|
|
13288
13655
|
totalCacheWrite += usage.cacheWrite;
|
|
13289
13656
|
totalPremiumRequests += usage.premiumRequests ?? 0;
|
|
@@ -13303,6 +13670,7 @@ export class AgentSession {
|
|
|
13303
13670
|
tokens: {
|
|
13304
13671
|
input: totalInput,
|
|
13305
13672
|
output: totalOutput,
|
|
13673
|
+
reasoning: totalReasoning,
|
|
13306
13674
|
cacheRead: totalCacheRead,
|
|
13307
13675
|
cacheWrite: totalCacheWrite,
|
|
13308
13676
|
total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
|
|
@@ -13835,6 +14203,23 @@ export class AgentSession {
|
|
|
13835
14203
|
return this.setAdvisorEnabled(!this.#advisorEnabled);
|
|
13836
14204
|
}
|
|
13837
14205
|
|
|
14206
|
+
/**
|
|
14207
|
+
* Replace the live advisor roster from an edited `WATCHDOG.yml` (the `/advisor
|
|
14208
|
+
* configure` save path). Swaps the configs + shared baseline, then rebuilds the
|
|
14209
|
+
* runtimes in place so the change applies without a restart. When the advisor is
|
|
14210
|
+
* disabled the new configs are simply stored for the next enable.
|
|
14211
|
+
*
|
|
14212
|
+
* @returns the number of advisors active after the rebuild.
|
|
14213
|
+
*/
|
|
14214
|
+
applyAdvisorConfigs(advisors: AdvisorConfig[], sharedInstructions: string | undefined): number {
|
|
14215
|
+
this.#advisorConfigs = advisors;
|
|
14216
|
+
this.#advisorSharedInstructions = sharedInstructions;
|
|
14217
|
+
if (!this.#advisorEnabled) return 0;
|
|
14218
|
+
this.#stopAdvisorRuntime();
|
|
14219
|
+
this.#buildAdvisorRuntime(true);
|
|
14220
|
+
return this.#advisors.length;
|
|
14221
|
+
}
|
|
14222
|
+
|
|
13838
14223
|
/**
|
|
13839
14224
|
* Whether the advisor setting is enabled for this session.
|
|
13840
14225
|
*/
|
|
@@ -13849,7 +14234,17 @@ export class AgentSession {
|
|
|
13849
14234
|
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
|
13850
14235
|
*/
|
|
13851
14236
|
isAdvisorActive(): boolean {
|
|
13852
|
-
return this.#
|
|
14237
|
+
return this.#advisors.length > 0;
|
|
14238
|
+
}
|
|
14239
|
+
|
|
14240
|
+
/**
|
|
14241
|
+
* The names of the tools available to advisors this session (the pool a
|
|
14242
|
+
* `/advisor configure` editor lists). The advisor is a full agent, so this is the
|
|
14243
|
+
* full built tool set; a tool whose optional factory returns null (e.g. lsp with
|
|
14244
|
+
* no servers) is absent.
|
|
14245
|
+
*/
|
|
14246
|
+
getAdvisorAvailableToolNames(): string[] {
|
|
14247
|
+
return (this.#advisorTools ?? []).map(tool => tool.name);
|
|
13853
14248
|
}
|
|
13854
14249
|
|
|
13855
14250
|
/**
|
|
@@ -13860,7 +14255,7 @@ export class AgentSession {
|
|
|
13860
14255
|
* (`streamFn`, `promptCacheKey`, `providerSessionState`, ...).
|
|
13861
14256
|
*/
|
|
13862
14257
|
getAdvisorAgent(): Agent | undefined {
|
|
13863
|
-
return this.#
|
|
14258
|
+
return this.#advisors[0]?.agent;
|
|
13864
14259
|
}
|
|
13865
14260
|
|
|
13866
14261
|
/**
|
|
@@ -13868,23 +14263,59 @@ export class AgentSession {
|
|
|
13868
14263
|
*/
|
|
13869
14264
|
getAdvisorStats(): AdvisorStats {
|
|
13870
14265
|
const configured = this.#advisorEnabled;
|
|
13871
|
-
const
|
|
13872
|
-
if (
|
|
14266
|
+
const advisors = this.#advisors.map(a => this.#computeAdvisorStat(a));
|
|
14267
|
+
if (advisors.length === 0) {
|
|
13873
14268
|
return {
|
|
13874
14269
|
configured,
|
|
13875
14270
|
active: false,
|
|
13876
14271
|
contextWindow: 0,
|
|
13877
14272
|
contextTokens: 0,
|
|
13878
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14273
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
13879
14274
|
cost: 0,
|
|
13880
14275
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
14276
|
+
advisors: [],
|
|
13881
14277
|
};
|
|
13882
14278
|
}
|
|
13883
|
-
const
|
|
13884
|
-
const messages =
|
|
14279
|
+
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
14280
|
+
const messages = { user: 0, assistant: 0, total: 0 };
|
|
14281
|
+
let cost = 0;
|
|
14282
|
+
let contextTokens = 0;
|
|
14283
|
+
for (const a of advisors) {
|
|
14284
|
+
tokens.input += a.tokens.input;
|
|
14285
|
+
tokens.output += a.tokens.output;
|
|
14286
|
+
tokens.reasoning += a.tokens.reasoning;
|
|
14287
|
+
tokens.cacheRead += a.tokens.cacheRead;
|
|
14288
|
+
tokens.cacheWrite += a.tokens.cacheWrite;
|
|
14289
|
+
tokens.total += a.tokens.total;
|
|
14290
|
+
messages.user += a.messages.user;
|
|
14291
|
+
messages.assistant += a.messages.assistant;
|
|
14292
|
+
messages.total += a.messages.total;
|
|
14293
|
+
cost += a.cost;
|
|
14294
|
+
contextTokens += a.contextTokens;
|
|
14295
|
+
}
|
|
14296
|
+
// Single-advisor displays read the top-level model/window directly; surface the
|
|
14297
|
+
// first advisor's so the legacy status line stays byte-identical.
|
|
14298
|
+
return {
|
|
14299
|
+
configured,
|
|
14300
|
+
active: true,
|
|
14301
|
+
model: advisors[0].model,
|
|
14302
|
+
contextWindow: advisors[0].contextWindow,
|
|
14303
|
+
contextTokens,
|
|
14304
|
+
tokens,
|
|
14305
|
+
cost,
|
|
14306
|
+
messages,
|
|
14307
|
+
advisors,
|
|
14308
|
+
};
|
|
14309
|
+
}
|
|
14310
|
+
|
|
14311
|
+
/** Compute one advisor's stats slice (tokens, cost, context, message counts). */
|
|
14312
|
+
#computeAdvisorStat(advisor: ActiveAdvisor): PerAdvisorStat {
|
|
14313
|
+
const model = advisor.agent.state.model;
|
|
14314
|
+
const messages = advisor.agent.state.messages;
|
|
13885
14315
|
const contextTokens = this.#estimateAdvisorContextTokens(messages);
|
|
13886
14316
|
let input = 0;
|
|
13887
14317
|
let output = 0;
|
|
14318
|
+
let reasoning = 0;
|
|
13888
14319
|
let cacheRead = 0;
|
|
13889
14320
|
let cacheWrite = 0;
|
|
13890
14321
|
let cost = 0;
|
|
@@ -13897,24 +14328,18 @@ export class AgentSession {
|
|
|
13897
14328
|
const assistantMsg = message as AssistantMessage;
|
|
13898
14329
|
input += assistantMsg.usage.input;
|
|
13899
14330
|
output += assistantMsg.usage.output;
|
|
14331
|
+
reasoning += assistantMsg.usage.reasoningTokens ?? 0;
|
|
13900
14332
|
cacheRead += assistantMsg.usage.cacheRead;
|
|
13901
14333
|
cacheWrite += assistantMsg.usage.cacheWrite;
|
|
13902
14334
|
cost += assistantMsg.usage.cost.total;
|
|
13903
14335
|
}
|
|
13904
14336
|
}
|
|
13905
14337
|
return {
|
|
13906
|
-
|
|
13907
|
-
active: true,
|
|
14338
|
+
name: advisor.name,
|
|
13908
14339
|
model,
|
|
13909
14340
|
contextWindow: model.contextWindow ?? 0,
|
|
13910
14341
|
contextTokens,
|
|
13911
|
-
tokens: {
|
|
13912
|
-
input,
|
|
13913
|
-
output,
|
|
13914
|
-
cacheRead,
|
|
13915
|
-
cacheWrite,
|
|
13916
|
-
total: input + output + cacheRead + cacheWrite,
|
|
13917
|
-
},
|
|
14342
|
+
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: input + output + cacheRead + cacheWrite },
|
|
13918
14343
|
cost,
|
|
13919
14344
|
messages: { user, assistant, total: messages.length },
|
|
13920
14345
|
};
|
|
@@ -13930,19 +14355,30 @@ export class AgentSession {
|
|
|
13930
14355
|
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
13931
14356
|
: "Advisor is disabled.";
|
|
13932
14357
|
}
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
`${
|
|
13940
|
-
`${
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
14358
|
+
if (stats.advisors.length <= 1) {
|
|
14359
|
+
const s = stats.advisors[0];
|
|
14360
|
+
const contextLine =
|
|
14361
|
+
s.contextWindow > 0
|
|
14362
|
+
? `Context: ${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} tokens (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
14363
|
+
: `Context: ${s.contextTokens.toLocaleString()} tokens`;
|
|
14364
|
+
const spendParts = [`${s.tokens.input.toLocaleString()} input`, `${s.tokens.output.toLocaleString()} output`];
|
|
14365
|
+
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
14366
|
+
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
14367
|
+
const spendLine = `Spend: ${spendParts.join(", ")}, $${s.cost.toFixed(4)}`;
|
|
14368
|
+
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
14369
|
+
}
|
|
14370
|
+
const lines = [`Advisors enabled (${stats.advisors.length}):`];
|
|
14371
|
+
for (const s of stats.advisors) {
|
|
14372
|
+
const ctx =
|
|
14373
|
+
s.contextWindow > 0
|
|
14374
|
+
? `${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
14375
|
+
: `${s.contextTokens.toLocaleString()}`;
|
|
14376
|
+
lines.push(` • ${s.name} (${s.model.provider}/${s.model.id}) — context ${ctx} tokens, $${s.cost.toFixed(4)}`);
|
|
14377
|
+
}
|
|
14378
|
+
lines.push(
|
|
14379
|
+
`Totals: ${stats.tokens.input.toLocaleString()} input, ${stats.tokens.output.toLocaleString()} output, $${stats.cost.toFixed(4)}.`,
|
|
14380
|
+
);
|
|
14381
|
+
return lines.join("\n");
|
|
13946
14382
|
}
|
|
13947
14383
|
|
|
13948
14384
|
/**
|
|
@@ -13986,18 +14422,21 @@ export class AgentSession {
|
|
|
13986
14422
|
* {@link formatSessionAsText}. Returns null when no advisor is active.
|
|
13987
14423
|
*/
|
|
13988
14424
|
formatAdvisorHistoryAsText(options?: { compact?: boolean }): string | null {
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14425
|
+
if (this.#advisors.length === 0) return null;
|
|
14426
|
+
const dump = (a: ActiveAdvisor): string =>
|
|
14427
|
+
options?.compact
|
|
14428
|
+
? formatSessionHistoryMarkdown(a.agent.state.messages)
|
|
14429
|
+
: formatSessionDumpText({
|
|
14430
|
+
messages: a.agent.state.messages,
|
|
14431
|
+
systemPrompt: a.agent.state.systemPrompt,
|
|
14432
|
+
model: a.agent.state.model,
|
|
14433
|
+
thinkingLevel: a.agent.state.thinkingLevel,
|
|
14434
|
+
tools: a.agent.state.tools,
|
|
14435
|
+
});
|
|
14436
|
+
if (this.#advisors.length === 1) return dump(this.#advisors[0]);
|
|
14437
|
+
return this.#advisors
|
|
14438
|
+
.map(a => `### Advisor: ${a.name} (${a.agent.state.model.provider}/${a.agent.state.model.id})\n\n${dump(a)}`)
|
|
14439
|
+
.join("\n\n");
|
|
14001
14440
|
}
|
|
14002
14441
|
|
|
14003
14442
|
// =========================================================================
|