@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.4
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 +60 -0
- package/dist/cli.js +3682 -3615
- 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/cli/update-cli.d.ts +15 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/collab/replication-shrink.d.ts +39 -0
- package/dist/types/config/provider-globals.d.ts +7 -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/memories/index.d.ts +20 -1
- 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 +35 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/command-controller.d.ts +3 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +11 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +9 -3
- 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 +8 -7
- 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/autolearn/controller.ts +13 -22
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/cli/update-cli.ts +254 -0
- package/src/collab/guest.ts +43 -7
- package/src/collab/host.ts +13 -8
- package/src/collab/replication-shrink.ts +111 -0
- package/src/config/model-registry.ts +80 -18
- package/src/config/provider-globals.ts +25 -0
- 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/__tests__/julia-prelude.test.ts +2 -2
- 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 +130 -15
- package/src/memory-backend/local-backend.ts +5 -3
- 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 +150 -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 +27 -36
- package/src/modes/controllers/event-controller.ts +113 -1
- 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 +74 -25
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +9 -3
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/goals/goal-todo-context.md +12 -0
- 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 +899 -429
- 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 +115 -22
- 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 +23 -24
- 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
- package/src/prompts/system/autolearn-nudge.md +0 -5
|
@@ -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";
|
|
@@ -227,11 +231,13 @@ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
|
|
|
227
231
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
228
232
|
import type { PlanModeState } from "../plan-mode/state";
|
|
229
233
|
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
234
|
+
import goalTodoContextPrompt from "../prompts/goals/goal-todo-context.md" with { type: "text" };
|
|
230
235
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
231
236
|
import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
|
|
232
237
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
233
238
|
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
234
239
|
import geminiToolReminderTemplate from "../prompts/system/gemini-tool-call-reminder.md" with { type: "text" };
|
|
240
|
+
import interruptedThinkingTemplate from "../prompts/system/interrupted-thinking.md" with { type: "text" };
|
|
235
241
|
import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
|
|
236
242
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
237
243
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
@@ -261,6 +267,7 @@ import {
|
|
|
261
267
|
shouldDisableReasoning,
|
|
262
268
|
toReasoningEffort,
|
|
263
269
|
} from "../thinking";
|
|
270
|
+
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/text";
|
|
264
271
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
265
272
|
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
266
273
|
import {
|
|
@@ -288,6 +295,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
288
295
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
289
296
|
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
290
297
|
import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallback";
|
|
298
|
+
import { generateSessionTitle } from "../utils/title-generator";
|
|
291
299
|
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
|
292
300
|
import type { AuthStorage } from "./auth-storage";
|
|
293
301
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
@@ -303,6 +311,10 @@ import {
|
|
|
303
311
|
type BashExecutionMessage,
|
|
304
312
|
type CustomMessage,
|
|
305
313
|
convertToLlm,
|
|
314
|
+
demoteInterruptedThinking,
|
|
315
|
+
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
316
|
+
type InterruptedThinkingDetails,
|
|
317
|
+
isUserInterruptAbort,
|
|
306
318
|
type PythonExecutionMessage,
|
|
307
319
|
readQueueChipText,
|
|
308
320
|
SILENT_ABORT_MARKER,
|
|
@@ -599,20 +611,29 @@ export interface AgentSessionConfig {
|
|
|
599
611
|
*/
|
|
600
612
|
providerSessionId?: string;
|
|
601
613
|
/**
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* advisor's
|
|
605
|
-
*
|
|
614
|
+
* Full advisor toolset, pre-built in `createAgentSession` against a distinct,
|
|
615
|
+
* advisor-scoped `ToolSession` (its own `-advisor` session/agent id) so the
|
|
616
|
+
* advisor's tool state stays isolated from the primary. The advisor is a full
|
|
617
|
+
* agent; its config `tools` selects a subset (default read/grep/glob). Undefined
|
|
618
|
+
* when the advisor is disabled.
|
|
606
619
|
*/
|
|
607
|
-
|
|
620
|
+
advisorTools?: AgentTool[];
|
|
608
621
|
/** Preloaded watchdog prompt content for the advisor. */
|
|
609
622
|
advisorWatchdogPrompt?: string;
|
|
623
|
+
/** Preloaded YAML top-level `instructions` shared baseline, kept separate from
|
|
624
|
+
* `advisorWatchdogPrompt` so `/advisor configure` can swap it live. */
|
|
625
|
+
advisorSharedInstructions?: string;
|
|
610
626
|
/**
|
|
611
627
|
* Preloaded project context files (AGENTS.md, etc.) rendered as a system-prompt
|
|
612
628
|
* block for the advisor — the same standing instructions the primary agent
|
|
613
629
|
* receives, so the reviewer holds the agent to them.
|
|
614
630
|
*/
|
|
615
631
|
advisorContextPrompt?: string;
|
|
632
|
+
/**
|
|
633
|
+
* Advisors discovered from `WATCHDOG.yml`. Empty/undefined runs a single
|
|
634
|
+
* legacy advisor on the `advisor` role (byte-for-byte the pre-config path).
|
|
635
|
+
*/
|
|
636
|
+
advisorConfigs?: AdvisorConfig[];
|
|
616
637
|
/**
|
|
617
638
|
* Strip tool descriptions from provider-bound tool specs on side requests
|
|
618
639
|
* (handoff). Must match the session-start value used to build the system
|
|
@@ -717,6 +738,7 @@ export interface SessionStats {
|
|
|
717
738
|
tokens: {
|
|
718
739
|
input: number;
|
|
719
740
|
output: number;
|
|
741
|
+
reasoning: number;
|
|
720
742
|
cacheRead: number;
|
|
721
743
|
cacheWrite: number;
|
|
722
744
|
total: number;
|
|
@@ -735,6 +757,7 @@ export interface AdvisorStats {
|
|
|
735
757
|
tokens: {
|
|
736
758
|
input: number;
|
|
737
759
|
output: number;
|
|
760
|
+
reasoning: number;
|
|
738
761
|
cacheRead: number;
|
|
739
762
|
cacheWrite: number;
|
|
740
763
|
total: number;
|
|
@@ -745,6 +768,43 @@ export interface AdvisorStats {
|
|
|
745
768
|
assistant: number;
|
|
746
769
|
total: number;
|
|
747
770
|
};
|
|
771
|
+
/** Per-advisor breakdown; one entry per active advisor (single-advisor sessions have one). */
|
|
772
|
+
advisors: PerAdvisorStat[];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** One advisor's slice of {@link AdvisorStats}, surfaced for the multi-advisor status panel. */
|
|
776
|
+
export interface PerAdvisorStat {
|
|
777
|
+
name: string;
|
|
778
|
+
model: Model;
|
|
779
|
+
contextWindow: number;
|
|
780
|
+
contextTokens: number;
|
|
781
|
+
tokens: AdvisorStats["tokens"];
|
|
782
|
+
cost: number;
|
|
783
|
+
messages: AdvisorStats["messages"];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* One live advisor instance: its own agent/runtime/tools/recorder plus a
|
|
788
|
+
* per-advisor emission guard and identity. The session holds an array of these;
|
|
789
|
+
* primary-scoped state (turn counters, interrupt latches, the shared yield
|
|
790
|
+
* channel) stays on the session.
|
|
791
|
+
*/
|
|
792
|
+
interface ActiveAdvisor {
|
|
793
|
+
/** Display name from config ("default" for the legacy no-YAML advisor). */
|
|
794
|
+
name: string;
|
|
795
|
+
/** Slug for the transcript filename/session id; "" → `__advisor.jsonl`. */
|
|
796
|
+
slug: string;
|
|
797
|
+
agent: Agent;
|
|
798
|
+
runtime: AdvisorRuntime;
|
|
799
|
+
adviseTool: AdviseTool;
|
|
800
|
+
emissionGuard: AdvisorEmissionGuard;
|
|
801
|
+
recorder: AdvisorTranscriptRecorder;
|
|
802
|
+
/** Latest recorder close, awaited by dispose() so the final turn lands on disk. */
|
|
803
|
+
recorderClosed: Promise<void>;
|
|
804
|
+
/** Unsubscribe for the advisor agent's event stream feeding the recorder. */
|
|
805
|
+
agentUnsubscribe?: () => void;
|
|
806
|
+
model: Model;
|
|
807
|
+
thinkingLevel: ThinkingLevel;
|
|
748
808
|
}
|
|
749
809
|
|
|
750
810
|
export interface FreshSessionResult {
|
|
@@ -1189,6 +1249,60 @@ type MessageEndPersistenceSlot = {
|
|
|
1189
1249
|
release: () => void;
|
|
1190
1250
|
};
|
|
1191
1251
|
|
|
1252
|
+
const REPLAN_TITLE_CONTEXT_TURN_LIMIT = 6;
|
|
1253
|
+
|
|
1254
|
+
type SessionTitleSource = "auto" | "user";
|
|
1255
|
+
type SessionNameTrigger = "replan";
|
|
1256
|
+
type SetSessionNameWithTrigger = (
|
|
1257
|
+
name: string,
|
|
1258
|
+
source?: SessionTitleSource,
|
|
1259
|
+
trigger?: SessionNameTrigger,
|
|
1260
|
+
) => Promise<boolean>;
|
|
1261
|
+
|
|
1262
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1263
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function textFromContent(content: unknown): string {
|
|
1267
|
+
if (typeof content === "string") return content.trim();
|
|
1268
|
+
if (!Array.isArray(content)) return "";
|
|
1269
|
+
const parts: string[] = [];
|
|
1270
|
+
for (const block of content) {
|
|
1271
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") continue;
|
|
1272
|
+
const text = block.text.trim();
|
|
1273
|
+
if (text) parts.push(text);
|
|
1274
|
+
}
|
|
1275
|
+
return parts.join("\n\n");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function thinkingFromContent(content: unknown): string {
|
|
1279
|
+
if (!Array.isArray(content)) return "";
|
|
1280
|
+
const parts: string[] = [];
|
|
1281
|
+
for (const block of content) {
|
|
1282
|
+
if (!isRecord(block) || block.type !== "thinking" || typeof block.thinking !== "string") continue;
|
|
1283
|
+
const thinking = block.thinking.trim();
|
|
1284
|
+
if (thinking) parts.push(thinking);
|
|
1285
|
+
}
|
|
1286
|
+
return parts.join("\n\n");
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function toolCallOpFromMessage(message: AgentMessage, toolCallId: string): string | undefined {
|
|
1290
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return undefined;
|
|
1291
|
+
for (const block of message.content) {
|
|
1292
|
+
if (!isRecord(block) || block.type !== "toolCall" || block.id !== toolCallId) continue;
|
|
1293
|
+
return isRecord(block.arguments) ? getStringProperty(block.arguments, "op") : undefined;
|
|
1294
|
+
}
|
|
1295
|
+
return undefined;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function titleConversationTurnFromMessage(message: AgentMessage): TitleConversationTurn | undefined {
|
|
1299
|
+
if (message.role !== "user" && message.role !== "assistant") return undefined;
|
|
1300
|
+
const text = textFromContent(message.content);
|
|
1301
|
+
const thinking = message.role === "assistant" ? thinkingFromContent(message.content) : undefined;
|
|
1302
|
+
if (!text && !thinking) return undefined;
|
|
1303
|
+
return { role: message.role, ...(text ? { text } : {}), ...(thinking ? { thinking } : {}) };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1192
1306
|
export class AgentSession {
|
|
1193
1307
|
readonly agent: Agent;
|
|
1194
1308
|
readonly sessionManager: SessionManager;
|
|
@@ -1229,29 +1343,21 @@ export class AgentSession {
|
|
|
1229
1343
|
#advisorAutoResumeSuppressed = false;
|
|
1230
1344
|
#advisorPrimaryTurnsCompleted = 0;
|
|
1231
1345
|
#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
1346
|
#planModeState: PlanModeState | undefined;
|
|
1238
1347
|
#goalModeState: GoalModeState | undefined;
|
|
1239
1348
|
#goalRuntime: GoalRuntime;
|
|
1240
|
-
#advisorRuntime?: AdvisorRuntime;
|
|
1241
1349
|
#advisorEnabled = false;
|
|
1242
|
-
|
|
1243
|
-
#advisorAgent?: Agent;
|
|
1244
|
-
#advisorAdviseTool?: AdviseTool;
|
|
1245
|
-
#advisorReadOnlyTools?: AgentTool[];
|
|
1350
|
+
#advisorTools?: AgentTool[];
|
|
1246
1351
|
#advisorWatchdogPrompt?: string;
|
|
1352
|
+
#advisorSharedInstructions?: string;
|
|
1247
1353
|
#advisorContextPrompt?: string;
|
|
1248
1354
|
#advisorYieldQueueUnsubscribe?: () => void;
|
|
1249
|
-
/**
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1355
|
+
/** Live advisors. Empty when no advisor is active. */
|
|
1356
|
+
#advisors: ActiveAdvisor[] = [];
|
|
1357
|
+
/** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
|
|
1358
|
+
#advisorConfigs?: AdvisorConfig[];
|
|
1359
|
+
/** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
|
|
1360
|
+
* used as the open barrier for the next build so two writers never share a file. */
|
|
1255
1361
|
#advisorRecorderClosed: Promise<void> = Promise.resolve();
|
|
1256
1362
|
#goalTurnCounter = 0;
|
|
1257
1363
|
#planReferenceSent = false;
|
|
@@ -1290,6 +1396,7 @@ export class AgentSession {
|
|
|
1290
1396
|
*/
|
|
1291
1397
|
#todoReminderAwaitingProgress = false;
|
|
1292
1398
|
#todoPhases: TodoPhase[] = [];
|
|
1399
|
+
#replanTitleRefreshInFlight: Promise<void> | undefined = undefined;
|
|
1293
1400
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
1294
1401
|
|
|
1295
1402
|
// Bash execution state
|
|
@@ -1669,9 +1776,11 @@ export class AgentSession {
|
|
|
1669
1776
|
// toggle scopes priority to Fireworks alone, without mutating the shared
|
|
1670
1777
|
// session `serviceTier` that drives `/fast` and OpenAI/Anthropic priority.
|
|
1671
1778
|
this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
|
|
1672
|
-
this.#
|
|
1779
|
+
this.#advisorTools = config.advisorTools;
|
|
1673
1780
|
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1781
|
+
this.#advisorSharedInstructions = config.advisorSharedInstructions;
|
|
1674
1782
|
this.#advisorContextPrompt = config.advisorContextPrompt;
|
|
1783
|
+
this.#advisorConfigs = config.advisorConfigs;
|
|
1675
1784
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
1676
1785
|
this.#validateRetryFallbackChains();
|
|
1677
1786
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
@@ -1716,12 +1825,15 @@ export class AgentSession {
|
|
|
1716
1825
|
await this.#applyRewind(rewindReport, messages);
|
|
1717
1826
|
}
|
|
1718
1827
|
this.#advisorPrimaryTurnsCompleted++;
|
|
1719
|
-
if (this.#
|
|
1720
|
-
this.#
|
|
1828
|
+
if (this.#advisors.length > 0) {
|
|
1829
|
+
for (const a of this.#advisors) {
|
|
1830
|
+
if (!a.runtime.disposed) a.runtime.onTurnEnd(messages);
|
|
1831
|
+
}
|
|
1721
1832
|
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
1722
1833
|
if (syncBacklog !== "off") {
|
|
1723
1834
|
const threshold = parseInt(syncBacklog, 10);
|
|
1724
|
-
|
|
1835
|
+
// Parallel so the 30s catch-up budget is shared across advisors, not summed.
|
|
1836
|
+
await Promise.all(this.#advisors.map(a => a.runtime.waitForCatchup(30000, threshold, signal)));
|
|
1725
1837
|
}
|
|
1726
1838
|
}
|
|
1727
1839
|
await this.#maintainContextMidRun(messages, signal, context);
|
|
@@ -1891,12 +2003,14 @@ export class AgentSession {
|
|
|
1891
2003
|
// Mute the recorder across the re-prime: AdvisorRuntime.reset() aborts the advisor
|
|
1892
2004
|
// loop, and that abort can emit an `aborted` message_end we must not attribute to
|
|
1893
2005
|
// either session's transcript. Detach, reset, then re-attach the live agent's feed.
|
|
1894
|
-
this.#
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2006
|
+
for (const a of this.#advisors) {
|
|
2007
|
+
a.agentUnsubscribe?.();
|
|
2008
|
+
a.agentUnsubscribe = undefined;
|
|
2009
|
+
a.runtime.reset();
|
|
2010
|
+
a.adviseTool.resetDeliveredNotes();
|
|
2011
|
+
a.emissionGuard.reset();
|
|
2012
|
+
this.#attachAdvisorRecorderFeed(a);
|
|
2013
|
+
}
|
|
1900
2014
|
this.#advisorPrimaryTurnsCompleted = 0;
|
|
1901
2015
|
this.#advisorInterruptImmuneTurnStart = undefined;
|
|
1902
2016
|
this.#advisorAutoResumeSuppressed = false;
|
|
@@ -1909,261 +2023,298 @@ export class AgentSession {
|
|
|
1909
2023
|
|
|
1910
2024
|
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1911
2025
|
if (this.#isDisposed) return false;
|
|
1912
|
-
if (this.#
|
|
2026
|
+
if (this.#advisors.length > 0) return true;
|
|
1913
2027
|
if (!this.#advisorEnabled) return false;
|
|
1914
2028
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1915
2029
|
|
|
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;
|
|
2030
|
+
// The lone implicit "default" advisor (slug "") reproduces the legacy
|
|
2031
|
+
// single-advisor path: the `advisor` role model + `__advisor.jsonl`.
|
|
2032
|
+
const legacy = !this.#advisorConfigs?.length;
|
|
2033
|
+
const roster: AdvisorConfig[] = legacy ? [{ name: "default" }] : this.#advisorConfigs!;
|
|
2001
2034
|
|
|
2002
2035
|
// Advisor service tier (`serviceTierAdvisor`): "none" (default) runs the
|
|
2003
2036
|
// advisor on standard processing; "inherit" tracks the session's live tier
|
|
2004
2037
|
// per request (like the main agent, including /fast toggles) via a resolver;
|
|
2005
|
-
// a concrete value pins the advisor to that tier
|
|
2038
|
+
// a concrete value pins the advisor to that tier. One value for all advisors.
|
|
2006
2039
|
const advisorTierSetting = this.settings.get("serviceTierAdvisor");
|
|
2007
2040
|
const advisorServiceTier =
|
|
2008
2041
|
advisorTierSetting === "inherit" ? undefined : resolveServiceTierSetting(advisorTierSetting, undefined);
|
|
2009
2042
|
const advisorServiceTierResolver =
|
|
2010
2043
|
advisorTierSetting === "inherit" ? (model: Model) => this.#effectiveServiceTier(model) : undefined;
|
|
2011
2044
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2045
|
+
const usedSlugs = new Set<string>();
|
|
2046
|
+
for (const config of roster) {
|
|
2047
|
+
let slug = legacy ? "" : slugifyAdvisorName(config.name);
|
|
2048
|
+
if (slug) {
|
|
2049
|
+
let candidate = slug;
|
|
2050
|
+
let n = 2;
|
|
2051
|
+
while (usedSlugs.has(candidate)) candidate = `${slug}-${n++}`;
|
|
2052
|
+
slug = candidate;
|
|
2053
|
+
usedSlugs.add(slug);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Resolve the advisor's model: an explicit `model` override wins; else the
|
|
2057
|
+
// `advisor` role chain. A model that fails to resolve skips just this advisor.
|
|
2058
|
+
let model: Model | undefined;
|
|
2059
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
2060
|
+
if (config.model) {
|
|
2061
|
+
const resolved = resolveModelOverride([config.model], this.#modelRegistry, this.settings);
|
|
2062
|
+
model = resolved.model;
|
|
2063
|
+
thinkingLevel = resolved.thinkingLevel;
|
|
2064
|
+
if (!model) {
|
|
2065
|
+
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2066
|
+
continue;
|
|
2027
2067
|
}
|
|
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;
|
|
2068
|
+
} else {
|
|
2069
|
+
const sel = resolveAdvisorRoleSelection(
|
|
2070
|
+
this.settings,
|
|
2071
|
+
this.#modelRegistry.getAvailable(),
|
|
2072
|
+
this.#modelRegistry,
|
|
2073
|
+
);
|
|
2074
|
+
if (!sel) {
|
|
2075
|
+
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2076
|
+
advisor: config.name,
|
|
2077
|
+
});
|
|
2078
|
+
continue;
|
|
2088
2079
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2080
|
+
model = sel.model;
|
|
2081
|
+
thinkingLevel = sel.thinkingLevel;
|
|
2082
|
+
}
|
|
2083
|
+
const advisorModel = model;
|
|
2084
|
+
const advisorName = config.name;
|
|
2085
|
+
const advisorThinkingLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
2086
|
+
|
|
2087
|
+
const emissionGuard = new AdvisorEmissionGuard();
|
|
2088
|
+
const adviseTool = new AdviseTool((note, severity) => this.#routeAdvice(advisorRef, note, severity));
|
|
2089
|
+
|
|
2090
|
+
// `#advisorWatchdogPrompt` already carries WATCHDOG.md + YAML shared
|
|
2091
|
+
// instructions; `config.instructions` adds this advisor's specialization.
|
|
2092
|
+
const systemPrompt = [advisorSystemPrompt];
|
|
2093
|
+
if (this.#advisorContextPrompt) systemPrompt.push(this.#advisorContextPrompt);
|
|
2094
|
+
if (this.#advisorWatchdogPrompt) systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
2095
|
+
if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
|
|
2096
|
+
if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
|
|
2097
|
+
|
|
2098
|
+
const names = config.tools?.length ? new Set(config.tools) : ADVISOR_DEFAULT_TOOL_NAMES;
|
|
2099
|
+
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
2100
|
+
|
|
2101
|
+
const advisorSessionId = this.#advisorSessionId(slug);
|
|
2102
|
+
const appendOnlyContext = new AppendOnlyContextManager();
|
|
2103
|
+
|
|
2104
|
+
// Thread the primary's telemetry into the advisor loop so the advisor
|
|
2105
|
+
// model's GenAI spans + usage/cost hooks fire stamped with the advisor's
|
|
2106
|
+
// own identity. `conversationId` is cleared so the advisor loop falls back
|
|
2107
|
+
// to its own session id; undefined telemetry stays undefined.
|
|
2108
|
+
const advisorTelemetry = this.agent.telemetry
|
|
2109
|
+
? {
|
|
2110
|
+
...this.agent.telemetry,
|
|
2111
|
+
agent: {
|
|
2112
|
+
id: advisorSessionId,
|
|
2113
|
+
name: slug ? `${MODEL_ROLES.advisor.name}: ${advisorName}` : MODEL_ROLES.advisor.name,
|
|
2114
|
+
description: formatModelString(advisorModel),
|
|
2115
|
+
},
|
|
2116
|
+
conversationId: undefined,
|
|
2117
|
+
}
|
|
2118
|
+
: undefined;
|
|
2119
|
+
// Mirror the SDK's provider-shaping options (streamFn/onPayload/...,
|
|
2120
|
+
// providerSessionState, promptCacheKey, transformProviderContext) so each
|
|
2121
|
+
// advisor's requests cache, route, and obfuscate like the main turn.
|
|
2122
|
+
const advisorAgent = new Agent({
|
|
2123
|
+
initialState: {
|
|
2124
|
+
systemPrompt,
|
|
2125
|
+
model: advisorModel,
|
|
2126
|
+
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
2127
|
+
tools: [adviseTool, ...tools],
|
|
2128
|
+
},
|
|
2129
|
+
appendOnlyContext,
|
|
2130
|
+
sessionId: advisorSessionId,
|
|
2131
|
+
promptCacheKey: advisorSessionId,
|
|
2132
|
+
providerSessionState: this.#providerSessionState,
|
|
2133
|
+
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
2134
|
+
streamFn: this.#advisorStreamFn,
|
|
2135
|
+
onPayload: this.#onPayload,
|
|
2136
|
+
onResponse: this.#onResponse,
|
|
2137
|
+
onSseEvent: this.#onSseEvent,
|
|
2138
|
+
transformProviderContext: this.#transformProviderContext,
|
|
2139
|
+
intentTracing: false,
|
|
2140
|
+
telemetry: advisorTelemetry,
|
|
2141
|
+
serviceTier: advisorServiceTier,
|
|
2142
|
+
serviceTierResolver: advisorServiceTierResolver,
|
|
2143
|
+
});
|
|
2144
|
+
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2145
|
+
|
|
2146
|
+
const advisorAgentFacade: AdvisorAgent = {
|
|
2147
|
+
prompt: input => advisorAgent.prompt(input),
|
|
2148
|
+
abort: reason => advisorAgent.abort(reason),
|
|
2149
|
+
reset: () => {
|
|
2150
|
+
advisorAgent.reset();
|
|
2151
|
+
appendOnlyContext.log.clear();
|
|
2152
|
+
},
|
|
2153
|
+
rollbackTo: count => {
|
|
2154
|
+
// Drop the failed user batch + synthetic assistant-error turn
|
|
2155
|
+
// `Agent.#runLoop` appended for a turn ending in `stopReason: "error"`.
|
|
2156
|
+
const messages = advisorAgent.state.messages;
|
|
2157
|
+
if (count < messages.length) {
|
|
2158
|
+
messages.length = count;
|
|
2159
|
+
}
|
|
2160
|
+
appendOnlyContext.resetSyncCursor();
|
|
2161
|
+
advisorAgent.state.error = undefined;
|
|
2162
|
+
},
|
|
2163
|
+
state: advisorAgent.state,
|
|
2164
|
+
};
|
|
2094
2165
|
|
|
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
|
-
|
|
2166
|
+
// Persist this advisor's turns to `<session>/__advisor[.<slug>].jsonl`
|
|
2167
|
+
// (resolved lazily so it follows session switches) for stats attribution
|
|
2168
|
+
// and Agent Hub observability, without registering it as a peer.
|
|
2169
|
+
const recorder = new AdvisorTranscriptRecorder(
|
|
2170
|
+
() => this.sessionManager.getSessionFile(),
|
|
2171
|
+
() => this.sessionManager.getCwd(),
|
|
2172
|
+
advisorTranscriptFilename(slug),
|
|
2173
|
+
// On the advisor on→off→on toggle, wait for the prior recorders' closes
|
|
2174
|
+
// so two SessionManagers never hold the same file at once.
|
|
2175
|
+
this.#advisorRecorderClosed,
|
|
2176
|
+
);
|
|
2177
|
+
const runtime = new AdvisorRuntime(advisorAgentFacade, {
|
|
2178
|
+
snapshotMessages: () => this.agent.state.messages,
|
|
2179
|
+
enqueueAdvice: (note, severity) => this.#routeAdvice(advisorRef, note, severity),
|
|
2180
|
+
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
2181
|
+
obfuscator: this.#obfuscator,
|
|
2182
|
+
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
2183
|
+
notifyFailure: error => {
|
|
2184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2185
|
+
this.emitNotice(
|
|
2186
|
+
"warning",
|
|
2187
|
+
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(advisorModel)}: ${message}`,
|
|
2188
|
+
"advisor",
|
|
2189
|
+
);
|
|
2190
|
+
},
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
const advisorRef: ActiveAdvisor = {
|
|
2194
|
+
name: advisorName,
|
|
2195
|
+
slug,
|
|
2196
|
+
agent: advisorAgent,
|
|
2197
|
+
runtime,
|
|
2198
|
+
adviseTool,
|
|
2199
|
+
emissionGuard,
|
|
2200
|
+
recorder,
|
|
2201
|
+
recorderClosed: Promise.resolve(),
|
|
2202
|
+
model: advisorModel,
|
|
2203
|
+
thinkingLevel: advisorThinkingLevel,
|
|
2204
|
+
};
|
|
2205
|
+
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
2206
|
+
if (seedToCurrent) runtime.seedTo(this.agent.state.messages.length);
|
|
2207
|
+
this.#advisors.push(advisorRef);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// One shared non-blocking aside channel for all advisors; the build callback
|
|
2211
|
+
// aggregates every advisor's queued nits into one card (each entry already
|
|
2212
|
+
// carries its own `advisor` name).
|
|
2213
|
+
if (this.#advisors.length > 0 && !this.#advisorYieldQueueUnsubscribe) {
|
|
2214
|
+
this.#advisorYieldQueueUnsubscribe = this.yieldQueue.register<AdvisorNote>("advisor", {
|
|
2215
|
+
build: entries =>
|
|
2216
|
+
entries.length === 0
|
|
2217
|
+
? null
|
|
2218
|
+
: ({
|
|
2219
|
+
role: "custom",
|
|
2220
|
+
customType: "advisor",
|
|
2221
|
+
display: true,
|
|
2222
|
+
attribution: "agent",
|
|
2223
|
+
timestamp: Date.now(),
|
|
2224
|
+
content: formatAdvisorBatchContent(entries),
|
|
2225
|
+
details: { notes: entries } satisfies AdvisorMessageDetails,
|
|
2226
|
+
} satisfies CustomMessage),
|
|
2227
|
+
skipIdleFlush: true,
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
return this.#advisors.length > 0;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
/** Provider/session id for an advisor's loop. The slug suffix MUST match the
|
|
2235
|
+
* advisor's transcript filename so stats/telemetry attribute the same advisor. */
|
|
2236
|
+
#advisorSessionId(slug: string): string | undefined {
|
|
2237
|
+
if (!this.sessionId) return undefined;
|
|
2238
|
+
return slug ? `${this.sessionId}-advisor-${slug}` : `${this.sessionId}-advisor`;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
2243
|
+
* blocker interrupt the running agent through the steering channel; once the
|
|
2244
|
+
* loop has yielded, `triggerTurn` resumes it. After a deliberate user interrupt
|
|
2245
|
+
* auto-resume is suppressed while idle/unwinding (the note becomes a preserved
|
|
2246
|
+
* card re-entering on resume); a live-streaming turn is steered in directly. A
|
|
2247
|
+
* plain nit always rides the non-interrupting YieldQueue aside. Suppression by
|
|
2248
|
+
* the per-advisor emission guard drops the note silently — the model still saw
|
|
2249
|
+
* `Recorded.`, so it isn't tempted to rephrase the same note past the dedupe.
|
|
2250
|
+
*/
|
|
2251
|
+
#routeAdvice(advisor: ActiveAdvisor, note: string, severity?: AdvisorSeverity): void {
|
|
2252
|
+
if (!advisor.emissionGuard.accept(note)) {
|
|
2253
|
+
logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
// The implicit single ("default") advisor stamps no source name, so its
|
|
2257
|
+
// agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
|
|
2258
|
+
const source = advisor.slug ? advisor.name : undefined;
|
|
2259
|
+
const interrupting = isInterruptingSeverity(severity);
|
|
2260
|
+
const channel = resolveAdvisorDeliveryChannel({
|
|
2261
|
+
severity,
|
|
2262
|
+
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
2263
|
+
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
2264
|
+
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
2265
|
+
// loop consumes a steer at its next boundary.
|
|
2266
|
+
streaming: this.agent.state.isStreaming,
|
|
2267
|
+
aborting: this.#abortInProgress,
|
|
2268
|
+
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
2142
2269
|
});
|
|
2270
|
+
if (channel === "aside") {
|
|
2271
|
+
this.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const notes: AdvisorNote[] = [{ note, severity, advisor: source }];
|
|
2275
|
+
const content = formatAdvisorBatchContent(notes);
|
|
2276
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
2277
|
+
if (channel === "preserve") {
|
|
2278
|
+
this.#preserveAdvisorCard({
|
|
2279
|
+
role: "custom",
|
|
2280
|
+
customType: "advisor",
|
|
2281
|
+
content,
|
|
2282
|
+
display: true,
|
|
2283
|
+
attribution: "agent",
|
|
2284
|
+
details,
|
|
2285
|
+
timestamp: Date.now(),
|
|
2286
|
+
});
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
this.#recordAdvisorInterruptDelivered();
|
|
2290
|
+
void this.sendCustomMessage(
|
|
2291
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
2292
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
2293
|
+
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
2294
|
+
}
|
|
2143
2295
|
|
|
2144
|
-
|
|
2296
|
+
/** Re-prime every advisor's transcript view (compaction/shake/rewind) without the
|
|
2297
|
+
* session-level latch reset {@link #resetAdvisorSessionState} performs. */
|
|
2298
|
+
#resetAllAdvisorRuntimes(): void {
|
|
2299
|
+
for (const a of this.#advisors) a.runtime.reset();
|
|
2145
2300
|
}
|
|
2146
2301
|
|
|
2147
2302
|
#stopAdvisorRuntime(): void {
|
|
2148
|
-
// Detach
|
|
2303
|
+
// Detach each recorder feed BEFORE aborting its advisor agent: dispose() aborts
|
|
2149
2304
|
// the loop, and an abort emits a final `message_end` we must not enqueue against
|
|
2150
2305
|
// 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 —
|
|
2306
|
+
const closes: Promise<void>[] = [];
|
|
2307
|
+
for (const a of this.#advisors) {
|
|
2308
|
+
a.agentUnsubscribe?.();
|
|
2309
|
+
a.agentUnsubscribe = undefined;
|
|
2310
|
+
a.runtime.dispose();
|
|
2311
|
+
// Capture each close so dispose()/`/drop` can await the queued open+append+close —
|
|
2159
2312
|
// the last advisor turn would otherwise be lost on a fast process exit.
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
}
|
|
2163
|
-
if (this.#advisorAgent) {
|
|
2164
|
-
this.#advisorAgent = undefined;
|
|
2313
|
+
a.recorderClosed = a.recorder.close();
|
|
2314
|
+
closes.push(a.recorderClosed);
|
|
2165
2315
|
}
|
|
2166
|
-
this.#
|
|
2316
|
+
this.#advisorRecorderClosed = Promise.all(closes).then(() => {});
|
|
2317
|
+
this.#advisors = [];
|
|
2167
2318
|
this.#advisorYieldQueueUnsubscribe?.();
|
|
2168
2319
|
this.#advisorYieldQueueUnsubscribe = undefined;
|
|
2169
2320
|
}
|
|
@@ -2171,16 +2322,13 @@ export class AgentSession {
|
|
|
2171
2322
|
/** Subscribe the advisor agent's finalized messages into the transcript recorder.
|
|
2172
2323
|
* Idempotent-by-replacement: callers detach the prior feed first. Kept separate
|
|
2173
2324
|
* 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);
|
|
2325
|
+
#attachAdvisorRecorderFeed(advisor: ActiveAdvisor): void {
|
|
2326
|
+
advisor.agentUnsubscribe = advisor.agent.subscribe(event => {
|
|
2327
|
+
if (event.type === "message_end") advisor.recorder.record(event.message);
|
|
2180
2328
|
});
|
|
2181
2329
|
}
|
|
2182
2330
|
|
|
2183
|
-
async #promoteAdvisorContextModel(currentModel: Model): Promise<boolean> {
|
|
2331
|
+
async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
|
|
2184
2332
|
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
2185
2333
|
if (!promotionSettings.enabled) return false;
|
|
2186
2334
|
const contextWindow = currentModel.contextWindow ?? 0;
|
|
@@ -2188,25 +2336,23 @@ export class AgentSession {
|
|
|
2188
2336
|
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
2189
2337
|
if (!targetModel) return false;
|
|
2190
2338
|
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
this.#modelRegistry,
|
|
2195
|
-
);
|
|
2196
|
-
const advisorThinkingLevel = advisorSel?.thinkingLevel ?? ThinkingLevel.Medium;
|
|
2197
|
-
|
|
2339
|
+
// Preserve this advisor's own thinking level (a configured `model:...:high`
|
|
2340
|
+
// keeps its suffix across a promotion); only the model changes.
|
|
2341
|
+
const advisorThinkingLevel = advisor.thinkingLevel;
|
|
2198
2342
|
try {
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2343
|
+
advisor.agent.setModel(targetModel);
|
|
2344
|
+
advisor.agent.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
2345
|
+
advisor.agent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2346
|
+
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
2203
2347
|
logger.debug("Advisor context promotion switched model on overflow", {
|
|
2348
|
+
advisor: advisor.name,
|
|
2204
2349
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2205
2350
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2206
2351
|
});
|
|
2207
2352
|
return true;
|
|
2208
2353
|
} catch (error) {
|
|
2209
2354
|
logger.warn("Advisor context promotion failed", {
|
|
2355
|
+
advisor: advisor.name,
|
|
2210
2356
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2211
2357
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2212
2358
|
error: String(error),
|
|
@@ -2215,19 +2361,18 @@ export class AgentSession {
|
|
|
2215
2361
|
}
|
|
2216
2362
|
}
|
|
2217
2363
|
|
|
2218
|
-
async #maintainAdvisorContext(incomingTokens: number): Promise<boolean> {
|
|
2219
|
-
const
|
|
2220
|
-
if (!advisor) return false;
|
|
2364
|
+
async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
|
|
2365
|
+
const agent = advisor.agent;
|
|
2221
2366
|
|
|
2222
2367
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
2223
2368
|
if (compactionSettings.strategy === "off") return false;
|
|
2224
2369
|
if (!compactionSettings.enabled) return false;
|
|
2225
2370
|
|
|
2226
|
-
const advisorModel =
|
|
2371
|
+
const advisorModel = agent.state.model;
|
|
2227
2372
|
const contextWindow = advisorModel.contextWindow ?? 0;
|
|
2228
2373
|
if (contextWindow <= 0) return false;
|
|
2229
2374
|
|
|
2230
|
-
const messages =
|
|
2375
|
+
const messages = agent.state.messages;
|
|
2231
2376
|
let contextTokens = incomingTokens;
|
|
2232
2377
|
for (const message of messages) {
|
|
2233
2378
|
contextTokens += estimateTokens(message);
|
|
@@ -2238,9 +2383,9 @@ export class AgentSession {
|
|
|
2238
2383
|
}
|
|
2239
2384
|
|
|
2240
2385
|
// 1. Try promotion first
|
|
2241
|
-
if (await this.#promoteAdvisorContextModel(advisorModel)) {
|
|
2386
|
+
if (await this.#promoteAdvisorContextModel(advisor, advisorModel)) {
|
|
2242
2387
|
// Promotion succeeded, check if new model has enough space
|
|
2243
|
-
const newModel =
|
|
2388
|
+
const newModel = agent.state.model;
|
|
2244
2389
|
const newWindow = newModel.contextWindow ?? 0;
|
|
2245
2390
|
if (newWindow > 0) {
|
|
2246
2391
|
const stillNeedsCompaction = shouldCompact(contextTokens, newWindow, compactionSettings);
|
|
@@ -2262,7 +2407,9 @@ export class AgentSession {
|
|
|
2262
2407
|
timestamp,
|
|
2263
2408
|
summary: message.summary,
|
|
2264
2409
|
shortSummary: message.shortSummary,
|
|
2265
|
-
firstKeptEntryId:
|
|
2410
|
+
firstKeptEntryId:
|
|
2411
|
+
(message as CompactionSummaryMessage & { firstKeptEntryId?: string }).firstKeptEntryId ||
|
|
2412
|
+
`msg-${i + 1}`,
|
|
2266
2413
|
tokensBefore: message.tokensBefore,
|
|
2267
2414
|
} satisfies CompactionEntry;
|
|
2268
2415
|
}
|
|
@@ -2276,33 +2423,37 @@ export class AgentSession {
|
|
|
2276
2423
|
} satisfies SessionMessageEntry;
|
|
2277
2424
|
});
|
|
2278
2425
|
|
|
2279
|
-
const
|
|
2426
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
2427
|
+
const candidates = this.#resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
2428
|
+
if (candidates.length === 0) {
|
|
2429
|
+
// No compaction candidates, fallback to re-prime
|
|
2430
|
+
return true;
|
|
2431
|
+
}
|
|
2432
|
+
const advisorSessionId = this.#advisorSessionId(advisor.slug);
|
|
2433
|
+
const preparation = prepareCompaction(
|
|
2434
|
+
pathEntries,
|
|
2435
|
+
compactionSettings,
|
|
2436
|
+
await this.#runnableCompactionCandidates(candidates, advisorSessionId),
|
|
2437
|
+
);
|
|
2280
2438
|
if (!preparation) {
|
|
2281
2439
|
// Cannot prepare compaction, fallback to re-prime
|
|
2282
2440
|
return true;
|
|
2283
2441
|
}
|
|
2284
2442
|
|
|
2285
|
-
const advisorCompactionThinkingLevel: ThinkingLevel | undefined =
|
|
2443
|
+
const advisorCompactionThinkingLevel: ThinkingLevel | undefined = agent.state.disableReasoning
|
|
2286
2444
|
? ThinkingLevel.Off
|
|
2287
|
-
:
|
|
2445
|
+
: agent.state.thinkingLevel;
|
|
2288
2446
|
|
|
2289
2447
|
// Advisor state is in-memory-only, so snapcompact's frame archive has no
|
|
2290
2448
|
// stable SessionEntry preserveData slot to carry across future advisor
|
|
2291
2449
|
// maintenance runs. Use an LLM summary even when the primary session is
|
|
2292
2450
|
// 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
2451
|
|
|
2300
2452
|
let compactResult: CompactionResult | undefined;
|
|
2301
2453
|
let lastError: unknown;
|
|
2302
|
-
const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
|
|
2303
2454
|
// Instrument the advisor's overflow-compaction one-shot like the primary
|
|
2304
2455
|
// compaction path so the advisor model's maintenance call also emits spans.
|
|
2305
|
-
const telemetry = resolveTelemetry(
|
|
2456
|
+
const telemetry = resolveTelemetry(agent.telemetry, advisorSessionId);
|
|
2306
2457
|
|
|
2307
2458
|
for (const candidate of candidates) {
|
|
2308
2459
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorSessionId);
|
|
@@ -2319,6 +2470,9 @@ export class AgentSession {
|
|
|
2319
2470
|
thinkingLevel: advisorCompactionThinkingLevel,
|
|
2320
2471
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
2321
2472
|
telemetry,
|
|
2473
|
+
tools: agent.state.tools,
|
|
2474
|
+
sessionId: advisorSessionId,
|
|
2475
|
+
promptCacheKey: advisorSessionId,
|
|
2322
2476
|
},
|
|
2323
2477
|
);
|
|
2324
2478
|
break;
|
|
@@ -2343,7 +2497,7 @@ export class AgentSession {
|
|
|
2343
2497
|
firstKeptEntryId,
|
|
2344
2498
|
} as CompactionSummaryMessage & { firstKeptEntryId?: string };
|
|
2345
2499
|
|
|
2346
|
-
|
|
2500
|
+
agent.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
2347
2501
|
return false;
|
|
2348
2502
|
}
|
|
2349
2503
|
|
|
@@ -2776,6 +2930,37 @@ export class AgentSession {
|
|
|
2776
2930
|
}
|
|
2777
2931
|
}
|
|
2778
2932
|
|
|
2933
|
+
/**
|
|
2934
|
+
* On a user-interrupted (`Esc`) abort, copy the trailing thinking run into a
|
|
2935
|
+
* hidden `display: false` continuity message for the next turn WITHOUT
|
|
2936
|
+
* mutating the assistant message. The original thinking stays on the message
|
|
2937
|
+
* so live render, reload, and Ctrl+L rebuilds keep showing it; `convertToLlm`
|
|
2938
|
+
* strips the run from the provider request (incomplete/unsigned thinking is
|
|
2939
|
+
* rejected on resend) when this continuity message follows the assistant turn.
|
|
2940
|
+
*/
|
|
2941
|
+
#demoteInterruptedThinkingOnUserInterrupt(
|
|
2942
|
+
message: AssistantMessage,
|
|
2943
|
+
): CustomMessage<InterruptedThinkingDetails> | undefined {
|
|
2944
|
+
if (message.stopReason !== "aborted" || !isUserInterruptAbort(message)) return undefined;
|
|
2945
|
+
const demoted = demoteInterruptedThinking(message);
|
|
2946
|
+
if (!demoted) return undefined;
|
|
2947
|
+
const interruptedAt = Date.now();
|
|
2948
|
+
return {
|
|
2949
|
+
role: "custom",
|
|
2950
|
+
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
2951
|
+
content: prompt.render(interruptedThinkingTemplate, { reasoning: demoted.reasoning }),
|
|
2952
|
+
display: false,
|
|
2953
|
+
details: {
|
|
2954
|
+
interruptedAt,
|
|
2955
|
+
provider: message.provider,
|
|
2956
|
+
model: message.model,
|
|
2957
|
+
blockCount: demoted.blockCount,
|
|
2958
|
+
},
|
|
2959
|
+
attribution: "agent",
|
|
2960
|
+
timestamp: interruptedAt,
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2779
2964
|
async #persistTurnMessagesForMidRunCompaction(context: AgentTurnEndContext | undefined): Promise<boolean> {
|
|
2780
2965
|
if (!context) return true;
|
|
2781
2966
|
const turnMessages = [context.message, ...context.toolResults];
|
|
@@ -2843,6 +3028,17 @@ export class AgentSession {
|
|
|
2843
3028
|
}
|
|
2844
3029
|
}
|
|
2845
3030
|
|
|
3031
|
+
const interruptedThinkingMessage =
|
|
3032
|
+
event.type === "message_end" && event.message.role === "assistant"
|
|
3033
|
+
? this.#demoteInterruptedThinkingOnUserInterrupt(event.message as AssistantMessage)
|
|
3034
|
+
: undefined;
|
|
3035
|
+
// `message_end` handling is fire-and-forget from agent-core. Make the
|
|
3036
|
+
// hidden continuity turn visible to the next prompt before any awaited
|
|
3037
|
+
// extension delivery or persistence can stall this handler.
|
|
3038
|
+
if (interruptedThinkingMessage) {
|
|
3039
|
+
this.agent.appendMessage(interruptedThinkingMessage);
|
|
3040
|
+
}
|
|
3041
|
+
|
|
2846
3042
|
const messageEndPersistence =
|
|
2847
3043
|
event.type === "message_end" ? this.#createMessageEndPersistenceSlot(event.message) : undefined;
|
|
2848
3044
|
|
|
@@ -2985,6 +3181,15 @@ export class AgentSession {
|
|
|
2985
3181
|
} else {
|
|
2986
3182
|
persistMessageEnd();
|
|
2987
3183
|
}
|
|
3184
|
+
if (interruptedThinkingMessage) {
|
|
3185
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
3186
|
+
interruptedThinkingMessage.customType,
|
|
3187
|
+
interruptedThinkingMessage.content,
|
|
3188
|
+
interruptedThinkingMessage.display,
|
|
3189
|
+
interruptedThinkingMessage.details,
|
|
3190
|
+
interruptedThinkingMessage.attribution,
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
2988
3193
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
2989
3194
|
|
|
2990
3195
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
@@ -3041,9 +3246,10 @@ export class AgentSession {
|
|
|
3041
3246
|
}
|
|
3042
3247
|
}
|
|
3043
3248
|
if (event.message.role === "toolResult") {
|
|
3044
|
-
const { toolName, details, isError, content } = event.message as {
|
|
3249
|
+
const { toolName, toolCallId, details, isError, content } = event.message as {
|
|
3250
|
+
toolCallId?: string;
|
|
3045
3251
|
toolName?: string;
|
|
3046
|
-
details?: { path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
|
|
3252
|
+
details?: { op?: string; path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
|
|
3047
3253
|
isError?: boolean;
|
|
3048
3254
|
content?: Array<TextContent | ImageContent>;
|
|
3049
3255
|
};
|
|
@@ -3057,6 +3263,9 @@ export class AgentSession {
|
|
|
3057
3263
|
}
|
|
3058
3264
|
if (toolName === "todo" && !isError && Array.isArray(details?.phases)) {
|
|
3059
3265
|
this.setTodoPhases(details.phases);
|
|
3266
|
+
if (this.#isTodoInitResult(details, toolCallId)) {
|
|
3267
|
+
this.#scheduleReplanTitleRefresh();
|
|
3268
|
+
}
|
|
3060
3269
|
}
|
|
3061
3270
|
if (toolName === "todo" && isError) {
|
|
3062
3271
|
const errorText = content?.find(part => part.type === "text")?.text;
|
|
@@ -6260,16 +6469,46 @@ export class AgentSession {
|
|
|
6260
6469
|
#buildGoalModeMessage(): CustomMessage | null {
|
|
6261
6470
|
const content = this.#goalRuntime.buildActivePrompt();
|
|
6262
6471
|
if (!content) return null;
|
|
6472
|
+
const todoContext = this.#buildGoalTodoContext();
|
|
6263
6473
|
return {
|
|
6264
6474
|
role: "custom",
|
|
6265
6475
|
customType: "goal-mode-context",
|
|
6266
|
-
content,
|
|
6476
|
+
content: todoContext ? `${content}\n\n${todoContext}` : content,
|
|
6267
6477
|
display: false,
|
|
6268
6478
|
attribution: "agent",
|
|
6269
6479
|
timestamp: Date.now(),
|
|
6270
6480
|
};
|
|
6271
6481
|
}
|
|
6272
6482
|
|
|
6483
|
+
#buildGoalTodoContext(): string | undefined {
|
|
6484
|
+
if (!this.settings.get("todo.enabled")) return undefined;
|
|
6485
|
+
const phases = this.getTodoPhases().filter(phase => phase.tasks.length > 0);
|
|
6486
|
+
if (phases.length === 0) return undefined;
|
|
6487
|
+
|
|
6488
|
+
let total = 0;
|
|
6489
|
+
let closed = 0;
|
|
6490
|
+
let open = 0;
|
|
6491
|
+
const promptPhases = phases.map(phase => ({
|
|
6492
|
+
name: phase.name,
|
|
6493
|
+
tasks: phase.tasks.map(task => {
|
|
6494
|
+
total++;
|
|
6495
|
+
if (task.status === "completed" || task.status === "abandoned") {
|
|
6496
|
+
closed++;
|
|
6497
|
+
} else {
|
|
6498
|
+
open++;
|
|
6499
|
+
}
|
|
6500
|
+
return { content: task.content, status: task.status };
|
|
6501
|
+
}),
|
|
6502
|
+
}));
|
|
6503
|
+
|
|
6504
|
+
return prompt.render(goalTodoContextPrompt, {
|
|
6505
|
+
closed: String(closed),
|
|
6506
|
+
open: String(open),
|
|
6507
|
+
phases: promptPhases,
|
|
6508
|
+
total: String(total),
|
|
6509
|
+
});
|
|
6510
|
+
}
|
|
6511
|
+
|
|
6273
6512
|
#normalizeImagesForModel(images: ImageContent[] | undefined): Promise<ImageContent[] | undefined> {
|
|
6274
6513
|
return normalizeModelContextImages(images, { model: this.model });
|
|
6275
6514
|
}
|
|
@@ -6526,7 +6765,10 @@ export class AgentSession {
|
|
|
6526
6765
|
|
|
6527
6766
|
async promptCustomMessage<T = unknown>(
|
|
6528
6767
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
6529
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & {
|
|
6768
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & {
|
|
6769
|
+
queueChipText?: string;
|
|
6770
|
+
queueOnly?: boolean;
|
|
6771
|
+
},
|
|
6530
6772
|
): Promise<void> {
|
|
6531
6773
|
const textContent =
|
|
6532
6774
|
typeof message.content === "string"
|
|
@@ -6546,6 +6788,16 @@ export class AgentSession {
|
|
|
6546
6788
|
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
6547
6789
|
}
|
|
6548
6790
|
|
|
6791
|
+
if (options?.queueOnly) {
|
|
6792
|
+
if (!options.streamingBehavior) {
|
|
6793
|
+
throw new AgentBusyError();
|
|
6794
|
+
}
|
|
6795
|
+
for (const notice of keywordNotices) {
|
|
6796
|
+
await this.#queueCustomMessage(notice, options.streamingBehavior);
|
|
6797
|
+
}
|
|
6798
|
+
await this.#queueCustomMessage(message, options.streamingBehavior, options.queueChipText);
|
|
6799
|
+
return;
|
|
6800
|
+
}
|
|
6549
6801
|
if (this.isStreaming) {
|
|
6550
6802
|
if (!options?.streamingBehavior) {
|
|
6551
6803
|
throw new AgentBusyError();
|
|
@@ -7111,6 +7363,40 @@ export class AgentSession {
|
|
|
7111
7363
|
}
|
|
7112
7364
|
}
|
|
7113
7365
|
|
|
7366
|
+
/** Queue a custom message without starting a turn, matching steer/follow-up delivery. */
|
|
7367
|
+
async #queueCustomMessage<T = unknown>(
|
|
7368
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
7369
|
+
deliverAs: "steer" | "followUp",
|
|
7370
|
+
queueChipText?: string,
|
|
7371
|
+
): Promise<void> {
|
|
7372
|
+
const details =
|
|
7373
|
+
queueChipText !== undefined
|
|
7374
|
+
? ({
|
|
7375
|
+
...((message.details && typeof message.details === "object" ? message.details : {}) as Record<
|
|
7376
|
+
string,
|
|
7377
|
+
unknown
|
|
7378
|
+
>),
|
|
7379
|
+
__queueChipText: queueChipText,
|
|
7380
|
+
} as T)
|
|
7381
|
+
: message.details;
|
|
7382
|
+
const appMessage: CustomMessage<T> = {
|
|
7383
|
+
role: "custom",
|
|
7384
|
+
customType: message.customType,
|
|
7385
|
+
content: message.content,
|
|
7386
|
+
display: message.display,
|
|
7387
|
+
details,
|
|
7388
|
+
attribution: message.attribution ?? "agent",
|
|
7389
|
+
timestamp: Date.now(),
|
|
7390
|
+
};
|
|
7391
|
+
const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
|
|
7392
|
+
if (deliverAs === "followUp") {
|
|
7393
|
+
this.agent.followUp(normalizedAppMessage);
|
|
7394
|
+
} else {
|
|
7395
|
+
this.agent.steer(normalizedAppMessage);
|
|
7396
|
+
}
|
|
7397
|
+
this.#scheduleIdleQueueDrain();
|
|
7398
|
+
}
|
|
7399
|
+
|
|
7114
7400
|
/**
|
|
7115
7401
|
* Send a custom message to the session. Creates a CustomMessageEntry.
|
|
7116
7402
|
*
|
|
@@ -7353,6 +7639,74 @@ export class AgentSession {
|
|
|
7353
7639
|
this.#todoPhases = this.#cloneTodoPhases(phases);
|
|
7354
7640
|
}
|
|
7355
7641
|
|
|
7642
|
+
#isTodoInitResult(details: Record<string, unknown>, toolCallId: string | undefined): boolean {
|
|
7643
|
+
const detailOp = getStringProperty(details, "op");
|
|
7644
|
+
if (detailOp) return detailOp === "init";
|
|
7645
|
+
if (!toolCallId) return false;
|
|
7646
|
+
for (let i = this.agent.state.messages.length - 1; i >= 0; i--) {
|
|
7647
|
+
const message = this.agent.state.messages[i];
|
|
7648
|
+
if (!message) continue;
|
|
7649
|
+
const op = toolCallOpFromMessage(message, toolCallId);
|
|
7650
|
+
if (op) return op === "init";
|
|
7651
|
+
}
|
|
7652
|
+
return false;
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
#buildReplanTitleContext(): string {
|
|
7656
|
+
const turns: TitleConversationTurn[] = [];
|
|
7657
|
+
for (
|
|
7658
|
+
let i = this.agent.state.messages.length - 1;
|
|
7659
|
+
i >= 0 && turns.length < REPLAN_TITLE_CONTEXT_TURN_LIMIT;
|
|
7660
|
+
i--
|
|
7661
|
+
) {
|
|
7662
|
+
const message = this.agent.state.messages[i];
|
|
7663
|
+
if (!message) continue;
|
|
7664
|
+
const turn = titleConversationTurnFromMessage(message);
|
|
7665
|
+
if (turn) turns.push(turn);
|
|
7666
|
+
}
|
|
7667
|
+
turns.reverse();
|
|
7668
|
+
return formatTitleConversationContext(turns);
|
|
7669
|
+
}
|
|
7670
|
+
|
|
7671
|
+
#scheduleReplanTitleRefresh(): void {
|
|
7672
|
+
if (this.#replanTitleRefreshInFlight) return;
|
|
7673
|
+
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
7674
|
+
if (this.sessionManager.titleSource === "user") return;
|
|
7675
|
+
const context = this.#buildReplanTitleContext();
|
|
7676
|
+
if (!context) return;
|
|
7677
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
7678
|
+
const refresh = this.#refreshTitleAfterReplan(context, sessionId)
|
|
7679
|
+
.catch(err => {
|
|
7680
|
+
logger.warn("title-generator: replan refresh failed", {
|
|
7681
|
+
sessionId,
|
|
7682
|
+
error: err instanceof Error ? err.message : String(err),
|
|
7683
|
+
});
|
|
7684
|
+
})
|
|
7685
|
+
.finally(() => {
|
|
7686
|
+
if (this.#replanTitleRefreshInFlight === refresh) {
|
|
7687
|
+
this.#replanTitleRefreshInFlight = undefined;
|
|
7688
|
+
}
|
|
7689
|
+
});
|
|
7690
|
+
this.#replanTitleRefreshInFlight = refresh;
|
|
7691
|
+
}
|
|
7692
|
+
|
|
7693
|
+
async #refreshTitleAfterReplan(context: string, sessionId: string): Promise<void> {
|
|
7694
|
+
const title = await generateSessionTitle(
|
|
7695
|
+
context,
|
|
7696
|
+
this.#modelRegistry,
|
|
7697
|
+
this.settings,
|
|
7698
|
+
sessionId,
|
|
7699
|
+
this.model,
|
|
7700
|
+
provider => this.agent.metadataForProvider(provider),
|
|
7701
|
+
);
|
|
7702
|
+
if (!title) return;
|
|
7703
|
+
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
7704
|
+
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
7705
|
+
if (this.sessionManager.titleSource === "user") return;
|
|
7706
|
+
const setSessionName = this.sessionManager.setSessionName as SetSessionNameWithTrigger;
|
|
7707
|
+
await setSessionName.call(this.sessionManager, title, "auto", "replan");
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7356
7710
|
#syncTodoPhasesFromBranch(): void {
|
|
7357
7711
|
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
7358
7712
|
// Strip completed/abandoned tasks — they were done in a previous run,
|
|
@@ -7495,9 +7849,11 @@ export class AgentSession {
|
|
|
7495
7849
|
// running advisor turn could otherwise finish, emit `message_end`, and recreate
|
|
7496
7850
|
// `<old>/__advisor.jsonl`. #resetAdvisorSessionState (after newSession) re-primes
|
|
7497
7851
|
// the advisor and re-attaches the feed at the new session's path.
|
|
7498
|
-
this.#
|
|
7499
|
-
|
|
7500
|
-
|
|
7852
|
+
for (const a of this.#advisors) {
|
|
7853
|
+
a.agentUnsubscribe?.();
|
|
7854
|
+
a.agentUnsubscribe = undefined;
|
|
7855
|
+
await a.recorder.close();
|
|
7856
|
+
}
|
|
7501
7857
|
try {
|
|
7502
7858
|
await this.sessionManager.dropSession(previousSessionFile);
|
|
7503
7859
|
} catch (err) {
|
|
@@ -7551,8 +7907,9 @@ export class AgentSession {
|
|
|
7551
7907
|
/**
|
|
7552
7908
|
* Set a display name for the current session.
|
|
7553
7909
|
*/
|
|
7554
|
-
setSessionName(name: string, source: "auto" | "user" = "auto"): Promise<boolean> {
|
|
7555
|
-
|
|
7910
|
+
setSessionName(name: string, source: "auto" | "user" = "auto", trigger?: SessionNameTrigger): Promise<boolean> {
|
|
7911
|
+
const setSessionName = this.sessionManager.setSessionName as SetSessionNameWithTrigger;
|
|
7912
|
+
return setSessionName.call(this.sessionManager, name, source, trigger);
|
|
7556
7913
|
}
|
|
7557
7914
|
|
|
7558
7915
|
/**
|
|
@@ -7630,16 +7987,24 @@ export class AgentSession {
|
|
|
7630
7987
|
/**
|
|
7631
7988
|
* Set model directly.
|
|
7632
7989
|
* Validates that a credential source is configured (synchronously, without
|
|
7633
|
-
* refreshing OAuth or running command-backed key programs)
|
|
7634
|
-
*
|
|
7635
|
-
*
|
|
7990
|
+
* refreshing OAuth or running command-backed key programs). The active
|
|
7991
|
+
* session switches by default; when `currentContextTokens` is provided and
|
|
7992
|
+
* exceeds the refreshed candidate's context window, the live switch is
|
|
7993
|
+
* skipped while role persistence still runs. Returns whether the active
|
|
7994
|
+
* model actually switched, computed against the refreshed metadata so
|
|
7995
|
+
* dynamic providers (e.g. llama.cpp) honor their post-load contextWindow.
|
|
7636
7996
|
* @throws Error if no API key available for the model
|
|
7637
7997
|
*/
|
|
7638
7998
|
async setModel(
|
|
7639
7999
|
model: Model,
|
|
7640
8000
|
role: string = "default",
|
|
7641
|
-
options?: {
|
|
7642
|
-
|
|
8001
|
+
options?: {
|
|
8002
|
+
selector?: string;
|
|
8003
|
+
thinkingLevel?: ThinkingLevel;
|
|
8004
|
+
persist?: boolean;
|
|
8005
|
+
currentContextTokens?: number;
|
|
8006
|
+
},
|
|
8007
|
+
): Promise<{ switched: boolean }> {
|
|
7643
8008
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
7644
8009
|
if (!this.#modelRegistry.hasConfiguredAuth(model)) {
|
|
7645
8010
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -7647,21 +8012,35 @@ export class AgentSession {
|
|
|
7647
8012
|
|
|
7648
8013
|
const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
|
|
7649
8014
|
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
8015
|
+
const currentContextTokens = options?.currentContextTokens ?? 0;
|
|
8016
|
+
const targetContextWindow = targetModel.contextWindow ?? 0;
|
|
8017
|
+
const switched = !(
|
|
8018
|
+
currentContextTokens > 0 &&
|
|
8019
|
+
targetContextWindow > 0 &&
|
|
8020
|
+
currentContextTokens > targetContextWindow
|
|
8021
|
+
);
|
|
8022
|
+
|
|
8023
|
+
if (switched) {
|
|
8024
|
+
this.#clearActiveRetryFallback();
|
|
8025
|
+
this.#setModelWithProviderSessionReset(targetModel);
|
|
8026
|
+
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
8027
|
+
}
|
|
7653
8028
|
if (options?.persist) {
|
|
7654
8029
|
this.settings.setModelRole(
|
|
7655
8030
|
role,
|
|
7656
8031
|
this.#formatRoleModelValue(role, targetModel, options.selector, options.thinkingLevel),
|
|
7657
8032
|
);
|
|
7658
8033
|
}
|
|
8034
|
+
if (!switched) {
|
|
8035
|
+
return { switched: false };
|
|
8036
|
+
}
|
|
7659
8037
|
this.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
7660
8038
|
|
|
7661
8039
|
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
7662
8040
|
// configured defaultLevel; otherwise preserve the current level (or auto).
|
|
7663
8041
|
this.#reapplyThinkingLevel(targetModel.thinking?.defaultLevel);
|
|
7664
8042
|
await this.#syncAfterModelChange(previousEditMode);
|
|
8043
|
+
return { switched: true };
|
|
7665
8044
|
}
|
|
7666
8045
|
|
|
7667
8046
|
/**
|
|
@@ -8186,7 +8565,7 @@ export class AgentSession {
|
|
|
8186
8565
|
await this.sessionManager.rewriteEntries();
|
|
8187
8566
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8188
8567
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8189
|
-
this.#
|
|
8568
|
+
this.#resetAllAdvisorRuntimes();
|
|
8190
8569
|
this.#syncTodoPhasesFromBranch();
|
|
8191
8570
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8192
8571
|
return result;
|
|
@@ -8223,7 +8602,7 @@ export class AgentSession {
|
|
|
8223
8602
|
|
|
8224
8603
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8225
8604
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8226
|
-
this.#
|
|
8605
|
+
this.#resetAllAdvisorRuntimes();
|
|
8227
8606
|
this.#syncTodoPhasesFromBranch();
|
|
8228
8607
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8229
8608
|
return result;
|
|
@@ -8274,7 +8653,7 @@ export class AgentSession {
|
|
|
8274
8653
|
await this.sessionManager.rewriteEntries();
|
|
8275
8654
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8276
8655
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8277
|
-
this.#
|
|
8656
|
+
this.#resetAllAdvisorRuntimes();
|
|
8278
8657
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8279
8658
|
return { removed };
|
|
8280
8659
|
}
|
|
@@ -8331,7 +8710,7 @@ export class AgentSession {
|
|
|
8331
8710
|
await this.sessionManager.rewriteEntries();
|
|
8332
8711
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8333
8712
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8334
|
-
this.#
|
|
8713
|
+
this.#resetAllAdvisorRuntimes();
|
|
8335
8714
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8336
8715
|
|
|
8337
8716
|
return {
|
|
@@ -8427,7 +8806,11 @@ export class AgentSession {
|
|
|
8427
8806
|
compactionCandidates = this.#getCompactionModelCandidates(availableModels);
|
|
8428
8807
|
}
|
|
8429
8808
|
const pathEntries = this.sessionManager.getBranch();
|
|
8430
|
-
const preparation = prepareCompaction(
|
|
8809
|
+
const preparation = prepareCompaction(
|
|
8810
|
+
pathEntries,
|
|
8811
|
+
effectiveSettings,
|
|
8812
|
+
await this.#runnableCompactionCandidates(compactionCandidates, this.sessionId),
|
|
8813
|
+
);
|
|
8431
8814
|
if (!preparation) {
|
|
8432
8815
|
// Check why we can't compact
|
|
8433
8816
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -8617,7 +9000,7 @@ export class AgentSession {
|
|
|
8617
9000
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
8618
9001
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
8619
9002
|
this.#planReferenceSent = false;
|
|
8620
|
-
this.#
|
|
9003
|
+
this.#resetAllAdvisorRuntimes();
|
|
8621
9004
|
this.#syncTodoPhasesFromBranch();
|
|
8622
9005
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8623
9006
|
|
|
@@ -8881,7 +9264,7 @@ export class AgentSession {
|
|
|
8881
9264
|
// Rebuild agent messages from session
|
|
8882
9265
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8883
9266
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8884
|
-
this.#
|
|
9267
|
+
this.#resetAllAdvisorRuntimes();
|
|
8885
9268
|
this.#syncTodoPhasesFromBranch();
|
|
8886
9269
|
|
|
8887
9270
|
return { document: handoffText, savedPath };
|
|
@@ -10201,6 +10584,17 @@ export class AgentSession {
|
|
|
10201
10584
|
return this.#resolveCompactionModelCandidates(this.model, availableModels, filter);
|
|
10202
10585
|
}
|
|
10203
10586
|
|
|
10587
|
+
/**
|
|
10588
|
+
* Compaction candidates that can actually run — those with a resolvable API
|
|
10589
|
+
* key, matching the per-candidate getApiKey gate the execution loop applies.
|
|
10590
|
+
* Re-expansion reusability (prepareCompaction) must judge remote-preserve
|
|
10591
|
+
* reuse against these, not against candidates the loop would skip at runtime.
|
|
10592
|
+
*/
|
|
10593
|
+
async #runnableCompactionCandidates(candidates: readonly Model[], sessionId: string | undefined): Promise<Model[]> {
|
|
10594
|
+
const keys = await Promise.all(candidates.map(model => this.#modelRegistry.getApiKey(model, sessionId)));
|
|
10595
|
+
return candidates.filter((_, index) => keys[index] !== undefined);
|
|
10596
|
+
}
|
|
10597
|
+
|
|
10204
10598
|
#resolveCompactionModelCandidates(
|
|
10205
10599
|
preferredModel: Model | null | undefined,
|
|
10206
10600
|
availableModels: Model[],
|
|
@@ -10297,6 +10691,9 @@ export class AgentSession {
|
|
|
10297
10691
|
// via resolveCompactionEffort so unsupported-effort models
|
|
10298
10692
|
// (xai-oauth/grok-build) don't trip requireSupportedEffort.
|
|
10299
10693
|
thinkingLevel: this.thinkingLevel,
|
|
10694
|
+
tools: this.agent.state.tools,
|
|
10695
|
+
sessionId: this.sessionId,
|
|
10696
|
+
promptCacheKey: this.sessionId,
|
|
10300
10697
|
},
|
|
10301
10698
|
);
|
|
10302
10699
|
} catch (error) {
|
|
@@ -10616,15 +11013,21 @@ export class AgentSession {
|
|
|
10616
11013
|
|
|
10617
11014
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
10618
11015
|
// 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.
|
|
11016
|
+
// so a handoff request on the existing context is still viable.
|
|
10622
11017
|
let action: "context-full" | "handoff" | "snapcompact" =
|
|
10623
11018
|
compactionSettings.strategy === "snapcompact"
|
|
10624
11019
|
? "snapcompact"
|
|
10625
11020
|
: compactionSettings.strategy === "handoff" && reason !== "overflow" && !suppressHandoff
|
|
10626
11021
|
? "handoff"
|
|
10627
11022
|
: "context-full";
|
|
11023
|
+
if (action === "snapcompact" && this.model && !this.model.input.includes("image")) {
|
|
11024
|
+
this.emitNotice(
|
|
11025
|
+
"warning",
|
|
11026
|
+
`snapcompact needs a vision-capable active model (${this.model.id} is text-only); using context-full auto-compaction instead.`,
|
|
11027
|
+
"compaction",
|
|
11028
|
+
);
|
|
11029
|
+
action = "context-full";
|
|
11030
|
+
}
|
|
10628
11031
|
// Abort any older auto-compaction before installing this run's controller.
|
|
10629
11032
|
this.#autoCompactionAbortController?.abort();
|
|
10630
11033
|
const autoCompactionAbortController = new AbortController();
|
|
@@ -10703,7 +11106,11 @@ export class AgentSession {
|
|
|
10703
11106
|
|
|
10704
11107
|
const pathEntries = this.sessionManager.getBranch();
|
|
10705
11108
|
|
|
10706
|
-
const
|
|
11109
|
+
const autoCompactionCandidates = await this.#runnableCompactionCandidates(
|
|
11110
|
+
this.#getCompactionModelCandidates(availableModels),
|
|
11111
|
+
this.sessionId,
|
|
11112
|
+
);
|
|
11113
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, autoCompactionCandidates);
|
|
10707
11114
|
if (!preparation) {
|
|
10708
11115
|
await this.#emitSessionEvent({
|
|
10709
11116
|
type: "auto_compaction_end",
|
|
@@ -10766,21 +11173,15 @@ export class AgentSession {
|
|
|
10766
11173
|
// + a summary message carrying the imaged archive at FRAME_TOKEN_ESTIMATE
|
|
10767
11174
|
// per frame; #computeSnapcompactMaxFrames sizes the frame cap from the
|
|
10768
11175
|
// live window so we don't run snapcompact just to overflow every threshold
|
|
10769
|
-
// tick. Any local blocker
|
|
10770
|
-
//
|
|
11176
|
+
// tick. Any local blocker (non-ASCII transcript, kept-history too large,
|
|
11177
|
+
// post-render overflow) downgrades auto maintenance to a context-full LLM
|
|
11178
|
+
// summary instead of wedging the session (#3659) — auto runs the default
|
|
11179
|
+
// strategy on the user's behalf, so a fallback that lets the session keep
|
|
11180
|
+
// running is the right behavior. Manual `/compact snapcompact` keeps the
|
|
11181
|
+
// local-only contract (#3599): the user explicitly picked it.
|
|
10771
11182
|
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
|
11183
|
+
let snapcompactBlocker: string | undefined;
|
|
10772
11184
|
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
11185
|
const text = snapcompact.serializeConversation(
|
|
10785
11186
|
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
|
10786
11187
|
);
|
|
@@ -10791,55 +11192,46 @@ export class AgentSession {
|
|
|
10791
11192
|
model: this.model?.id,
|
|
10792
11193
|
unrenderableRatio: renderScan.unrenderableRatio,
|
|
10793
11194
|
});
|
|
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", {
|
|
11195
|
+
snapcompactBlocker = `snapcompact disabled: high non-ASCII rate detected (${percent}%); using context-full auto-compaction instead.`;
|
|
11196
|
+
} else {
|
|
11197
|
+
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
|
|
11198
|
+
if (maxFrames < 1) {
|
|
11199
|
+
logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
|
|
10831
11200
|
model: this.model?.id,
|
|
10832
|
-
projected,
|
|
10833
|
-
budget,
|
|
10834
11201
|
});
|
|
10835
|
-
|
|
10836
|
-
"
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
11202
|
+
snapcompactBlocker =
|
|
11203
|
+
"snapcompact: kept history alone exceeds the context budget; using context-full auto-compaction instead.";
|
|
11204
|
+
} else {
|
|
11205
|
+
snapcompactResult = await snapcompact.compact(preparation, {
|
|
11206
|
+
convertToLlm,
|
|
11207
|
+
model: this.model,
|
|
11208
|
+
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
11209
|
+
maxFrames,
|
|
11210
|
+
});
|
|
11211
|
+
if (snapcompactResult) {
|
|
11212
|
+
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
11213
|
+
const budget =
|
|
11214
|
+
ctxWindow > 0
|
|
11215
|
+
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
|
11216
|
+
: Number.POSITIVE_INFINITY;
|
|
11217
|
+
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
|
11218
|
+
if (projected > budget) {
|
|
11219
|
+
logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
|
|
11220
|
+
model: this.model?.id,
|
|
11221
|
+
projected,
|
|
11222
|
+
budget,
|
|
11223
|
+
});
|
|
11224
|
+
snapcompactBlocker =
|
|
11225
|
+
"snapcompact could not bring the context under the limit; using context-full auto-compaction instead.";
|
|
11226
|
+
snapcompactResult = undefined;
|
|
11227
|
+
}
|
|
11228
|
+
}
|
|
10841
11229
|
}
|
|
10842
11230
|
}
|
|
11231
|
+
if (snapcompactBlocker) {
|
|
11232
|
+
this.emitNotice("warning", snapcompactBlocker, "compaction");
|
|
11233
|
+
action = "context-full";
|
|
11234
|
+
}
|
|
10843
11235
|
}
|
|
10844
11236
|
|
|
10845
11237
|
if (compactionPrep.kind === "fromHook") {
|
|
@@ -10891,6 +11283,9 @@ export class AgentSession {
|
|
|
10891
11283
|
// site. Clamped per-model inside compact() via
|
|
10892
11284
|
// resolveCompactionEffort.
|
|
10893
11285
|
thinkingLevel: this.thinkingLevel,
|
|
11286
|
+
tools: this.agent.state.tools,
|
|
11287
|
+
sessionId: this.sessionId,
|
|
11288
|
+
promptCacheKey: this.sessionId,
|
|
10894
11289
|
},
|
|
10895
11290
|
);
|
|
10896
11291
|
break;
|
|
@@ -11007,7 +11402,7 @@ export class AgentSession {
|
|
|
11007
11402
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
11008
11403
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
11009
11404
|
this.#planReferenceSent = false;
|
|
11010
|
-
this.#
|
|
11405
|
+
this.#resetAllAdvisorRuntimes();
|
|
11011
11406
|
this.#syncTodoPhasesFromBranch();
|
|
11012
11407
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
11013
11408
|
|
|
@@ -12853,7 +13248,7 @@ export class AgentSession {
|
|
|
12853
13248
|
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
12854
13249
|
this.agent.serviceTier = previousServiceTier;
|
|
12855
13250
|
this.#syncTodoPhasesFromBranch();
|
|
12856
|
-
this.#
|
|
13251
|
+
this.#resetAllAdvisorRuntimes();
|
|
12857
13252
|
this.#reconnectToAgent();
|
|
12858
13253
|
if (restoreMcpError) {
|
|
12859
13254
|
throw restoreMcpError;
|
|
@@ -13255,10 +13650,11 @@ export class AgentSession {
|
|
|
13255
13650
|
let totalInput = 0;
|
|
13256
13651
|
let totalOutput = 0;
|
|
13257
13652
|
let totalCacheRead = 0;
|
|
13653
|
+
let totalReasoning = 0;
|
|
13258
13654
|
let totalCacheWrite = 0;
|
|
13259
13655
|
let totalCost = 0;
|
|
13260
|
-
|
|
13261
13656
|
let totalPremiumRequests = 0;
|
|
13657
|
+
|
|
13262
13658
|
const getTaskToolUsage = (details: unknown): Usage | undefined => {
|
|
13263
13659
|
if (!details || typeof details !== "object") return undefined;
|
|
13264
13660
|
const record = details as Record<string, unknown>;
|
|
@@ -13273,6 +13669,7 @@ export class AgentSession {
|
|
|
13273
13669
|
toolCalls += assistantMsg.content.filter(c => c.type === "toolCall").length;
|
|
13274
13670
|
totalInput += assistantMsg.usage.input;
|
|
13275
13671
|
totalOutput += assistantMsg.usage.output;
|
|
13672
|
+
totalReasoning += assistantMsg.usage.reasoningTokens ?? 0;
|
|
13276
13673
|
totalCacheRead += assistantMsg.usage.cacheRead;
|
|
13277
13674
|
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
|
13278
13675
|
totalPremiumRequests += assistantMsg.usage.premiumRequests ?? 0;
|
|
@@ -13284,6 +13681,7 @@ export class AgentSession {
|
|
|
13284
13681
|
if (usage) {
|
|
13285
13682
|
totalInput += usage.input;
|
|
13286
13683
|
totalOutput += usage.output;
|
|
13684
|
+
totalReasoning += usage.reasoningTokens ?? 0;
|
|
13287
13685
|
totalCacheRead += usage.cacheRead;
|
|
13288
13686
|
totalCacheWrite += usage.cacheWrite;
|
|
13289
13687
|
totalPremiumRequests += usage.premiumRequests ?? 0;
|
|
@@ -13303,6 +13701,7 @@ export class AgentSession {
|
|
|
13303
13701
|
tokens: {
|
|
13304
13702
|
input: totalInput,
|
|
13305
13703
|
output: totalOutput,
|
|
13704
|
+
reasoning: totalReasoning,
|
|
13306
13705
|
cacheRead: totalCacheRead,
|
|
13307
13706
|
cacheWrite: totalCacheWrite,
|
|
13308
13707
|
total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
|
|
@@ -13835,6 +14234,23 @@ export class AgentSession {
|
|
|
13835
14234
|
return this.setAdvisorEnabled(!this.#advisorEnabled);
|
|
13836
14235
|
}
|
|
13837
14236
|
|
|
14237
|
+
/**
|
|
14238
|
+
* Replace the live advisor roster from an edited `WATCHDOG.yml` (the `/advisor
|
|
14239
|
+
* configure` save path). Swaps the configs + shared baseline, then rebuilds the
|
|
14240
|
+
* runtimes in place so the change applies without a restart. When the advisor is
|
|
14241
|
+
* disabled the new configs are simply stored for the next enable.
|
|
14242
|
+
*
|
|
14243
|
+
* @returns the number of advisors active after the rebuild.
|
|
14244
|
+
*/
|
|
14245
|
+
applyAdvisorConfigs(advisors: AdvisorConfig[], sharedInstructions: string | undefined): number {
|
|
14246
|
+
this.#advisorConfigs = advisors;
|
|
14247
|
+
this.#advisorSharedInstructions = sharedInstructions;
|
|
14248
|
+
if (!this.#advisorEnabled) return 0;
|
|
14249
|
+
this.#stopAdvisorRuntime();
|
|
14250
|
+
this.#buildAdvisorRuntime(true);
|
|
14251
|
+
return this.#advisors.length;
|
|
14252
|
+
}
|
|
14253
|
+
|
|
13838
14254
|
/**
|
|
13839
14255
|
* Whether the advisor setting is enabled for this session.
|
|
13840
14256
|
*/
|
|
@@ -13849,7 +14265,17 @@ export class AgentSession {
|
|
|
13849
14265
|
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
|
13850
14266
|
*/
|
|
13851
14267
|
isAdvisorActive(): boolean {
|
|
13852
|
-
return this.#
|
|
14268
|
+
return this.#advisors.length > 0;
|
|
14269
|
+
}
|
|
14270
|
+
|
|
14271
|
+
/**
|
|
14272
|
+
* The names of the tools available to advisors this session (the pool a
|
|
14273
|
+
* `/advisor configure` editor lists). The advisor is a full agent, so this is the
|
|
14274
|
+
* full built tool set; a tool whose optional factory returns null (e.g. lsp with
|
|
14275
|
+
* no servers) is absent.
|
|
14276
|
+
*/
|
|
14277
|
+
getAdvisorAvailableToolNames(): string[] {
|
|
14278
|
+
return (this.#advisorTools ?? []).map(tool => tool.name);
|
|
13853
14279
|
}
|
|
13854
14280
|
|
|
13855
14281
|
/**
|
|
@@ -13860,7 +14286,7 @@ export class AgentSession {
|
|
|
13860
14286
|
* (`streamFn`, `promptCacheKey`, `providerSessionState`, ...).
|
|
13861
14287
|
*/
|
|
13862
14288
|
getAdvisorAgent(): Agent | undefined {
|
|
13863
|
-
return this.#
|
|
14289
|
+
return this.#advisors[0]?.agent;
|
|
13864
14290
|
}
|
|
13865
14291
|
|
|
13866
14292
|
/**
|
|
@@ -13868,23 +14294,59 @@ export class AgentSession {
|
|
|
13868
14294
|
*/
|
|
13869
14295
|
getAdvisorStats(): AdvisorStats {
|
|
13870
14296
|
const configured = this.#advisorEnabled;
|
|
13871
|
-
const
|
|
13872
|
-
if (
|
|
14297
|
+
const advisors = this.#advisors.map(a => this.#computeAdvisorStat(a));
|
|
14298
|
+
if (advisors.length === 0) {
|
|
13873
14299
|
return {
|
|
13874
14300
|
configured,
|
|
13875
14301
|
active: false,
|
|
13876
14302
|
contextWindow: 0,
|
|
13877
14303
|
contextTokens: 0,
|
|
13878
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14304
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
13879
14305
|
cost: 0,
|
|
13880
14306
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
14307
|
+
advisors: [],
|
|
13881
14308
|
};
|
|
13882
14309
|
}
|
|
13883
|
-
const
|
|
13884
|
-
const messages =
|
|
14310
|
+
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
14311
|
+
const messages = { user: 0, assistant: 0, total: 0 };
|
|
14312
|
+
let cost = 0;
|
|
14313
|
+
let contextTokens = 0;
|
|
14314
|
+
for (const a of advisors) {
|
|
14315
|
+
tokens.input += a.tokens.input;
|
|
14316
|
+
tokens.output += a.tokens.output;
|
|
14317
|
+
tokens.reasoning += a.tokens.reasoning;
|
|
14318
|
+
tokens.cacheRead += a.tokens.cacheRead;
|
|
14319
|
+
tokens.cacheWrite += a.tokens.cacheWrite;
|
|
14320
|
+
tokens.total += a.tokens.total;
|
|
14321
|
+
messages.user += a.messages.user;
|
|
14322
|
+
messages.assistant += a.messages.assistant;
|
|
14323
|
+
messages.total += a.messages.total;
|
|
14324
|
+
cost += a.cost;
|
|
14325
|
+
contextTokens += a.contextTokens;
|
|
14326
|
+
}
|
|
14327
|
+
// Single-advisor displays read the top-level model/window directly; surface the
|
|
14328
|
+
// first advisor's so the legacy status line stays byte-identical.
|
|
14329
|
+
return {
|
|
14330
|
+
configured,
|
|
14331
|
+
active: true,
|
|
14332
|
+
model: advisors[0].model,
|
|
14333
|
+
contextWindow: advisors[0].contextWindow,
|
|
14334
|
+
contextTokens,
|
|
14335
|
+
tokens,
|
|
14336
|
+
cost,
|
|
14337
|
+
messages,
|
|
14338
|
+
advisors,
|
|
14339
|
+
};
|
|
14340
|
+
}
|
|
14341
|
+
|
|
14342
|
+
/** Compute one advisor's stats slice (tokens, cost, context, message counts). */
|
|
14343
|
+
#computeAdvisorStat(advisor: ActiveAdvisor): PerAdvisorStat {
|
|
14344
|
+
const model = advisor.agent.state.model;
|
|
14345
|
+
const messages = advisor.agent.state.messages;
|
|
13885
14346
|
const contextTokens = this.#estimateAdvisorContextTokens(messages);
|
|
13886
14347
|
let input = 0;
|
|
13887
14348
|
let output = 0;
|
|
14349
|
+
let reasoning = 0;
|
|
13888
14350
|
let cacheRead = 0;
|
|
13889
14351
|
let cacheWrite = 0;
|
|
13890
14352
|
let cost = 0;
|
|
@@ -13897,24 +14359,18 @@ export class AgentSession {
|
|
|
13897
14359
|
const assistantMsg = message as AssistantMessage;
|
|
13898
14360
|
input += assistantMsg.usage.input;
|
|
13899
14361
|
output += assistantMsg.usage.output;
|
|
14362
|
+
reasoning += assistantMsg.usage.reasoningTokens ?? 0;
|
|
13900
14363
|
cacheRead += assistantMsg.usage.cacheRead;
|
|
13901
14364
|
cacheWrite += assistantMsg.usage.cacheWrite;
|
|
13902
14365
|
cost += assistantMsg.usage.cost.total;
|
|
13903
14366
|
}
|
|
13904
14367
|
}
|
|
13905
14368
|
return {
|
|
13906
|
-
|
|
13907
|
-
active: true,
|
|
14369
|
+
name: advisor.name,
|
|
13908
14370
|
model,
|
|
13909
14371
|
contextWindow: model.contextWindow ?? 0,
|
|
13910
14372
|
contextTokens,
|
|
13911
|
-
tokens: {
|
|
13912
|
-
input,
|
|
13913
|
-
output,
|
|
13914
|
-
cacheRead,
|
|
13915
|
-
cacheWrite,
|
|
13916
|
-
total: input + output + cacheRead + cacheWrite,
|
|
13917
|
-
},
|
|
14373
|
+
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: input + output + cacheRead + cacheWrite },
|
|
13918
14374
|
cost,
|
|
13919
14375
|
messages: { user, assistant, total: messages.length },
|
|
13920
14376
|
};
|
|
@@ -13930,19 +14386,30 @@ export class AgentSession {
|
|
|
13930
14386
|
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
13931
14387
|
: "Advisor is disabled.";
|
|
13932
14388
|
}
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
`${
|
|
13940
|
-
`${
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
14389
|
+
if (stats.advisors.length <= 1) {
|
|
14390
|
+
const s = stats.advisors[0];
|
|
14391
|
+
const contextLine =
|
|
14392
|
+
s.contextWindow > 0
|
|
14393
|
+
? `Context: ${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} tokens (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
14394
|
+
: `Context: ${s.contextTokens.toLocaleString()} tokens`;
|
|
14395
|
+
const spendParts = [`${s.tokens.input.toLocaleString()} input`, `${s.tokens.output.toLocaleString()} output`];
|
|
14396
|
+
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
14397
|
+
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
14398
|
+
const spendLine = `Spend: ${spendParts.join(", ")}, $${s.cost.toFixed(4)}`;
|
|
14399
|
+
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
14400
|
+
}
|
|
14401
|
+
const lines = [`Advisors enabled (${stats.advisors.length}):`];
|
|
14402
|
+
for (const s of stats.advisors) {
|
|
14403
|
+
const ctx =
|
|
14404
|
+
s.contextWindow > 0
|
|
14405
|
+
? `${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
14406
|
+
: `${s.contextTokens.toLocaleString()}`;
|
|
14407
|
+
lines.push(` • ${s.name} (${s.model.provider}/${s.model.id}) — context ${ctx} tokens, $${s.cost.toFixed(4)}`);
|
|
14408
|
+
}
|
|
14409
|
+
lines.push(
|
|
14410
|
+
`Totals: ${stats.tokens.input.toLocaleString()} input, ${stats.tokens.output.toLocaleString()} output, $${stats.cost.toFixed(4)}.`,
|
|
14411
|
+
);
|
|
14412
|
+
return lines.join("\n");
|
|
13946
14413
|
}
|
|
13947
14414
|
|
|
13948
14415
|
/**
|
|
@@ -13986,18 +14453,21 @@ export class AgentSession {
|
|
|
13986
14453
|
* {@link formatSessionAsText}. Returns null when no advisor is active.
|
|
13987
14454
|
*/
|
|
13988
14455
|
formatAdvisorHistoryAsText(options?: { compact?: boolean }): string | null {
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14456
|
+
if (this.#advisors.length === 0) return null;
|
|
14457
|
+
const dump = (a: ActiveAdvisor): string =>
|
|
14458
|
+
options?.compact
|
|
14459
|
+
? formatSessionHistoryMarkdown(a.agent.state.messages)
|
|
14460
|
+
: formatSessionDumpText({
|
|
14461
|
+
messages: a.agent.state.messages,
|
|
14462
|
+
systemPrompt: a.agent.state.systemPrompt,
|
|
14463
|
+
model: a.agent.state.model,
|
|
14464
|
+
thinkingLevel: a.agent.state.thinkingLevel,
|
|
14465
|
+
tools: a.agent.state.tools,
|
|
14466
|
+
});
|
|
14467
|
+
if (this.#advisors.length === 1) return dump(this.#advisors[0]);
|
|
14468
|
+
return this.#advisors
|
|
14469
|
+
.map(a => `### Advisor: ${a.name} (${a.agent.state.model.provider}/${a.agent.state.model.id})\n\n${dump(a)}`)
|
|
14470
|
+
.join("\n\n");
|
|
14001
14471
|
}
|
|
14002
14472
|
|
|
14003
14473
|
// =========================================================================
|