@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/dist/cli.js +3621 -3579
- 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/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -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 +438 -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 +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- 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/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- 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/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- 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/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- 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/assistant-message.ts +5 -5
- 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 +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- 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/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- 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/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type CompactionSummaryMessage,
|
|
37
37
|
countTokens,
|
|
38
38
|
resolveTelemetry,
|
|
39
|
+
type StreamFn,
|
|
39
40
|
ThinkingLevel,
|
|
40
41
|
type ToolChoiceDirective,
|
|
41
42
|
} from "@oh-my-pi/pi-agent-core";
|
|
@@ -80,6 +81,7 @@ import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/to
|
|
|
80
81
|
import type {
|
|
81
82
|
AssistantMessage,
|
|
82
83
|
AssistantMessageEvent,
|
|
84
|
+
Context,
|
|
83
85
|
ImageContent,
|
|
84
86
|
Message,
|
|
85
87
|
MessageAttribution,
|
|
@@ -102,18 +104,13 @@ import {
|
|
|
102
104
|
clearAnthropicFastModeFallback,
|
|
103
105
|
deriveClaudeDeviceId,
|
|
104
106
|
Effort,
|
|
105
|
-
isContextOverflow,
|
|
106
|
-
isUsageLimitError,
|
|
107
107
|
parseRateLimitReason,
|
|
108
108
|
resolveServiceTier,
|
|
109
109
|
streamSimple,
|
|
110
110
|
} from "@oh-my-pi/pi-ai";
|
|
111
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
111
112
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
112
|
-
import {
|
|
113
|
-
GeminiHeaderRunDetector,
|
|
114
|
-
isGeminiThinkingModel,
|
|
115
|
-
THINKING_LOOP_ERROR_MARKER,
|
|
116
|
-
} from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
113
|
+
import { GeminiHeaderRunDetector, isGeminiThinkingModel } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
117
114
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
118
115
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
119
116
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
@@ -125,7 +122,6 @@ import {
|
|
|
125
122
|
getInstallId,
|
|
126
123
|
isBunTestRuntime,
|
|
127
124
|
isEnoent,
|
|
128
|
-
isUnexpectedSocketCloseMessage,
|
|
129
125
|
logger,
|
|
130
126
|
prompt,
|
|
131
127
|
relativePathWithinRoot,
|
|
@@ -134,18 +130,22 @@ import {
|
|
|
134
130
|
} from "@oh-my-pi/pi-utils";
|
|
135
131
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
136
132
|
import {
|
|
133
|
+
ADVISOR_DEFAULT_TOOL_NAMES,
|
|
137
134
|
AdviseTool,
|
|
138
135
|
type AdvisorAgent,
|
|
136
|
+
type AdvisorConfig,
|
|
139
137
|
AdvisorEmissionGuard,
|
|
140
138
|
type AdvisorMessageDetails,
|
|
141
139
|
type AdvisorNote,
|
|
142
140
|
AdvisorRuntime,
|
|
143
141
|
type AdvisorSeverity,
|
|
144
142
|
AdvisorTranscriptRecorder,
|
|
143
|
+
advisorTranscriptFilename,
|
|
145
144
|
formatAdvisorBatchContent,
|
|
146
145
|
isAdvisorInterruptImmuneTurnActive,
|
|
147
146
|
isInterruptingSeverity,
|
|
148
147
|
resolveAdvisorDeliveryChannel,
|
|
148
|
+
slugifyAdvisorName,
|
|
149
149
|
} from "../advisor";
|
|
150
150
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
151
151
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
@@ -236,6 +236,7 @@ import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text
|
|
|
236
236
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
237
237
|
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
238
238
|
import geminiToolReminderTemplate from "../prompts/system/gemini-tool-call-reminder.md" with { type: "text" };
|
|
239
|
+
import interruptedThinkingTemplate from "../prompts/system/interrupted-thinking.md" with { type: "text" };
|
|
239
240
|
import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
|
|
240
241
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
241
242
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
@@ -265,6 +266,7 @@ import {
|
|
|
265
266
|
shouldDisableReasoning,
|
|
266
267
|
toReasoningEffort,
|
|
267
268
|
} from "../thinking";
|
|
269
|
+
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/text";
|
|
268
270
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
269
271
|
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
270
272
|
import {
|
|
@@ -292,6 +294,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
292
294
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
293
295
|
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
294
296
|
import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallback";
|
|
297
|
+
import { generateSessionTitle } from "../utils/title-generator";
|
|
295
298
|
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
|
296
299
|
import type { AuthStorage } from "./auth-storage";
|
|
297
300
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
@@ -307,7 +310,10 @@ import {
|
|
|
307
310
|
type BashExecutionMessage,
|
|
308
311
|
type CustomMessage,
|
|
309
312
|
convertToLlm,
|
|
310
|
-
|
|
313
|
+
demoteInterruptedThinking,
|
|
314
|
+
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
315
|
+
type InterruptedThinkingDetails,
|
|
316
|
+
isUserInterruptAbort,
|
|
311
317
|
type PythonExecutionMessage,
|
|
312
318
|
readQueueChipText,
|
|
313
319
|
SILENT_ABORT_MARKER,
|
|
@@ -324,6 +330,7 @@ import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
|
324
330
|
import { cleanupEmptyMoveSession, type SessionManager } from "./session-manager";
|
|
325
331
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
326
332
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
333
|
+
import { planTurnPersistence, sameMessageContent, sessionMessagePersistenceKey } from "./turn-persistence";
|
|
327
334
|
import { classifyUnexpectedStop, isUnexpectedStopCandidate } from "./unexpected-stop-classifier";
|
|
328
335
|
import { YieldQueue } from "./yield-queue";
|
|
329
336
|
|
|
@@ -369,7 +376,14 @@ export type AgentSessionEvent =
|
|
|
369
376
|
/** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
|
|
370
377
|
skipped?: boolean;
|
|
371
378
|
}
|
|
372
|
-
| {
|
|
379
|
+
| {
|
|
380
|
+
type: "auto_retry_start";
|
|
381
|
+
attempt: number;
|
|
382
|
+
maxAttempts: number;
|
|
383
|
+
delayMs: number;
|
|
384
|
+
errorMessage: string;
|
|
385
|
+
errorId?: number;
|
|
386
|
+
}
|
|
373
387
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
374
388
|
| { type: "retry_fallback_applied"; from: string; to: string; role: string }
|
|
375
389
|
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
@@ -512,6 +526,21 @@ export interface AgentSessionConfig {
|
|
|
512
526
|
builtInToolNames?: Iterable<string>;
|
|
513
527
|
/** Current session pre-LLM message transform pipeline */
|
|
514
528
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
529
|
+
/**
|
|
530
|
+
* Per-request transform applied after `convertToLlm` and before the
|
|
531
|
+
* provider call. Used for snapcompact, secret obfuscation, and image
|
|
532
|
+
* clamping. When supplied via {@link createAgentSession}, the advisor agent
|
|
533
|
+
* inherits this so its requests undergo the same shaping as the main turn.
|
|
534
|
+
*/
|
|
535
|
+
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
536
|
+
/**
|
|
537
|
+
* Stream wrapper passed to the advisor agent so its requests apply the
|
|
538
|
+
* session's `providers.openrouterVariant`, `providers.antigravityEndpoint`,
|
|
539
|
+
* `providers.maxInFlightRequests`, and `model.loopGuard.*` settings —
|
|
540
|
+
* keeping OpenRouter sticky-routing / response caching consistent with the
|
|
541
|
+
* main agent. Defaults to plain `streamSimple` when omitted.
|
|
542
|
+
*/
|
|
543
|
+
advisorStreamFn?: StreamFn;
|
|
515
544
|
/** Provider payload hook used by the active session request path */
|
|
516
545
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
517
546
|
/** Provider response hook used by the active session request path */
|
|
@@ -581,20 +610,29 @@ export interface AgentSessionConfig {
|
|
|
581
610
|
*/
|
|
582
611
|
providerSessionId?: string;
|
|
583
612
|
/**
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* advisor's
|
|
587
|
-
*
|
|
613
|
+
* Full advisor toolset, pre-built in `createAgentSession` against a distinct,
|
|
614
|
+
* advisor-scoped `ToolSession` (its own `-advisor` session/agent id) so the
|
|
615
|
+
* advisor's tool state stays isolated from the primary. The advisor is a full
|
|
616
|
+
* agent; its config `tools` selects a subset (default read/grep/glob). Undefined
|
|
617
|
+
* when the advisor is disabled.
|
|
588
618
|
*/
|
|
589
|
-
|
|
619
|
+
advisorTools?: AgentTool[];
|
|
590
620
|
/** Preloaded watchdog prompt content for the advisor. */
|
|
591
621
|
advisorWatchdogPrompt?: string;
|
|
622
|
+
/** Preloaded YAML top-level `instructions` shared baseline, kept separate from
|
|
623
|
+
* `advisorWatchdogPrompt` so `/advisor configure` can swap it live. */
|
|
624
|
+
advisorSharedInstructions?: string;
|
|
592
625
|
/**
|
|
593
626
|
* Preloaded project context files (AGENTS.md, etc.) rendered as a system-prompt
|
|
594
627
|
* block for the advisor — the same standing instructions the primary agent
|
|
595
628
|
* receives, so the reviewer holds the agent to them.
|
|
596
629
|
*/
|
|
597
630
|
advisorContextPrompt?: string;
|
|
631
|
+
/**
|
|
632
|
+
* Advisors discovered from `WATCHDOG.yml`. Empty/undefined runs a single
|
|
633
|
+
* legacy advisor on the `advisor` role (byte-for-byte the pre-config path).
|
|
634
|
+
*/
|
|
635
|
+
advisorConfigs?: AdvisorConfig[];
|
|
598
636
|
/**
|
|
599
637
|
* Strip tool descriptions from provider-bound tool specs on side requests
|
|
600
638
|
* (handoff). Must match the session-start value used to build the system
|
|
@@ -699,6 +737,7 @@ export interface SessionStats {
|
|
|
699
737
|
tokens: {
|
|
700
738
|
input: number;
|
|
701
739
|
output: number;
|
|
740
|
+
reasoning: number;
|
|
702
741
|
cacheRead: number;
|
|
703
742
|
cacheWrite: number;
|
|
704
743
|
total: number;
|
|
@@ -717,6 +756,7 @@ export interface AdvisorStats {
|
|
|
717
756
|
tokens: {
|
|
718
757
|
input: number;
|
|
719
758
|
output: number;
|
|
759
|
+
reasoning: number;
|
|
720
760
|
cacheRead: number;
|
|
721
761
|
cacheWrite: number;
|
|
722
762
|
total: number;
|
|
@@ -727,6 +767,43 @@ export interface AdvisorStats {
|
|
|
727
767
|
assistant: number;
|
|
728
768
|
total: number;
|
|
729
769
|
};
|
|
770
|
+
/** Per-advisor breakdown; one entry per active advisor (single-advisor sessions have one). */
|
|
771
|
+
advisors: PerAdvisorStat[];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** One advisor's slice of {@link AdvisorStats}, surfaced for the multi-advisor status panel. */
|
|
775
|
+
export interface PerAdvisorStat {
|
|
776
|
+
name: string;
|
|
777
|
+
model: Model;
|
|
778
|
+
contextWindow: number;
|
|
779
|
+
contextTokens: number;
|
|
780
|
+
tokens: AdvisorStats["tokens"];
|
|
781
|
+
cost: number;
|
|
782
|
+
messages: AdvisorStats["messages"];
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* One live advisor instance: its own agent/runtime/tools/recorder plus a
|
|
787
|
+
* per-advisor emission guard and identity. The session holds an array of these;
|
|
788
|
+
* primary-scoped state (turn counters, interrupt latches, the shared yield
|
|
789
|
+
* channel) stays on the session.
|
|
790
|
+
*/
|
|
791
|
+
interface ActiveAdvisor {
|
|
792
|
+
/** Display name from config ("default" for the legacy no-YAML advisor). */
|
|
793
|
+
name: string;
|
|
794
|
+
/** Slug for the transcript filename/session id; "" → `__advisor.jsonl`. */
|
|
795
|
+
slug: string;
|
|
796
|
+
agent: Agent;
|
|
797
|
+
runtime: AdvisorRuntime;
|
|
798
|
+
adviseTool: AdviseTool;
|
|
799
|
+
emissionGuard: AdvisorEmissionGuard;
|
|
800
|
+
recorder: AdvisorTranscriptRecorder;
|
|
801
|
+
/** Latest recorder close, awaited by dispose() so the final turn lands on disk. */
|
|
802
|
+
recorderClosed: Promise<void>;
|
|
803
|
+
/** Unsubscribe for the advisor agent's event stream feeding the recorder. */
|
|
804
|
+
agentUnsubscribe?: () => void;
|
|
805
|
+
model: Model;
|
|
806
|
+
thinkingLevel: ThinkingLevel;
|
|
730
807
|
}
|
|
731
808
|
|
|
732
809
|
export interface FreshSessionResult {
|
|
@@ -1171,6 +1248,60 @@ type MessageEndPersistenceSlot = {
|
|
|
1171
1248
|
release: () => void;
|
|
1172
1249
|
};
|
|
1173
1250
|
|
|
1251
|
+
const REPLAN_TITLE_CONTEXT_TURN_LIMIT = 6;
|
|
1252
|
+
|
|
1253
|
+
type SessionTitleSource = "auto" | "user";
|
|
1254
|
+
type SessionNameTrigger = "replan";
|
|
1255
|
+
type SetSessionNameWithTrigger = (
|
|
1256
|
+
name: string,
|
|
1257
|
+
source?: SessionTitleSource,
|
|
1258
|
+
trigger?: SessionNameTrigger,
|
|
1259
|
+
) => Promise<boolean>;
|
|
1260
|
+
|
|
1261
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1262
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function textFromContent(content: unknown): string {
|
|
1266
|
+
if (typeof content === "string") return content.trim();
|
|
1267
|
+
if (!Array.isArray(content)) return "";
|
|
1268
|
+
const parts: string[] = [];
|
|
1269
|
+
for (const block of content) {
|
|
1270
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") continue;
|
|
1271
|
+
const text = block.text.trim();
|
|
1272
|
+
if (text) parts.push(text);
|
|
1273
|
+
}
|
|
1274
|
+
return parts.join("\n\n");
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function thinkingFromContent(content: unknown): string {
|
|
1278
|
+
if (!Array.isArray(content)) return "";
|
|
1279
|
+
const parts: string[] = [];
|
|
1280
|
+
for (const block of content) {
|
|
1281
|
+
if (!isRecord(block) || block.type !== "thinking" || typeof block.thinking !== "string") continue;
|
|
1282
|
+
const thinking = block.thinking.trim();
|
|
1283
|
+
if (thinking) parts.push(thinking);
|
|
1284
|
+
}
|
|
1285
|
+
return parts.join("\n\n");
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function toolCallOpFromMessage(message: AgentMessage, toolCallId: string): string | undefined {
|
|
1289
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return undefined;
|
|
1290
|
+
for (const block of message.content) {
|
|
1291
|
+
if (!isRecord(block) || block.type !== "toolCall" || block.id !== toolCallId) continue;
|
|
1292
|
+
return isRecord(block.arguments) ? getStringProperty(block.arguments, "op") : undefined;
|
|
1293
|
+
}
|
|
1294
|
+
return undefined;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function titleConversationTurnFromMessage(message: AgentMessage): TitleConversationTurn | undefined {
|
|
1298
|
+
if (message.role !== "user" && message.role !== "assistant") return undefined;
|
|
1299
|
+
const text = textFromContent(message.content);
|
|
1300
|
+
const thinking = message.role === "assistant" ? thinkingFromContent(message.content) : undefined;
|
|
1301
|
+
if (!text && !thinking) return undefined;
|
|
1302
|
+
return { role: message.role, ...(text ? { text } : {}), ...(thinking ? { thinking } : {}) };
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1174
1305
|
export class AgentSession {
|
|
1175
1306
|
readonly agent: Agent;
|
|
1176
1307
|
readonly sessionManager: SessionManager;
|
|
@@ -1211,29 +1342,21 @@ export class AgentSession {
|
|
|
1211
1342
|
#advisorAutoResumeSuppressed = false;
|
|
1212
1343
|
#advisorPrimaryTurnsCompleted = 0;
|
|
1213
1344
|
#advisorInterruptImmuneTurnStart: number | undefined;
|
|
1214
|
-
/** Dedupe + per-update rate-limit + content-free-phrase filter applied to
|
|
1215
|
-
* every accepted advisor `advise()` call. Owned by the session because the
|
|
1216
|
-
* session is what routes accepted notes back to the primary transcript.
|
|
1217
|
-
* Reset on advisor reset (compaction, session switch, `/new`). */
|
|
1218
|
-
readonly #advisorEmissionGuard = new AdvisorEmissionGuard();
|
|
1219
1345
|
#planModeState: PlanModeState | undefined;
|
|
1220
1346
|
#goalModeState: GoalModeState | undefined;
|
|
1221
1347
|
#goalRuntime: GoalRuntime;
|
|
1222
|
-
#advisorRuntime?: AdvisorRuntime;
|
|
1223
1348
|
#advisorEnabled = false;
|
|
1224
|
-
|
|
1225
|
-
#advisorAgent?: Agent;
|
|
1226
|
-
#advisorAdviseTool?: AdviseTool;
|
|
1227
|
-
#advisorReadOnlyTools?: AgentTool[];
|
|
1349
|
+
#advisorTools?: AgentTool[];
|
|
1228
1350
|
#advisorWatchdogPrompt?: string;
|
|
1351
|
+
#advisorSharedInstructions?: string;
|
|
1229
1352
|
#advisorContextPrompt?: string;
|
|
1230
1353
|
#advisorYieldQueueUnsubscribe?: () => void;
|
|
1231
|
-
/**
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1354
|
+
/** Live advisors. Empty when no advisor is active. */
|
|
1355
|
+
#advisors: ActiveAdvisor[] = [];
|
|
1356
|
+
/** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
|
|
1357
|
+
#advisorConfigs?: AdvisorConfig[];
|
|
1358
|
+
/** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
|
|
1359
|
+
* used as the open barrier for the next build so two writers never share a file. */
|
|
1237
1360
|
#advisorRecorderClosed: Promise<void> = Promise.resolve();
|
|
1238
1361
|
#goalTurnCounter = 0;
|
|
1239
1362
|
#planReferenceSent = false;
|
|
@@ -1272,6 +1395,7 @@ export class AgentSession {
|
|
|
1272
1395
|
*/
|
|
1273
1396
|
#todoReminderAwaitingProgress = false;
|
|
1274
1397
|
#todoPhases: TodoPhase[] = [];
|
|
1398
|
+
#replanTitleRefreshInFlight: Promise<void> | undefined = undefined;
|
|
1275
1399
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
1276
1400
|
|
|
1277
1401
|
// Bash execution state
|
|
@@ -1333,6 +1457,8 @@ export class AgentSession {
|
|
|
1333
1457
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
1334
1458
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
1335
1459
|
#onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
1460
|
+
#transformProviderContext: ((context: Context, model: Model) => Context | Promise<Context>) | undefined;
|
|
1461
|
+
#advisorStreamFn: StreamFn | undefined;
|
|
1336
1462
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
1337
1463
|
#rebuildSystemPrompt:
|
|
1338
1464
|
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
@@ -1387,6 +1513,7 @@ export class AgentSession {
|
|
|
1387
1513
|
* `message_end` + `stopReason: "aborted"`; callers clear it in `finally` so
|
|
1388
1514
|
* it cannot leak into later unrelated aborts. */
|
|
1389
1515
|
#planInternalAbortPending = false;
|
|
1516
|
+
#pendingAbortErrorId?: number;
|
|
1390
1517
|
|
|
1391
1518
|
#postPromptTasks = new Set<Promise<unknown>>();
|
|
1392
1519
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
@@ -1648,15 +1775,19 @@ export class AgentSession {
|
|
|
1648
1775
|
// toggle scopes priority to Fireworks alone, without mutating the shared
|
|
1649
1776
|
// session `serviceTier` that drives `/fast` and OpenAI/Anthropic priority.
|
|
1650
1777
|
this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
|
|
1651
|
-
this.#
|
|
1778
|
+
this.#advisorTools = config.advisorTools;
|
|
1652
1779
|
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1780
|
+
this.#advisorSharedInstructions = config.advisorSharedInstructions;
|
|
1653
1781
|
this.#advisorContextPrompt = config.advisorContextPrompt;
|
|
1782
|
+
this.#advisorConfigs = config.advisorConfigs;
|
|
1654
1783
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
1655
1784
|
this.#validateRetryFallbackChains();
|
|
1656
1785
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
1657
1786
|
this.#builtInToolNames = new Set(config.builtInToolNames ?? []);
|
|
1658
1787
|
this.#requestedToolNames = config.requestedToolNames;
|
|
1659
1788
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
1789
|
+
this.#transformProviderContext = config.transformProviderContext;
|
|
1790
|
+
this.#advisorStreamFn = config.advisorStreamFn;
|
|
1660
1791
|
this.#onPayload = config.onPayload;
|
|
1661
1792
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
1662
1793
|
// Avoid wrapping in an `async` closure when no user callback is configured: the
|
|
@@ -1693,12 +1824,15 @@ export class AgentSession {
|
|
|
1693
1824
|
await this.#applyRewind(rewindReport, messages);
|
|
1694
1825
|
}
|
|
1695
1826
|
this.#advisorPrimaryTurnsCompleted++;
|
|
1696
|
-
if (this.#
|
|
1697
|
-
this.#
|
|
1827
|
+
if (this.#advisors.length > 0) {
|
|
1828
|
+
for (const a of this.#advisors) {
|
|
1829
|
+
if (!a.runtime.disposed) a.runtime.onTurnEnd(messages);
|
|
1830
|
+
}
|
|
1698
1831
|
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
1699
1832
|
if (syncBacklog !== "off") {
|
|
1700
1833
|
const threshold = parseInt(syncBacklog, 10);
|
|
1701
|
-
|
|
1834
|
+
// Parallel so the 30s catch-up budget is shared across advisors, not summed.
|
|
1835
|
+
await Promise.all(this.#advisors.map(a => a.runtime.waitForCatchup(30000, threshold, signal)));
|
|
1702
1836
|
}
|
|
1703
1837
|
}
|
|
1704
1838
|
await this.#maintainContextMidRun(messages, signal, context);
|
|
@@ -1868,12 +2002,14 @@ export class AgentSession {
|
|
|
1868
2002
|
// Mute the recorder across the re-prime: AdvisorRuntime.reset() aborts the advisor
|
|
1869
2003
|
// loop, and that abort can emit an `aborted` message_end we must not attribute to
|
|
1870
2004
|
// either session's transcript. Detach, reset, then re-attach the live agent's feed.
|
|
1871
|
-
this.#
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2005
|
+
for (const a of this.#advisors) {
|
|
2006
|
+
a.agentUnsubscribe?.();
|
|
2007
|
+
a.agentUnsubscribe = undefined;
|
|
2008
|
+
a.runtime.reset();
|
|
2009
|
+
a.adviseTool.resetDeliveredNotes();
|
|
2010
|
+
a.emissionGuard.reset();
|
|
2011
|
+
this.#attachAdvisorRecorderFeed(a);
|
|
2012
|
+
}
|
|
1877
2013
|
this.#advisorPrimaryTurnsCompleted = 0;
|
|
1878
2014
|
this.#advisorInterruptImmuneTurnStart = undefined;
|
|
1879
2015
|
this.#advisorAutoResumeSuppressed = false;
|
|
@@ -1886,214 +2022,298 @@ export class AgentSession {
|
|
|
1886
2022
|
|
|
1887
2023
|
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1888
2024
|
if (this.#isDisposed) return false;
|
|
1889
|
-
if (this.#
|
|
2025
|
+
if (this.#advisors.length > 0) return true;
|
|
1890
2026
|
if (!this.#advisorEnabled) return false;
|
|
1891
2027
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1892
2028
|
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
);
|
|
1898
|
-
if (!advisorSel) {
|
|
1899
|
-
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive");
|
|
1900
|
-
return false;
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
// Concern and blocker interrupt the running agent through the steering
|
|
1904
|
-
// channel (aborting in-flight tools at the next steering boundary); when the
|
|
1905
|
-
// loop has already yielded, triggerTurn resumes it so the advice is acted on
|
|
1906
|
-
// immediately rather than waiting for the next user prompt. After a deliberate
|
|
1907
|
-
// user interrupt the auto-resume is suppressed — but only while the agent is
|
|
1908
|
-
// idle or still tearing the interrupted turn down: a concern is then recorded
|
|
1909
|
-
// as a visible card and re-enters context when the user resumes. Once a turn
|
|
1910
|
-
// is streaming again (a resume the user already drove) it is steered in live,
|
|
1911
|
-
// since steering an active run auto-resumes nothing; parking it there would
|
|
1912
|
-
// strand the advice and dump the backlog as one burst at the next prompt. A
|
|
1913
|
-
// plain nit always rides the non-interrupting YieldQueue aside.
|
|
1914
|
-
// Apply the per-session emission policy (one-advise-per-update gate,
|
|
1915
|
-
// exact-text dedupe, content-free phrase filter) before any routing.
|
|
1916
|
-
// Suppression here means the advisor model called `advise()` but the call
|
|
1917
|
-
// is dropped silently — the model still sees `Recorded.` from the tool, so
|
|
1918
|
-
// telling it "suppressed" doesn't tempt it into rephrasing the same useless
|
|
1919
|
-
// note to bypass the dedupe.
|
|
1920
|
-
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1921
|
-
if (!this.#advisorEmissionGuard.accept(note)) {
|
|
1922
|
-
logger.debug("advisor advice suppressed by emission guard", { severity });
|
|
1923
|
-
return;
|
|
1924
|
-
}
|
|
1925
|
-
const interrupting = isInterruptingSeverity(severity);
|
|
1926
|
-
const channel = resolveAdvisorDeliveryChannel({
|
|
1927
|
-
severity,
|
|
1928
|
-
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
1929
|
-
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
1930
|
-
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
1931
|
-
// loop will consume a steer at its next boundary; steering into the unwind
|
|
1932
|
-
// window would strand the card and let #drainStrandedQueuedMessages
|
|
1933
|
-
// auto-resume it despite the user's interrupt.
|
|
1934
|
-
streaming: this.agent.state.isStreaming,
|
|
1935
|
-
aborting: this.#abortInProgress,
|
|
1936
|
-
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
1937
|
-
});
|
|
1938
|
-
if (channel === "aside") {
|
|
1939
|
-
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
1940
|
-
return;
|
|
1941
|
-
}
|
|
1942
|
-
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1943
|
-
const content = formatAdvisorBatchContent(notes);
|
|
1944
|
-
const details = { notes } satisfies AdvisorMessageDetails;
|
|
1945
|
-
if (channel === "preserve") {
|
|
1946
|
-
this.#preserveAdvisorCard({
|
|
1947
|
-
role: "custom",
|
|
1948
|
-
customType: "advisor",
|
|
1949
|
-
content,
|
|
1950
|
-
display: true,
|
|
1951
|
-
attribution: "agent",
|
|
1952
|
-
details,
|
|
1953
|
-
timestamp: Date.now(),
|
|
1954
|
-
});
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
this.#recordAdvisorInterruptDelivered();
|
|
1958
|
-
void this.sendCustomMessage(
|
|
1959
|
-
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1960
|
-
{ deliverAs: "steer", triggerTurn: true },
|
|
1961
|
-
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1962
|
-
};
|
|
1963
|
-
|
|
1964
|
-
const adviseTool = new AdviseTool(enqueueAdvice);
|
|
1965
|
-
this.#advisorAdviseTool = adviseTool;
|
|
1966
|
-
const advisorReadOnlyTools = this.#advisorReadOnlyTools ?? [];
|
|
1967
|
-
|
|
1968
|
-
const appendOnlyContext = new AppendOnlyContextManager();
|
|
1969
|
-
const advisorThinkingLevel = advisorSel.thinkingLevel ?? ThinkingLevel.Medium;
|
|
1970
|
-
const systemPrompt = [advisorSystemPrompt];
|
|
1971
|
-
if (this.#advisorContextPrompt) {
|
|
1972
|
-
systemPrompt.push(this.#advisorContextPrompt);
|
|
1973
|
-
}
|
|
1974
|
-
if (this.#advisorWatchdogPrompt) {
|
|
1975
|
-
systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
1976
|
-
}
|
|
1977
|
-
const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
|
|
2029
|
+
// The lone implicit "default" advisor (slug "") reproduces the legacy
|
|
2030
|
+
// single-advisor path: the `advisor` role model + `__advisor.jsonl`.
|
|
2031
|
+
const legacy = !this.#advisorConfigs?.length;
|
|
2032
|
+
const roster: AdvisorConfig[] = legacy ? [{ name: "default" }] : this.#advisorConfigs!;
|
|
1978
2033
|
|
|
1979
2034
|
// Advisor service tier (`serviceTierAdvisor`): "none" (default) runs the
|
|
1980
2035
|
// advisor on standard processing; "inherit" tracks the session's live tier
|
|
1981
2036
|
// per request (like the main agent, including /fast toggles) via a resolver;
|
|
1982
|
-
// a concrete value pins the advisor to that tier
|
|
2037
|
+
// a concrete value pins the advisor to that tier. One value for all advisors.
|
|
1983
2038
|
const advisorTierSetting = this.settings.get("serviceTierAdvisor");
|
|
1984
2039
|
const advisorServiceTier =
|
|
1985
2040
|
advisorTierSetting === "inherit" ? undefined : resolveServiceTierSetting(advisorTierSetting, undefined);
|
|
1986
2041
|
const advisorServiceTierResolver =
|
|
1987
2042
|
advisorTierSetting === "inherit" ? (model: Model) => this.#effectiveServiceTier(model) : undefined;
|
|
1988
2043
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2044
|
+
const usedSlugs = new Set<string>();
|
|
2045
|
+
for (const config of roster) {
|
|
2046
|
+
let slug = legacy ? "" : slugifyAdvisorName(config.name);
|
|
2047
|
+
if (slug) {
|
|
2048
|
+
let candidate = slug;
|
|
2049
|
+
let n = 2;
|
|
2050
|
+
while (usedSlugs.has(candidate)) candidate = `${slug}-${n++}`;
|
|
2051
|
+
slug = candidate;
|
|
2052
|
+
usedSlugs.add(slug);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Resolve the advisor's model: an explicit `model` override wins; else the
|
|
2056
|
+
// `advisor` role chain. A model that fails to resolve skips just this advisor.
|
|
2057
|
+
let model: Model | undefined;
|
|
2058
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
2059
|
+
if (config.model) {
|
|
2060
|
+
const resolved = resolveModelOverride([config.model], this.#modelRegistry, this.settings);
|
|
2061
|
+
model = resolved.model;
|
|
2062
|
+
thinkingLevel = resolved.thinkingLevel;
|
|
2063
|
+
if (!model) {
|
|
2064
|
+
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2065
|
+
continue;
|
|
2004
2066
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2067
|
+
} else {
|
|
2068
|
+
const sel = resolveAdvisorRoleSelection(
|
|
2069
|
+
this.settings,
|
|
2070
|
+
this.#modelRegistry.getAvailable(),
|
|
2071
|
+
this.#modelRegistry,
|
|
2072
|
+
);
|
|
2073
|
+
if (!sel) {
|
|
2074
|
+
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2075
|
+
advisor: config.name,
|
|
2076
|
+
});
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
model = sel.model;
|
|
2080
|
+
thinkingLevel = sel.thinkingLevel;
|
|
2081
|
+
}
|
|
2082
|
+
const advisorModel = model;
|
|
2083
|
+
const advisorName = config.name;
|
|
2084
|
+
const advisorThinkingLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
2085
|
+
|
|
2086
|
+
const emissionGuard = new AdvisorEmissionGuard();
|
|
2087
|
+
const adviseTool = new AdviseTool((note, severity) => this.#routeAdvice(advisorRef, note, severity));
|
|
2088
|
+
|
|
2089
|
+
// `#advisorWatchdogPrompt` already carries WATCHDOG.md + YAML shared
|
|
2090
|
+
// instructions; `config.instructions` adds this advisor's specialization.
|
|
2091
|
+
const systemPrompt = [advisorSystemPrompt];
|
|
2092
|
+
if (this.#advisorContextPrompt) systemPrompt.push(this.#advisorContextPrompt);
|
|
2093
|
+
if (this.#advisorWatchdogPrompt) systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
2094
|
+
if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
|
|
2095
|
+
if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
|
|
2096
|
+
|
|
2097
|
+
const names = config.tools?.length ? new Set(config.tools) : ADVISOR_DEFAULT_TOOL_NAMES;
|
|
2098
|
+
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
2099
|
+
|
|
2100
|
+
const advisorSessionId = this.#advisorSessionId(slug);
|
|
2101
|
+
const appendOnlyContext = new AppendOnlyContextManager();
|
|
2102
|
+
|
|
2103
|
+
// Thread the primary's telemetry into the advisor loop so the advisor
|
|
2104
|
+
// model's GenAI spans + usage/cost hooks fire stamped with the advisor's
|
|
2105
|
+
// own identity. `conversationId` is cleared so the advisor loop falls back
|
|
2106
|
+
// to its own session id; undefined telemetry stays undefined.
|
|
2107
|
+
const advisorTelemetry = this.agent.telemetry
|
|
2108
|
+
? {
|
|
2109
|
+
...this.agent.telemetry,
|
|
2110
|
+
agent: {
|
|
2111
|
+
id: advisorSessionId,
|
|
2112
|
+
name: slug ? `${MODEL_ROLES.advisor.name}: ${advisorName}` : MODEL_ROLES.advisor.name,
|
|
2113
|
+
description: formatModelString(advisorModel),
|
|
2114
|
+
},
|
|
2115
|
+
conversationId: undefined,
|
|
2116
|
+
}
|
|
2117
|
+
: undefined;
|
|
2118
|
+
// Mirror the SDK's provider-shaping options (streamFn/onPayload/...,
|
|
2119
|
+
// providerSessionState, promptCacheKey, transformProviderContext) so each
|
|
2120
|
+
// advisor's requests cache, route, and obfuscate like the main turn.
|
|
2121
|
+
const advisorAgent = new Agent({
|
|
2122
|
+
initialState: {
|
|
2123
|
+
systemPrompt,
|
|
2124
|
+
model: advisorModel,
|
|
2125
|
+
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
2126
|
+
tools: [adviseTool, ...tools],
|
|
2127
|
+
},
|
|
2128
|
+
appendOnlyContext,
|
|
2129
|
+
sessionId: advisorSessionId,
|
|
2130
|
+
promptCacheKey: advisorSessionId,
|
|
2131
|
+
providerSessionState: this.#providerSessionState,
|
|
2132
|
+
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
2133
|
+
streamFn: this.#advisorStreamFn,
|
|
2134
|
+
onPayload: this.#onPayload,
|
|
2135
|
+
onResponse: this.#onResponse,
|
|
2136
|
+
onSseEvent: this.#onSseEvent,
|
|
2137
|
+
transformProviderContext: this.#transformProviderContext,
|
|
2138
|
+
intentTracing: false,
|
|
2139
|
+
telemetry: advisorTelemetry,
|
|
2140
|
+
serviceTier: advisorServiceTier,
|
|
2141
|
+
serviceTierResolver: advisorServiceTierResolver,
|
|
2142
|
+
});
|
|
2143
|
+
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2144
|
+
|
|
2145
|
+
const advisorAgentFacade: AdvisorAgent = {
|
|
2146
|
+
prompt: input => advisorAgent.prompt(input),
|
|
2147
|
+
abort: reason => advisorAgent.abort(reason),
|
|
2148
|
+
reset: () => {
|
|
2149
|
+
advisorAgent.reset();
|
|
2150
|
+
appendOnlyContext.log.clear();
|
|
2151
|
+
},
|
|
2152
|
+
rollbackTo: count => {
|
|
2153
|
+
// Drop the failed user batch + synthetic assistant-error turn
|
|
2154
|
+
// `Agent.#runLoop` appended for a turn ending in `stopReason: "error"`.
|
|
2155
|
+
const messages = advisorAgent.state.messages;
|
|
2156
|
+
if (count < messages.length) {
|
|
2157
|
+
messages.length = count;
|
|
2158
|
+
}
|
|
2159
|
+
appendOnlyContext.resetSyncCursor();
|
|
2160
|
+
advisorAgent.state.error = undefined;
|
|
2161
|
+
},
|
|
2162
|
+
state: advisorAgent.state,
|
|
2163
|
+
};
|
|
2032
2164
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2165
|
+
// Persist this advisor's turns to `<session>/__advisor[.<slug>].jsonl`
|
|
2166
|
+
// (resolved lazily so it follows session switches) for stats attribution
|
|
2167
|
+
// and Agent Hub observability, without registering it as a peer.
|
|
2168
|
+
const recorder = new AdvisorTranscriptRecorder(
|
|
2169
|
+
() => this.sessionManager.getSessionFile(),
|
|
2170
|
+
() => this.sessionManager.getCwd(),
|
|
2171
|
+
advisorTranscriptFilename(slug),
|
|
2172
|
+
// On the advisor on→off→on toggle, wait for the prior recorders' closes
|
|
2173
|
+
// so two SessionManagers never hold the same file at once.
|
|
2174
|
+
this.#advisorRecorderClosed,
|
|
2175
|
+
);
|
|
2176
|
+
const runtime = new AdvisorRuntime(advisorAgentFacade, {
|
|
2177
|
+
snapshotMessages: () => this.agent.state.messages,
|
|
2178
|
+
enqueueAdvice: (note, severity) => this.#routeAdvice(advisorRef, note, severity),
|
|
2179
|
+
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
2180
|
+
obfuscator: this.#obfuscator,
|
|
2181
|
+
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
2182
|
+
notifyFailure: error => {
|
|
2183
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2184
|
+
this.emitNotice(
|
|
2185
|
+
"warning",
|
|
2186
|
+
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(advisorModel)}: ${message}`,
|
|
2187
|
+
"advisor",
|
|
2188
|
+
);
|
|
2189
|
+
},
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
const advisorRef: ActiveAdvisor = {
|
|
2193
|
+
name: advisorName,
|
|
2194
|
+
slug,
|
|
2195
|
+
agent: advisorAgent,
|
|
2196
|
+
runtime,
|
|
2197
|
+
adviseTool,
|
|
2198
|
+
emissionGuard,
|
|
2199
|
+
recorder,
|
|
2200
|
+
recorderClosed: Promise.resolve(),
|
|
2201
|
+
model: advisorModel,
|
|
2202
|
+
thinkingLevel: advisorThinkingLevel,
|
|
2203
|
+
};
|
|
2204
|
+
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
2205
|
+
if (seedToCurrent) runtime.seedTo(this.agent.state.messages.length);
|
|
2206
|
+
this.#advisors.push(advisorRef);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// One shared non-blocking aside channel for all advisors; the build callback
|
|
2210
|
+
// aggregates every advisor's queued nits into one card (each entry already
|
|
2211
|
+
// carries its own `advisor` name).
|
|
2212
|
+
if (this.#advisors.length > 0 && !this.#advisorYieldQueueUnsubscribe) {
|
|
2213
|
+
this.#advisorYieldQueueUnsubscribe = this.yieldQueue.register<AdvisorNote>("advisor", {
|
|
2214
|
+
build: entries =>
|
|
2215
|
+
entries.length === 0
|
|
2216
|
+
? null
|
|
2217
|
+
: ({
|
|
2218
|
+
role: "custom",
|
|
2219
|
+
customType: "advisor",
|
|
2220
|
+
display: true,
|
|
2221
|
+
attribution: "agent",
|
|
2222
|
+
timestamp: Date.now(),
|
|
2223
|
+
content: formatAdvisorBatchContent(entries),
|
|
2224
|
+
details: { notes: entries } satisfies AdvisorMessageDetails,
|
|
2225
|
+
} satisfies CustomMessage),
|
|
2226
|
+
skipIdleFlush: true,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return this.#advisors.length > 0;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/** Provider/session id for an advisor's loop. The slug suffix MUST match the
|
|
2234
|
+
* advisor's transcript filename so stats/telemetry attribute the same advisor. */
|
|
2235
|
+
#advisorSessionId(slug: string): string | undefined {
|
|
2236
|
+
if (!this.sessionId) return undefined;
|
|
2237
|
+
return slug ? `${this.sessionId}-advisor-${slug}` : `${this.sessionId}-advisor`;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
/**
|
|
2241
|
+
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
2242
|
+
* blocker interrupt the running agent through the steering channel; once the
|
|
2243
|
+
* loop has yielded, `triggerTurn` resumes it. After a deliberate user interrupt
|
|
2244
|
+
* auto-resume is suppressed while idle/unwinding (the note becomes a preserved
|
|
2245
|
+
* card re-entering on resume); a live-streaming turn is steered in directly. A
|
|
2246
|
+
* plain nit always rides the non-interrupting YieldQueue aside. Suppression by
|
|
2247
|
+
* the per-advisor emission guard drops the note silently — the model still saw
|
|
2248
|
+
* `Recorded.`, so it isn't tempted to rephrase the same note past the dedupe.
|
|
2249
|
+
*/
|
|
2250
|
+
#routeAdvice(advisor: ActiveAdvisor, note: string, severity?: AdvisorSeverity): void {
|
|
2251
|
+
if (!advisor.emissionGuard.accept(note)) {
|
|
2252
|
+
logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
// The implicit single ("default") advisor stamps no source name, so its
|
|
2256
|
+
// agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
|
|
2257
|
+
const source = advisor.slug ? advisor.name : undefined;
|
|
2258
|
+
const interrupting = isInterruptingSeverity(severity);
|
|
2259
|
+
const channel = resolveAdvisorDeliveryChannel({
|
|
2260
|
+
severity,
|
|
2261
|
+
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
2262
|
+
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
2263
|
+
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
2264
|
+
// loop consumes a steer at its next boundary.
|
|
2265
|
+
streaming: this.agent.state.isStreaming,
|
|
2266
|
+
aborting: this.#abortInProgress,
|
|
2267
|
+
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
2072
2268
|
});
|
|
2269
|
+
if (channel === "aside") {
|
|
2270
|
+
this.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const notes: AdvisorNote[] = [{ note, severity, advisor: source }];
|
|
2274
|
+
const content = formatAdvisorBatchContent(notes);
|
|
2275
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
2276
|
+
if (channel === "preserve") {
|
|
2277
|
+
this.#preserveAdvisorCard({
|
|
2278
|
+
role: "custom",
|
|
2279
|
+
customType: "advisor",
|
|
2280
|
+
content,
|
|
2281
|
+
display: true,
|
|
2282
|
+
attribution: "agent",
|
|
2283
|
+
details,
|
|
2284
|
+
timestamp: Date.now(),
|
|
2285
|
+
});
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
this.#recordAdvisorInterruptDelivered();
|
|
2289
|
+
void this.sendCustomMessage(
|
|
2290
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
2291
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
2292
|
+
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
2293
|
+
}
|
|
2073
2294
|
|
|
2074
|
-
|
|
2295
|
+
/** Re-prime every advisor's transcript view (compaction/shake/rewind) without the
|
|
2296
|
+
* session-level latch reset {@link #resetAdvisorSessionState} performs. */
|
|
2297
|
+
#resetAllAdvisorRuntimes(): void {
|
|
2298
|
+
for (const a of this.#advisors) a.runtime.reset();
|
|
2075
2299
|
}
|
|
2076
2300
|
|
|
2077
2301
|
#stopAdvisorRuntime(): void {
|
|
2078
|
-
// Detach
|
|
2302
|
+
// Detach each recorder feed BEFORE aborting its advisor agent: dispose() aborts
|
|
2079
2303
|
// the loop, and an abort emits a final `message_end` we must not enqueue against
|
|
2080
2304
|
// a closing recorder (it would reopen and resurrect an already-released file).
|
|
2081
|
-
|
|
2082
|
-
this.#
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
if (this.#advisorTranscriptRecorder) {
|
|
2088
|
-
// Capture the close so dispose()/`/drop` can await the queued open+append+close —
|
|
2305
|
+
const closes: Promise<void>[] = [];
|
|
2306
|
+
for (const a of this.#advisors) {
|
|
2307
|
+
a.agentUnsubscribe?.();
|
|
2308
|
+
a.agentUnsubscribe = undefined;
|
|
2309
|
+
a.runtime.dispose();
|
|
2310
|
+
// Capture each close so dispose()/`/drop` can await the queued open+append+close —
|
|
2089
2311
|
// the last advisor turn would otherwise be lost on a fast process exit.
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
if (this.#advisorAgent) {
|
|
2094
|
-
this.#advisorAgent = undefined;
|
|
2312
|
+
a.recorderClosed = a.recorder.close();
|
|
2313
|
+
closes.push(a.recorderClosed);
|
|
2095
2314
|
}
|
|
2096
|
-
this.#
|
|
2315
|
+
this.#advisorRecorderClosed = Promise.all(closes).then(() => {});
|
|
2316
|
+
this.#advisors = [];
|
|
2097
2317
|
this.#advisorYieldQueueUnsubscribe?.();
|
|
2098
2318
|
this.#advisorYieldQueueUnsubscribe = undefined;
|
|
2099
2319
|
}
|
|
@@ -2101,16 +2321,13 @@ export class AgentSession {
|
|
|
2101
2321
|
/** Subscribe the advisor agent's finalized messages into the transcript recorder.
|
|
2102
2322
|
* Idempotent-by-replacement: callers detach the prior feed first. Kept separate
|
|
2103
2323
|
* so the re-prime path can mute the feed across an abort-driven reset. */
|
|
2104
|
-
#attachAdvisorRecorderFeed(): void {
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
if (!agent || !recorder) return;
|
|
2108
|
-
this.#advisorAgentUnsubscribe = agent.subscribe(event => {
|
|
2109
|
-
if (event.type === "message_end") recorder.record(event.message);
|
|
2324
|
+
#attachAdvisorRecorderFeed(advisor: ActiveAdvisor): void {
|
|
2325
|
+
advisor.agentUnsubscribe = advisor.agent.subscribe(event => {
|
|
2326
|
+
if (event.type === "message_end") advisor.recorder.record(event.message);
|
|
2110
2327
|
});
|
|
2111
2328
|
}
|
|
2112
2329
|
|
|
2113
|
-
async #promoteAdvisorContextModel(currentModel: Model): Promise<boolean> {
|
|
2330
|
+
async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
|
|
2114
2331
|
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
2115
2332
|
if (!promotionSettings.enabled) return false;
|
|
2116
2333
|
const contextWindow = currentModel.contextWindow ?? 0;
|
|
@@ -2118,25 +2335,23 @@ export class AgentSession {
|
|
|
2118
2335
|
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
2119
2336
|
if (!targetModel) return false;
|
|
2120
2337
|
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
this.#modelRegistry,
|
|
2125
|
-
);
|
|
2126
|
-
const advisorThinkingLevel = advisorSel?.thinkingLevel ?? ThinkingLevel.Medium;
|
|
2127
|
-
|
|
2338
|
+
// Preserve this advisor's own thinking level (a configured `model:...:high`
|
|
2339
|
+
// keeps its suffix across a promotion); only the model changes.
|
|
2340
|
+
const advisorThinkingLevel = advisor.thinkingLevel;
|
|
2128
2341
|
try {
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2342
|
+
advisor.agent.setModel(targetModel);
|
|
2343
|
+
advisor.agent.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
2344
|
+
advisor.agent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2345
|
+
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
2133
2346
|
logger.debug("Advisor context promotion switched model on overflow", {
|
|
2347
|
+
advisor: advisor.name,
|
|
2134
2348
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2135
2349
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2136
2350
|
});
|
|
2137
2351
|
return true;
|
|
2138
2352
|
} catch (error) {
|
|
2139
2353
|
logger.warn("Advisor context promotion failed", {
|
|
2354
|
+
advisor: advisor.name,
|
|
2140
2355
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2141
2356
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2142
2357
|
error: String(error),
|
|
@@ -2145,19 +2360,18 @@ export class AgentSession {
|
|
|
2145
2360
|
}
|
|
2146
2361
|
}
|
|
2147
2362
|
|
|
2148
|
-
async #maintainAdvisorContext(incomingTokens: number): Promise<boolean> {
|
|
2149
|
-
const
|
|
2150
|
-
if (!advisor) return false;
|
|
2363
|
+
async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
|
|
2364
|
+
const agent = advisor.agent;
|
|
2151
2365
|
|
|
2152
2366
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
2153
2367
|
if (compactionSettings.strategy === "off") return false;
|
|
2154
2368
|
if (!compactionSettings.enabled) return false;
|
|
2155
2369
|
|
|
2156
|
-
const advisorModel =
|
|
2370
|
+
const advisorModel = agent.state.model;
|
|
2157
2371
|
const contextWindow = advisorModel.contextWindow ?? 0;
|
|
2158
2372
|
if (contextWindow <= 0) return false;
|
|
2159
2373
|
|
|
2160
|
-
const messages =
|
|
2374
|
+
const messages = agent.state.messages;
|
|
2161
2375
|
let contextTokens = incomingTokens;
|
|
2162
2376
|
for (const message of messages) {
|
|
2163
2377
|
contextTokens += estimateTokens(message);
|
|
@@ -2168,9 +2382,9 @@ export class AgentSession {
|
|
|
2168
2382
|
}
|
|
2169
2383
|
|
|
2170
2384
|
// 1. Try promotion first
|
|
2171
|
-
if (await this.#promoteAdvisorContextModel(advisorModel)) {
|
|
2385
|
+
if (await this.#promoteAdvisorContextModel(advisor, advisorModel)) {
|
|
2172
2386
|
// Promotion succeeded, check if new model has enough space
|
|
2173
|
-
const newModel =
|
|
2387
|
+
const newModel = agent.state.model;
|
|
2174
2388
|
const newWindow = newModel.contextWindow ?? 0;
|
|
2175
2389
|
if (newWindow > 0) {
|
|
2176
2390
|
const stillNeedsCompaction = shouldCompact(contextTokens, newWindow, compactionSettings);
|
|
@@ -2192,7 +2406,9 @@ export class AgentSession {
|
|
|
2192
2406
|
timestamp,
|
|
2193
2407
|
summary: message.summary,
|
|
2194
2408
|
shortSummary: message.shortSummary,
|
|
2195
|
-
firstKeptEntryId:
|
|
2409
|
+
firstKeptEntryId:
|
|
2410
|
+
(message as CompactionSummaryMessage & { firstKeptEntryId?: string }).firstKeptEntryId ||
|
|
2411
|
+
`msg-${i + 1}`,
|
|
2196
2412
|
tokensBefore: message.tokensBefore,
|
|
2197
2413
|
} satisfies CompactionEntry;
|
|
2198
2414
|
}
|
|
@@ -2206,33 +2422,37 @@ export class AgentSession {
|
|
|
2206
2422
|
} satisfies SessionMessageEntry;
|
|
2207
2423
|
});
|
|
2208
2424
|
|
|
2209
|
-
const
|
|
2425
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
2426
|
+
const candidates = this.#resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
2427
|
+
if (candidates.length === 0) {
|
|
2428
|
+
// No compaction candidates, fallback to re-prime
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
const advisorSessionId = this.#advisorSessionId(advisor.slug);
|
|
2432
|
+
const preparation = prepareCompaction(
|
|
2433
|
+
pathEntries,
|
|
2434
|
+
compactionSettings,
|
|
2435
|
+
await this.#runnableCompactionCandidates(candidates, advisorSessionId),
|
|
2436
|
+
);
|
|
2210
2437
|
if (!preparation) {
|
|
2211
2438
|
// Cannot prepare compaction, fallback to re-prime
|
|
2212
2439
|
return true;
|
|
2213
2440
|
}
|
|
2214
2441
|
|
|
2215
|
-
const advisorCompactionThinkingLevel: ThinkingLevel | undefined =
|
|
2442
|
+
const advisorCompactionThinkingLevel: ThinkingLevel | undefined = agent.state.disableReasoning
|
|
2216
2443
|
? ThinkingLevel.Off
|
|
2217
|
-
:
|
|
2444
|
+
: agent.state.thinkingLevel;
|
|
2218
2445
|
|
|
2219
2446
|
// Advisor state is in-memory-only, so snapcompact's frame archive has no
|
|
2220
2447
|
// stable SessionEntry preserveData slot to carry across future advisor
|
|
2221
2448
|
// maintenance runs. Use an LLM summary even when the primary session is
|
|
2222
2449
|
// configured for snapcompact.
|
|
2223
|
-
const availableModels = this.#modelRegistry.getAvailable();
|
|
2224
|
-
const candidates = this.#resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
2225
|
-
if (candidates.length === 0) {
|
|
2226
|
-
// No compaction candidates, fallback to re-prime
|
|
2227
|
-
return true;
|
|
2228
|
-
}
|
|
2229
2450
|
|
|
2230
2451
|
let compactResult: CompactionResult | undefined;
|
|
2231
2452
|
let lastError: unknown;
|
|
2232
|
-
const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
|
|
2233
2453
|
// Instrument the advisor's overflow-compaction one-shot like the primary
|
|
2234
2454
|
// compaction path so the advisor model's maintenance call also emits spans.
|
|
2235
|
-
const telemetry = resolveTelemetry(
|
|
2455
|
+
const telemetry = resolveTelemetry(agent.telemetry, advisorSessionId);
|
|
2236
2456
|
|
|
2237
2457
|
for (const candidate of candidates) {
|
|
2238
2458
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorSessionId);
|
|
@@ -2249,6 +2469,9 @@ export class AgentSession {
|
|
|
2249
2469
|
thinkingLevel: advisorCompactionThinkingLevel,
|
|
2250
2470
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
2251
2471
|
telemetry,
|
|
2472
|
+
tools: agent.state.tools,
|
|
2473
|
+
sessionId: advisorSessionId,
|
|
2474
|
+
promptCacheKey: advisorSessionId,
|
|
2252
2475
|
},
|
|
2253
2476
|
);
|
|
2254
2477
|
break;
|
|
@@ -2273,7 +2496,7 @@ export class AgentSession {
|
|
|
2273
2496
|
firstKeptEntryId,
|
|
2274
2497
|
} as CompactionSummaryMessage & { firstKeptEntryId?: string };
|
|
2275
2498
|
|
|
2276
|
-
|
|
2499
|
+
agent.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
2277
2500
|
return false;
|
|
2278
2501
|
}
|
|
2279
2502
|
|
|
@@ -2586,82 +2809,8 @@ export class AgentSession {
|
|
|
2586
2809
|
}
|
|
2587
2810
|
};
|
|
2588
2811
|
|
|
2589
|
-
#messageValueSignature(value: unknown): string {
|
|
2590
|
-
return JSON.stringify(value) ?? "undefined";
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
#sessionMessagesReferToSameTurn(left: AgentMessage, right: AgentMessage): boolean {
|
|
2594
|
-
if (left === right) return true;
|
|
2595
|
-
if (left.role !== right.role) return false;
|
|
2596
|
-
switch (left.role) {
|
|
2597
|
-
case "assistant":
|
|
2598
|
-
if (right.role !== "assistant") return false;
|
|
2599
|
-
return (
|
|
2600
|
-
left.timestamp === right.timestamp &&
|
|
2601
|
-
left.provider === right.provider &&
|
|
2602
|
-
left.model === right.model &&
|
|
2603
|
-
left.responseId === right.responseId &&
|
|
2604
|
-
left.stopReason === right.stopReason &&
|
|
2605
|
-
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2606
|
-
);
|
|
2607
|
-
case "toolResult":
|
|
2608
|
-
if (right.role !== "toolResult") return false;
|
|
2609
|
-
return (
|
|
2610
|
-
left.timestamp === right.timestamp &&
|
|
2611
|
-
left.toolCallId === right.toolCallId &&
|
|
2612
|
-
left.toolName === right.toolName &&
|
|
2613
|
-
left.isError === right.isError &&
|
|
2614
|
-
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2615
|
-
);
|
|
2616
|
-
case "user":
|
|
2617
|
-
if (right.role !== "user") return false;
|
|
2618
|
-
return (
|
|
2619
|
-
left.timestamp === right.timestamp &&
|
|
2620
|
-
left.attribution === right.attribution &&
|
|
2621
|
-
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2622
|
-
);
|
|
2623
|
-
case "developer":
|
|
2624
|
-
if (right.role !== "developer") return false;
|
|
2625
|
-
return (
|
|
2626
|
-
left.timestamp === right.timestamp &&
|
|
2627
|
-
left.attribution === right.attribution &&
|
|
2628
|
-
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2629
|
-
);
|
|
2630
|
-
case "fileMention":
|
|
2631
|
-
if (right.role !== "fileMention") return false;
|
|
2632
|
-
return (
|
|
2633
|
-
left.timestamp === right.timestamp &&
|
|
2634
|
-
this.#messageValueSignature(left.files) === this.#messageValueSignature(right.files)
|
|
2635
|
-
);
|
|
2636
|
-
default:
|
|
2637
|
-
return false;
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
#sessionMessagePersistenceKey(message: AgentMessage): string | undefined {
|
|
2642
|
-
switch (message.role) {
|
|
2643
|
-
case "assistant":
|
|
2644
|
-
return [
|
|
2645
|
-
"assistant",
|
|
2646
|
-
message.timestamp,
|
|
2647
|
-
message.provider,
|
|
2648
|
-
message.model,
|
|
2649
|
-
message.responseId ?? "",
|
|
2650
|
-
message.stopReason,
|
|
2651
|
-
].join(":");
|
|
2652
|
-
case "toolResult":
|
|
2653
|
-
return `toolResult:${message.timestamp}:${message.toolCallId}:${message.toolName}`;
|
|
2654
|
-
case "user":
|
|
2655
|
-
case "developer":
|
|
2656
|
-
case "fileMention":
|
|
2657
|
-
return `${message.role}:${message.timestamp}`;
|
|
2658
|
-
default:
|
|
2659
|
-
return undefined;
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
2812
|
#createMessageEndPersistenceSlot(message: AgentMessage): MessageEndPersistenceSlot | undefined {
|
|
2664
|
-
const key =
|
|
2813
|
+
const key = sessionMessagePersistenceKey(message);
|
|
2665
2814
|
if (!key) return undefined;
|
|
2666
2815
|
const previous = this.#messageEndPersistenceTail;
|
|
2667
2816
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
@@ -2691,31 +2840,61 @@ export class AgentSession {
|
|
|
2691
2840
|
}
|
|
2692
2841
|
|
|
2693
2842
|
async #waitForSessionMessagePersistence(message: AgentMessage): Promise<void> {
|
|
2694
|
-
const key =
|
|
2843
|
+
const key = sessionMessagePersistenceKey(message);
|
|
2695
2844
|
if (!key) return;
|
|
2696
2845
|
await this.#pendingMessageEndPersistence.get(key);
|
|
2697
2846
|
}
|
|
2698
2847
|
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2848
|
+
/**
|
|
2849
|
+
* Index every message entry on the current branch by persistence key, so
|
|
2850
|
+
* the mid-run-compaction planner can ask "is this turn message already on
|
|
2851
|
+
* the branch?" in O(1) instead of re-walking the branch per check.
|
|
2852
|
+
*
|
|
2853
|
+
* The Map's value is the list of branch messages that share a key — almost
|
|
2854
|
+
* always one. We only need the LIST when content equality matters (rare
|
|
2855
|
+
* collision tiebreaker via {@link sameMessageContent}); the empty/single-
|
|
2856
|
+
* entry common case lets the caller's lookup short-circuit at presence.
|
|
2857
|
+
*
|
|
2858
|
+
* Pre-#3629 the equivalent was `sessionManager.getBranch()` called twice
|
|
2859
|
+
* per turn message, each call rebuilding the path via O(n²) `unshift` and
|
|
2860
|
+
* structurally JSON-comparing every entry — seconds of synchronous work
|
|
2861
|
+
* per `onTurnEnd` on a long session and the load-bearing source of the
|
|
2862
|
+
* `ui.loop-blocked` warnings in the bug report.
|
|
2863
|
+
*/
|
|
2864
|
+
#indexPersistedMessagesByKey(): Map<string, AgentMessage[]> {
|
|
2865
|
+
const index = new Map<string, AgentMessage[]>();
|
|
2866
|
+
for (const entry of this.sessionManager.getBranch()) {
|
|
2867
|
+
if (entry.type !== "message") continue;
|
|
2868
|
+
const key = sessionMessagePersistenceKey(entry.message);
|
|
2869
|
+
if (key === undefined) continue;
|
|
2870
|
+
const existing = index.get(key);
|
|
2871
|
+
if (existing) existing.push(entry.message);
|
|
2872
|
+
else index.set(key, [entry.message]);
|
|
2704
2873
|
}
|
|
2705
|
-
return
|
|
2874
|
+
return index;
|
|
2706
2875
|
}
|
|
2707
2876
|
|
|
2708
|
-
|
|
2877
|
+
/**
|
|
2878
|
+
* True when {@link message} is structurally identical to a message already
|
|
2879
|
+
* appended to the current branch. Pairs a fast persistence-key lookup with
|
|
2880
|
+
* a content-equality fallback so two logically distinct messages that
|
|
2881
|
+
* happen to collide on the cheap key (e.g. two assistant turns at the same
|
|
2882
|
+
* millisecond with `undefined` responseId) still count as DISTINCT.
|
|
2883
|
+
*/
|
|
2884
|
+
#sessionMessageAlreadyPersisted(message: AgentMessage): boolean {
|
|
2885
|
+
const key = sessionMessagePersistenceKey(message);
|
|
2886
|
+
if (key === undefined) return false;
|
|
2709
2887
|
const branch = this.sessionManager.getBranch();
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2888
|
+
// Reverse walk: recently-appended entries are at the tail, so the common
|
|
2889
|
+
// "is the message I just emitted already in the branch?" lookup short-
|
|
2890
|
+
// circuits in O(1) hot, O(branch) cold. Cheap-key compare for every
|
|
2891
|
+
// entry; content compare only when the cheap check matches, so the
|
|
2892
|
+
// expensive `JSON.stringify(content)` path stays off the hot loop.
|
|
2893
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
2894
|
+
const entry = branch[index];
|
|
2895
|
+
if (entry.type !== "message") continue;
|
|
2896
|
+
if (sessionMessagePersistenceKey(entry.message) !== key) continue;
|
|
2897
|
+
if (sameMessageContent(entry.message, message)) return true;
|
|
2719
2898
|
}
|
|
2720
2899
|
return false;
|
|
2721
2900
|
}
|
|
@@ -2750,23 +2929,73 @@ export class AgentSession {
|
|
|
2750
2929
|
}
|
|
2751
2930
|
}
|
|
2752
2931
|
|
|
2932
|
+
/**
|
|
2933
|
+
* On a user-interrupted (`Esc`) abort, copy the trailing thinking run into a
|
|
2934
|
+
* hidden `display: false` continuity message for the next turn WITHOUT
|
|
2935
|
+
* mutating the assistant message. The original thinking stays on the message
|
|
2936
|
+
* so live render, reload, and Ctrl+L rebuilds keep showing it; `convertToLlm`
|
|
2937
|
+
* strips the run from the provider request (incomplete/unsigned thinking is
|
|
2938
|
+
* rejected on resend) when this continuity message follows the assistant turn.
|
|
2939
|
+
*/
|
|
2940
|
+
#demoteInterruptedThinkingOnUserInterrupt(
|
|
2941
|
+
message: AssistantMessage,
|
|
2942
|
+
): CustomMessage<InterruptedThinkingDetails> | undefined {
|
|
2943
|
+
if (message.stopReason !== "aborted" || !isUserInterruptAbort(message)) return undefined;
|
|
2944
|
+
const demoted = demoteInterruptedThinking(message);
|
|
2945
|
+
if (!demoted) return undefined;
|
|
2946
|
+
const interruptedAt = Date.now();
|
|
2947
|
+
return {
|
|
2948
|
+
role: "custom",
|
|
2949
|
+
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
2950
|
+
content: prompt.render(interruptedThinkingTemplate, { reasoning: demoted.reasoning }),
|
|
2951
|
+
display: false,
|
|
2952
|
+
details: {
|
|
2953
|
+
interruptedAt,
|
|
2954
|
+
provider: message.provider,
|
|
2955
|
+
model: message.model,
|
|
2956
|
+
blockCount: demoted.blockCount,
|
|
2957
|
+
},
|
|
2958
|
+
attribution: "agent",
|
|
2959
|
+
timestamp: interruptedAt,
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2753
2963
|
async #persistTurnMessagesForMidRunCompaction(context: AgentTurnEndContext | undefined): Promise<boolean> {
|
|
2754
2964
|
if (!context) return true;
|
|
2755
2965
|
const turnMessages = [context.message, ...context.toolResults];
|
|
2756
2966
|
for (const message of turnMessages) {
|
|
2757
2967
|
await this.#waitForSessionMessagePersistence(message);
|
|
2758
2968
|
}
|
|
2969
|
+
// One branch snapshot + one persistence-key index drives the entire
|
|
2970
|
+
// planning pass. Pre-#3629 this re-walked the branch and structurally
|
|
2971
|
+
// JSON-compared every entry per turn message, which on long sessions
|
|
2972
|
+
// turned each `onTurnEnd` into a seconds-long sync block (the
|
|
2973
|
+
// `ui.loop-blocked` warnings tagged `subagent:*` in the bug report).
|
|
2974
|
+
const branchIndex = this.#indexPersistedMessagesByKey();
|
|
2975
|
+
const turnKeys = turnMessages.map(sessionMessagePersistenceKey);
|
|
2976
|
+
const persistedKeys = new Set<string>();
|
|
2759
2977
|
for (let index = 0; index < turnMessages.length; index++) {
|
|
2760
|
-
const
|
|
2761
|
-
if (
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2978
|
+
const key = turnKeys[index];
|
|
2979
|
+
if (key === undefined) continue;
|
|
2980
|
+
const candidates = branchIndex.get(key);
|
|
2981
|
+
if (!candidates) continue;
|
|
2982
|
+
// Key match only counts when content also matches — two distinct
|
|
2983
|
+
// messages that collided on the cheap key must STILL be persisted.
|
|
2984
|
+
if (candidates.some(persisted => sameMessageContent(persisted, turnMessages[index]))) {
|
|
2985
|
+
persistedKeys.add(key);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
const plan = planTurnPersistence(turnKeys, persistedKeys);
|
|
2989
|
+
if (plan.kind === "out-of-order") {
|
|
2990
|
+
const message = turnMessages[plan.messageIndex];
|
|
2991
|
+
logger.debug("Skipping mid-run compaction because turn persistence is out of order", {
|
|
2992
|
+
role: message.role,
|
|
2993
|
+
timestamp: message.timestamp,
|
|
2994
|
+
});
|
|
2995
|
+
return false;
|
|
2996
|
+
}
|
|
2997
|
+
for (const index of plan.toPersist) {
|
|
2998
|
+
this.#persistSessionMessageIfMissing(turnMessages[index]);
|
|
2770
2999
|
}
|
|
2771
3000
|
return true;
|
|
2772
3001
|
}
|
|
@@ -2785,11 +3014,28 @@ export class AgentSession {
|
|
|
2785
3014
|
if (
|
|
2786
3015
|
event.type === "message_end" &&
|
|
2787
3016
|
event.message.role === "assistant" &&
|
|
2788
|
-
event.message.stopReason === "aborted"
|
|
2789
|
-
this.#planInternalAbortPending
|
|
3017
|
+
event.message.stopReason === "aborted"
|
|
2790
3018
|
) {
|
|
2791
|
-
|
|
2792
|
-
this.#planInternalAbortPending
|
|
3019
|
+
const message = event.message as AssistantMessage;
|
|
3020
|
+
if (this.#planInternalAbortPending) {
|
|
3021
|
+
message.errorMessage = SILENT_ABORT_MARKER;
|
|
3022
|
+
message.errorId = AIError.create(AIError.Flag.SilentAbort);
|
|
3023
|
+
this.#planInternalAbortPending = false;
|
|
3024
|
+
} else if (this.#pendingAbortErrorId) {
|
|
3025
|
+
message.errorId = this.#pendingAbortErrorId;
|
|
3026
|
+
this.#pendingAbortErrorId = undefined;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
const interruptedThinkingMessage =
|
|
3031
|
+
event.type === "message_end" && event.message.role === "assistant"
|
|
3032
|
+
? this.#demoteInterruptedThinkingOnUserInterrupt(event.message as AssistantMessage)
|
|
3033
|
+
: undefined;
|
|
3034
|
+
// `message_end` handling is fire-and-forget from agent-core. Make the
|
|
3035
|
+
// hidden continuity turn visible to the next prompt before any awaited
|
|
3036
|
+
// extension delivery or persistence can stall this handler.
|
|
3037
|
+
if (interruptedThinkingMessage) {
|
|
3038
|
+
this.agent.appendMessage(interruptedThinkingMessage);
|
|
2793
3039
|
}
|
|
2794
3040
|
|
|
2795
3041
|
const messageEndPersistence =
|
|
@@ -2934,6 +3180,15 @@ export class AgentSession {
|
|
|
2934
3180
|
} else {
|
|
2935
3181
|
persistMessageEnd();
|
|
2936
3182
|
}
|
|
3183
|
+
if (interruptedThinkingMessage) {
|
|
3184
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
3185
|
+
interruptedThinkingMessage.customType,
|
|
3186
|
+
interruptedThinkingMessage.content,
|
|
3187
|
+
interruptedThinkingMessage.display,
|
|
3188
|
+
interruptedThinkingMessage.details,
|
|
3189
|
+
interruptedThinkingMessage.attribution,
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
2937
3192
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
2938
3193
|
|
|
2939
3194
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
@@ -2990,9 +3245,10 @@ export class AgentSession {
|
|
|
2990
3245
|
}
|
|
2991
3246
|
}
|
|
2992
3247
|
if (event.message.role === "toolResult") {
|
|
2993
|
-
const { toolName, details, isError, content } = event.message as {
|
|
3248
|
+
const { toolName, toolCallId, details, isError, content } = event.message as {
|
|
3249
|
+
toolCallId?: string;
|
|
2994
3250
|
toolName?: string;
|
|
2995
|
-
details?: { path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
|
|
3251
|
+
details?: { op?: string; path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
|
|
2996
3252
|
isError?: boolean;
|
|
2997
3253
|
content?: Array<TextContent | ImageContent>;
|
|
2998
3254
|
};
|
|
@@ -3006,6 +3262,9 @@ export class AgentSession {
|
|
|
3006
3262
|
}
|
|
3007
3263
|
if (toolName === "todo" && !isError && Array.isArray(details?.phases)) {
|
|
3008
3264
|
this.setTodoPhases(details.phases);
|
|
3265
|
+
if (this.#isTodoInitResult(details, toolCallId)) {
|
|
3266
|
+
this.#scheduleReplanTitleRefresh();
|
|
3267
|
+
}
|
|
3009
3268
|
}
|
|
3010
3269
|
if (toolName === "todo" && isError) {
|
|
3011
3270
|
const errorText = content?.find(part => part.type === "text")?.text;
|
|
@@ -3102,7 +3361,7 @@ export class AgentSession {
|
|
|
3102
3361
|
if (
|
|
3103
3362
|
msg.stopReason === "error" &&
|
|
3104
3363
|
msg.provider === "github-copilot" &&
|
|
3105
|
-
|
|
3364
|
+
AIError.is(AIError.classifyMessage(msg), AIError.Flag.AuthFailed)
|
|
3106
3365
|
) {
|
|
3107
3366
|
await this.#modelRegistry.authStorage.remove("github-copilot");
|
|
3108
3367
|
}
|
|
@@ -3716,10 +3975,34 @@ export class AgentSession {
|
|
|
3716
3975
|
|
|
3717
3976
|
context.toolName = toolCall.name;
|
|
3718
3977
|
context.streamKey = toolCall.id ? `toolcall:${toolCall.id}` : `tool:${toolCall.name}:${contentIndex}`;
|
|
3719
|
-
context.filePaths = this.#
|
|
3978
|
+
context.filePaths = this.#extractTtsrToolFilePaths(toolCall);
|
|
3720
3979
|
return context;
|
|
3721
3980
|
}
|
|
3722
3981
|
|
|
3982
|
+
/**
|
|
3983
|
+
* Resolve the file paths a tool call would touch for TTSR path-glob matching.
|
|
3984
|
+
*
|
|
3985
|
+
* Prefer the tool's own `matcherPaths` hook — it understands the wire format
|
|
3986
|
+
* (hashline `[path#TAG]` section headers, apply_patch envelope markers) and
|
|
3987
|
+
* surfaces paths the generic top-level argument scan never sees. Fall back
|
|
3988
|
+
* to {@link #extractTtsrFilePathsFromArgs} for tools that pass paths as
|
|
3989
|
+
* `path`/`paths` arguments and for tool calls whose payload has not yet
|
|
3990
|
+
* streamed a header.
|
|
3991
|
+
*/
|
|
3992
|
+
#extractTtsrToolFilePaths(toolCall: ToolCall): string[] | undefined {
|
|
3993
|
+
const args = toolCall.arguments ?? {};
|
|
3994
|
+
const tools = this.agent.state.tools;
|
|
3995
|
+
const tool =
|
|
3996
|
+
tools.find(t => t.name === toolCall.name) ??
|
|
3997
|
+
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
3998
|
+
const toolPaths = tool?.matcherPaths?.(args);
|
|
3999
|
+
if (toolPaths && toolPaths.length > 0) {
|
|
4000
|
+
const normalized = toolPaths.flatMap(p => this.#normalizeTtsrPathCandidates(p));
|
|
4001
|
+
if (normalized.length > 0) return Array.from(new Set(normalized));
|
|
4002
|
+
}
|
|
4003
|
+
return this.#extractTtsrFilePathsFromArgs(args);
|
|
4004
|
+
}
|
|
4005
|
+
|
|
3723
4006
|
/**
|
|
3724
4007
|
* Match a stream delta against TTSR rules.
|
|
3725
4008
|
*
|
|
@@ -3733,6 +4016,14 @@ export class AgentSession {
|
|
|
3733
4016
|
if (!manager) {
|
|
3734
4017
|
return [];
|
|
3735
4018
|
}
|
|
4019
|
+
const entries = this.#resolveTtsrMatcherEntries(toolCall);
|
|
4020
|
+
if (entries) {
|
|
4021
|
+
const matches: Rule[] = [];
|
|
4022
|
+
for (const entry of entries) {
|
|
4023
|
+
matches.push(...manager.checkSnapshot(entry.digest, this.#perFileTtsrContext(matchContext, entry.path)));
|
|
4024
|
+
}
|
|
4025
|
+
return matches;
|
|
4026
|
+
}
|
|
3736
4027
|
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
3737
4028
|
if (digest !== undefined) {
|
|
3738
4029
|
return manager.checkSnapshot(digest, matchContext);
|
|
@@ -3742,14 +4033,44 @@ export class AgentSession {
|
|
|
3742
4033
|
|
|
3743
4034
|
/** Reconstruct the tool's normalized source snapshot via its `matcherDigest`, if any. */
|
|
3744
4035
|
#resolveTtsrMatcherDigest(toolCall: ToolCall | undefined): string | undefined {
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
4036
|
+
const tool = this.#resolveTtsrTool(toolCall);
|
|
4037
|
+
return tool?.matcherDigest?.(toolCall?.arguments ?? {});
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
/**
|
|
4041
|
+
* Per-file split of a streamed call (one entry per touched file paired with
|
|
4042
|
+
* the digest of only that file's added lines). Lets {@link #checkTtsrStream}
|
|
4043
|
+
* and {@link #checkTtsrAstStream} evaluate each file in isolation so a
|
|
4044
|
+
* path-scoped rule like `tool:edit(*.ts)` never fires on text that belongs
|
|
4045
|
+
* to a sibling Markdown hunk in a multi-file payload.
|
|
4046
|
+
*/
|
|
4047
|
+
#resolveTtsrMatcherEntries(toolCall: ToolCall | undefined): readonly { path: string; digest: string }[] | undefined {
|
|
4048
|
+
const tool = this.#resolveTtsrTool(toolCall);
|
|
4049
|
+
const entries = tool?.matcherEntries?.(toolCall?.arguments ?? {});
|
|
4050
|
+
return entries && entries.length > 0 ? entries : undefined;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
#resolveTtsrTool(toolCall: ToolCall | undefined) {
|
|
4054
|
+
if (!toolCall) return undefined;
|
|
3748
4055
|
const tools = this.agent.state.tools;
|
|
3749
|
-
|
|
4056
|
+
return (
|
|
3750
4057
|
tools.find(t => t.name === toolCall.name) ??
|
|
3751
|
-
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name)
|
|
3752
|
-
|
|
4058
|
+
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name)
|
|
4059
|
+
);
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
/**
|
|
4063
|
+
* Replace `matchContext`'s `filePaths` + `streamKey` so a per-file entry
|
|
4064
|
+
* gets its own glob-eligible path and its own TTSR buffer/repeat tracking
|
|
4065
|
+
* (each file's stream is independent inside the same tool call).
|
|
4066
|
+
*/
|
|
4067
|
+
#perFileTtsrContext(base: TtsrMatchContext, filePath: string): TtsrMatchContext {
|
|
4068
|
+
const filePaths = this.#normalizeTtsrPathCandidates(filePath);
|
|
4069
|
+
return {
|
|
4070
|
+
...base,
|
|
4071
|
+
filePaths: filePaths.length > 0 ? filePaths : [filePath],
|
|
4072
|
+
streamKey: base.streamKey ? `${base.streamKey}#${filePath}` : undefined,
|
|
4073
|
+
};
|
|
3753
4074
|
}
|
|
3754
4075
|
|
|
3755
4076
|
/**
|
|
@@ -3764,6 +4085,16 @@ export class AgentSession {
|
|
|
3764
4085
|
if (!manager) {
|
|
3765
4086
|
return [];
|
|
3766
4087
|
}
|
|
4088
|
+
const entries = this.#resolveTtsrMatcherEntries(toolCall);
|
|
4089
|
+
if (entries) {
|
|
4090
|
+
const matches: Rule[] = [];
|
|
4091
|
+
for (const entry of entries) {
|
|
4092
|
+
matches.push(
|
|
4093
|
+
...(await manager.checkAstSnapshot(entry.digest, this.#perFileTtsrContext(matchContext, entry.path))),
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
return matches;
|
|
4097
|
+
}
|
|
3767
4098
|
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
3768
4099
|
if (digest === undefined) {
|
|
3769
4100
|
return [];
|
|
@@ -4493,6 +4824,7 @@ export class AgentSession {
|
|
|
4493
4824
|
maxAttempts: event.maxAttempts,
|
|
4494
4825
|
delayMs: event.delayMs,
|
|
4495
4826
|
errorMessage: event.errorMessage,
|
|
4827
|
+
errorId: event.errorId,
|
|
4496
4828
|
});
|
|
4497
4829
|
} else if (event.type === "auto_retry_end") {
|
|
4498
4830
|
await this.#extensionRunner.emit({
|
|
@@ -6402,7 +6734,10 @@ export class AgentSession {
|
|
|
6402
6734
|
|
|
6403
6735
|
async promptCustomMessage<T = unknown>(
|
|
6404
6736
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
6405
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & {
|
|
6737
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & {
|
|
6738
|
+
queueChipText?: string;
|
|
6739
|
+
queueOnly?: boolean;
|
|
6740
|
+
},
|
|
6406
6741
|
): Promise<void> {
|
|
6407
6742
|
const textContent =
|
|
6408
6743
|
typeof message.content === "string"
|
|
@@ -6422,6 +6757,16 @@ export class AgentSession {
|
|
|
6422
6757
|
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
6423
6758
|
}
|
|
6424
6759
|
|
|
6760
|
+
if (options?.queueOnly) {
|
|
6761
|
+
if (!options.streamingBehavior) {
|
|
6762
|
+
throw new AgentBusyError();
|
|
6763
|
+
}
|
|
6764
|
+
for (const notice of keywordNotices) {
|
|
6765
|
+
await this.#queueCustomMessage(notice, options.streamingBehavior);
|
|
6766
|
+
}
|
|
6767
|
+
await this.#queueCustomMessage(message, options.streamingBehavior, options.queueChipText);
|
|
6768
|
+
return;
|
|
6769
|
+
}
|
|
6425
6770
|
if (this.isStreaming) {
|
|
6426
6771
|
if (!options?.streamingBehavior) {
|
|
6427
6772
|
throw new AgentBusyError();
|
|
@@ -6987,6 +7332,40 @@ export class AgentSession {
|
|
|
6987
7332
|
}
|
|
6988
7333
|
}
|
|
6989
7334
|
|
|
7335
|
+
/** Queue a custom message without starting a turn, matching steer/follow-up delivery. */
|
|
7336
|
+
async #queueCustomMessage<T = unknown>(
|
|
7337
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
7338
|
+
deliverAs: "steer" | "followUp",
|
|
7339
|
+
queueChipText?: string,
|
|
7340
|
+
): Promise<void> {
|
|
7341
|
+
const details =
|
|
7342
|
+
queueChipText !== undefined
|
|
7343
|
+
? ({
|
|
7344
|
+
...((message.details && typeof message.details === "object" ? message.details : {}) as Record<
|
|
7345
|
+
string,
|
|
7346
|
+
unknown
|
|
7347
|
+
>),
|
|
7348
|
+
__queueChipText: queueChipText,
|
|
7349
|
+
} as T)
|
|
7350
|
+
: message.details;
|
|
7351
|
+
const appMessage: CustomMessage<T> = {
|
|
7352
|
+
role: "custom",
|
|
7353
|
+
customType: message.customType,
|
|
7354
|
+
content: message.content,
|
|
7355
|
+
display: message.display,
|
|
7356
|
+
details,
|
|
7357
|
+
attribution: message.attribution ?? "agent",
|
|
7358
|
+
timestamp: Date.now(),
|
|
7359
|
+
};
|
|
7360
|
+
const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
|
|
7361
|
+
if (deliverAs === "followUp") {
|
|
7362
|
+
this.agent.followUp(normalizedAppMessage);
|
|
7363
|
+
} else {
|
|
7364
|
+
this.agent.steer(normalizedAppMessage);
|
|
7365
|
+
}
|
|
7366
|
+
this.#scheduleIdleQueueDrain();
|
|
7367
|
+
}
|
|
7368
|
+
|
|
6990
7369
|
/**
|
|
6991
7370
|
* Send a custom message to the session. Creates a CustomMessageEntry.
|
|
6992
7371
|
*
|
|
@@ -7229,6 +7608,74 @@ export class AgentSession {
|
|
|
7229
7608
|
this.#todoPhases = this.#cloneTodoPhases(phases);
|
|
7230
7609
|
}
|
|
7231
7610
|
|
|
7611
|
+
#isTodoInitResult(details: Record<string, unknown>, toolCallId: string | undefined): boolean {
|
|
7612
|
+
const detailOp = getStringProperty(details, "op");
|
|
7613
|
+
if (detailOp) return detailOp === "init";
|
|
7614
|
+
if (!toolCallId) return false;
|
|
7615
|
+
for (let i = this.agent.state.messages.length - 1; i >= 0; i--) {
|
|
7616
|
+
const message = this.agent.state.messages[i];
|
|
7617
|
+
if (!message) continue;
|
|
7618
|
+
const op = toolCallOpFromMessage(message, toolCallId);
|
|
7619
|
+
if (op) return op === "init";
|
|
7620
|
+
}
|
|
7621
|
+
return false;
|
|
7622
|
+
}
|
|
7623
|
+
|
|
7624
|
+
#buildReplanTitleContext(): string {
|
|
7625
|
+
const turns: TitleConversationTurn[] = [];
|
|
7626
|
+
for (
|
|
7627
|
+
let i = this.agent.state.messages.length - 1;
|
|
7628
|
+
i >= 0 && turns.length < REPLAN_TITLE_CONTEXT_TURN_LIMIT;
|
|
7629
|
+
i--
|
|
7630
|
+
) {
|
|
7631
|
+
const message = this.agent.state.messages[i];
|
|
7632
|
+
if (!message) continue;
|
|
7633
|
+
const turn = titleConversationTurnFromMessage(message);
|
|
7634
|
+
if (turn) turns.push(turn);
|
|
7635
|
+
}
|
|
7636
|
+
turns.reverse();
|
|
7637
|
+
return formatTitleConversationContext(turns);
|
|
7638
|
+
}
|
|
7639
|
+
|
|
7640
|
+
#scheduleReplanTitleRefresh(): void {
|
|
7641
|
+
if (this.#replanTitleRefreshInFlight) return;
|
|
7642
|
+
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
7643
|
+
if (this.sessionManager.titleSource === "user") return;
|
|
7644
|
+
const context = this.#buildReplanTitleContext();
|
|
7645
|
+
if (!context) return;
|
|
7646
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
7647
|
+
const refresh = this.#refreshTitleAfterReplan(context, sessionId)
|
|
7648
|
+
.catch(err => {
|
|
7649
|
+
logger.warn("title-generator: replan refresh failed", {
|
|
7650
|
+
sessionId,
|
|
7651
|
+
error: err instanceof Error ? err.message : String(err),
|
|
7652
|
+
});
|
|
7653
|
+
})
|
|
7654
|
+
.finally(() => {
|
|
7655
|
+
if (this.#replanTitleRefreshInFlight === refresh) {
|
|
7656
|
+
this.#replanTitleRefreshInFlight = undefined;
|
|
7657
|
+
}
|
|
7658
|
+
});
|
|
7659
|
+
this.#replanTitleRefreshInFlight = refresh;
|
|
7660
|
+
}
|
|
7661
|
+
|
|
7662
|
+
async #refreshTitleAfterReplan(context: string, sessionId: string): Promise<void> {
|
|
7663
|
+
const title = await generateSessionTitle(
|
|
7664
|
+
context,
|
|
7665
|
+
this.#modelRegistry,
|
|
7666
|
+
this.settings,
|
|
7667
|
+
sessionId,
|
|
7668
|
+
this.model,
|
|
7669
|
+
provider => this.agent.metadataForProvider(provider),
|
|
7670
|
+
);
|
|
7671
|
+
if (!title) return;
|
|
7672
|
+
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
7673
|
+
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
7674
|
+
if (this.sessionManager.titleSource === "user") return;
|
|
7675
|
+
const setSessionName = this.sessionManager.setSessionName as SetSessionNameWithTrigger;
|
|
7676
|
+
await setSessionName.call(this.sessionManager, title, "auto", "replan");
|
|
7677
|
+
}
|
|
7678
|
+
|
|
7232
7679
|
#syncTodoPhasesFromBranch(): void {
|
|
7233
7680
|
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
7234
7681
|
// Strip completed/abandoned tasks — they were done in a previous run,
|
|
@@ -7267,6 +7714,7 @@ export class AgentSession {
|
|
|
7267
7714
|
preserveCompaction?: boolean;
|
|
7268
7715
|
}): Promise<void> {
|
|
7269
7716
|
const userInterrupt = options?.reason === USER_INTERRUPT_LABEL;
|
|
7717
|
+
this.#pendingAbortErrorId = userInterrupt ? AIError.create(AIError.Flag.UserInterrupt) : undefined;
|
|
7270
7718
|
if (userInterrupt) this.#advisorAutoResumeSuppressed = true;
|
|
7271
7719
|
// Pull advisor concerns out of the steer/follow-up queues before any await so
|
|
7272
7720
|
// the post-abort stranded-message drain can't auto-resume the run on them.
|
|
@@ -7370,9 +7818,11 @@ export class AgentSession {
|
|
|
7370
7818
|
// running advisor turn could otherwise finish, emit `message_end`, and recreate
|
|
7371
7819
|
// `<old>/__advisor.jsonl`. #resetAdvisorSessionState (after newSession) re-primes
|
|
7372
7820
|
// the advisor and re-attaches the feed at the new session's path.
|
|
7373
|
-
this.#
|
|
7374
|
-
|
|
7375
|
-
|
|
7821
|
+
for (const a of this.#advisors) {
|
|
7822
|
+
a.agentUnsubscribe?.();
|
|
7823
|
+
a.agentUnsubscribe = undefined;
|
|
7824
|
+
await a.recorder.close();
|
|
7825
|
+
}
|
|
7376
7826
|
try {
|
|
7377
7827
|
await this.sessionManager.dropSession(previousSessionFile);
|
|
7378
7828
|
} catch (err) {
|
|
@@ -7426,8 +7876,9 @@ export class AgentSession {
|
|
|
7426
7876
|
/**
|
|
7427
7877
|
* Set a display name for the current session.
|
|
7428
7878
|
*/
|
|
7429
|
-
setSessionName(name: string, source: "auto" | "user" = "auto"): Promise<boolean> {
|
|
7430
|
-
|
|
7879
|
+
setSessionName(name: string, source: "auto" | "user" = "auto", trigger?: SessionNameTrigger): Promise<boolean> {
|
|
7880
|
+
const setSessionName = this.sessionManager.setSessionName as SetSessionNameWithTrigger;
|
|
7881
|
+
return setSessionName.call(this.sessionManager, name, source, trigger);
|
|
7431
7882
|
}
|
|
7432
7883
|
|
|
7433
7884
|
/**
|
|
@@ -7505,16 +7956,24 @@ export class AgentSession {
|
|
|
7505
7956
|
/**
|
|
7506
7957
|
* Set model directly.
|
|
7507
7958
|
* Validates that a credential source is configured (synchronously, without
|
|
7508
|
-
* refreshing OAuth or running command-backed key programs)
|
|
7509
|
-
*
|
|
7510
|
-
*
|
|
7959
|
+
* refreshing OAuth or running command-backed key programs). The active
|
|
7960
|
+
* session switches by default; when `currentContextTokens` is provided and
|
|
7961
|
+
* exceeds the refreshed candidate's context window, the live switch is
|
|
7962
|
+
* skipped while role persistence still runs. Returns whether the active
|
|
7963
|
+
* model actually switched, computed against the refreshed metadata so
|
|
7964
|
+
* dynamic providers (e.g. llama.cpp) honor their post-load contextWindow.
|
|
7511
7965
|
* @throws Error if no API key available for the model
|
|
7512
7966
|
*/
|
|
7513
7967
|
async setModel(
|
|
7514
7968
|
model: Model,
|
|
7515
7969
|
role: string = "default",
|
|
7516
|
-
options?: {
|
|
7517
|
-
|
|
7970
|
+
options?: {
|
|
7971
|
+
selector?: string;
|
|
7972
|
+
thinkingLevel?: ThinkingLevel;
|
|
7973
|
+
persist?: boolean;
|
|
7974
|
+
currentContextTokens?: number;
|
|
7975
|
+
},
|
|
7976
|
+
): Promise<{ switched: boolean }> {
|
|
7518
7977
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
7519
7978
|
if (!this.#modelRegistry.hasConfiguredAuth(model)) {
|
|
7520
7979
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -7522,21 +7981,35 @@ export class AgentSession {
|
|
|
7522
7981
|
|
|
7523
7982
|
const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
|
|
7524
7983
|
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7984
|
+
const currentContextTokens = options?.currentContextTokens ?? 0;
|
|
7985
|
+
const targetContextWindow = targetModel.contextWindow ?? 0;
|
|
7986
|
+
const switched = !(
|
|
7987
|
+
currentContextTokens > 0 &&
|
|
7988
|
+
targetContextWindow > 0 &&
|
|
7989
|
+
currentContextTokens > targetContextWindow
|
|
7990
|
+
);
|
|
7991
|
+
|
|
7992
|
+
if (switched) {
|
|
7993
|
+
this.#clearActiveRetryFallback();
|
|
7994
|
+
this.#setModelWithProviderSessionReset(targetModel);
|
|
7995
|
+
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
7996
|
+
}
|
|
7528
7997
|
if (options?.persist) {
|
|
7529
7998
|
this.settings.setModelRole(
|
|
7530
7999
|
role,
|
|
7531
8000
|
this.#formatRoleModelValue(role, targetModel, options.selector, options.thinkingLevel),
|
|
7532
8001
|
);
|
|
7533
8002
|
}
|
|
8003
|
+
if (!switched) {
|
|
8004
|
+
return { switched: false };
|
|
8005
|
+
}
|
|
7534
8006
|
this.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
7535
8007
|
|
|
7536
8008
|
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
7537
8009
|
// configured defaultLevel; otherwise preserve the current level (or auto).
|
|
7538
8010
|
this.#reapplyThinkingLevel(targetModel.thinking?.defaultLevel);
|
|
7539
8011
|
await this.#syncAfterModelChange(previousEditMode);
|
|
8012
|
+
return { switched: true };
|
|
7540
8013
|
}
|
|
7541
8014
|
|
|
7542
8015
|
/**
|
|
@@ -8061,7 +8534,7 @@ export class AgentSession {
|
|
|
8061
8534
|
await this.sessionManager.rewriteEntries();
|
|
8062
8535
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8063
8536
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8064
|
-
this.#
|
|
8537
|
+
this.#resetAllAdvisorRuntimes();
|
|
8065
8538
|
this.#syncTodoPhasesFromBranch();
|
|
8066
8539
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8067
8540
|
return result;
|
|
@@ -8098,7 +8571,7 @@ export class AgentSession {
|
|
|
8098
8571
|
|
|
8099
8572
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8100
8573
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8101
|
-
this.#
|
|
8574
|
+
this.#resetAllAdvisorRuntimes();
|
|
8102
8575
|
this.#syncTodoPhasesFromBranch();
|
|
8103
8576
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8104
8577
|
return result;
|
|
@@ -8149,7 +8622,7 @@ export class AgentSession {
|
|
|
8149
8622
|
await this.sessionManager.rewriteEntries();
|
|
8150
8623
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8151
8624
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8152
|
-
this.#
|
|
8625
|
+
this.#resetAllAdvisorRuntimes();
|
|
8153
8626
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8154
8627
|
return { removed };
|
|
8155
8628
|
}
|
|
@@ -8206,7 +8679,7 @@ export class AgentSession {
|
|
|
8206
8679
|
await this.sessionManager.rewriteEntries();
|
|
8207
8680
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8208
8681
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8209
|
-
this.#
|
|
8682
|
+
this.#resetAllAdvisorRuntimes();
|
|
8210
8683
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8211
8684
|
|
|
8212
8685
|
return {
|
|
@@ -8302,7 +8775,11 @@ export class AgentSession {
|
|
|
8302
8775
|
compactionCandidates = this.#getCompactionModelCandidates(availableModels);
|
|
8303
8776
|
}
|
|
8304
8777
|
const pathEntries = this.sessionManager.getBranch();
|
|
8305
|
-
const preparation = prepareCompaction(
|
|
8778
|
+
const preparation = prepareCompaction(
|
|
8779
|
+
pathEntries,
|
|
8780
|
+
effectiveSettings,
|
|
8781
|
+
await this.#runnableCompactionCandidates(compactionCandidates, this.sessionId),
|
|
8782
|
+
);
|
|
8306
8783
|
if (!preparation) {
|
|
8307
8784
|
// Check why we can't compact
|
|
8308
8785
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -8492,7 +8969,7 @@ export class AgentSession {
|
|
|
8492
8969
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
8493
8970
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
8494
8971
|
this.#planReferenceSent = false;
|
|
8495
|
-
this.#
|
|
8972
|
+
this.#resetAllAdvisorRuntimes();
|
|
8496
8973
|
this.#syncTodoPhasesFromBranch();
|
|
8497
8974
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
8498
8975
|
|
|
@@ -8756,7 +9233,7 @@ export class AgentSession {
|
|
|
8756
9233
|
// Rebuild agent messages from session
|
|
8757
9234
|
const sessionContext = this.buildDisplaySessionContext();
|
|
8758
9235
|
this.agent.replaceMessages(sessionContext.messages);
|
|
8759
|
-
this.#
|
|
9236
|
+
this.#resetAllAdvisorRuntimes();
|
|
8760
9237
|
this.#syncTodoPhasesFromBranch();
|
|
8761
9238
|
|
|
8762
9239
|
return { document: handoffText, savedPath };
|
|
@@ -8973,7 +9450,7 @@ export class AgentSession {
|
|
|
8973
9450
|
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
8974
9451
|
const errorIsFromBeforeCompaction =
|
|
8975
9452
|
compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
|
|
8976
|
-
if (sameModel && !errorIsFromBeforeCompaction && isContextOverflow(assistantMessage, contextWindow)) {
|
|
9453
|
+
if (sameModel && !errorIsFromBeforeCompaction && AIError.isContextOverflow(assistantMessage, contextWindow)) {
|
|
8977
9454
|
// Remove the error message from agent state (it IS saved to session for history,
|
|
8978
9455
|
// but we don't want it in context for the retry)
|
|
8979
9456
|
const messages = this.agent.state.messages;
|
|
@@ -10076,6 +10553,17 @@ export class AgentSession {
|
|
|
10076
10553
|
return this.#resolveCompactionModelCandidates(this.model, availableModels, filter);
|
|
10077
10554
|
}
|
|
10078
10555
|
|
|
10556
|
+
/**
|
|
10557
|
+
* Compaction candidates that can actually run — those with a resolvable API
|
|
10558
|
+
* key, matching the per-candidate getApiKey gate the execution loop applies.
|
|
10559
|
+
* Re-expansion reusability (prepareCompaction) must judge remote-preserve
|
|
10560
|
+
* reuse against these, not against candidates the loop would skip at runtime.
|
|
10561
|
+
*/
|
|
10562
|
+
async #runnableCompactionCandidates(candidates: readonly Model[], sessionId: string | undefined): Promise<Model[]> {
|
|
10563
|
+
const keys = await Promise.all(candidates.map(model => this.#modelRegistry.getApiKey(model, sessionId)));
|
|
10564
|
+
return candidates.filter((_, index) => keys[index] !== undefined);
|
|
10565
|
+
}
|
|
10566
|
+
|
|
10079
10567
|
#resolveCompactionModelCandidates(
|
|
10080
10568
|
preferredModel: Model | null | undefined,
|
|
10081
10569
|
availableModels: Model[],
|
|
@@ -10172,6 +10660,9 @@ export class AgentSession {
|
|
|
10172
10660
|
// via resolveCompactionEffort so unsupported-effort models
|
|
10173
10661
|
// (xai-oauth/grok-build) don't trip requireSupportedEffort.
|
|
10174
10662
|
thinkingLevel: this.thinkingLevel,
|
|
10663
|
+
tools: this.agent.state.tools,
|
|
10664
|
+
sessionId: this.sessionId,
|
|
10665
|
+
promptCacheKey: this.sessionId,
|
|
10175
10666
|
},
|
|
10176
10667
|
);
|
|
10177
10668
|
} catch (error) {
|
|
@@ -10491,15 +10982,21 @@ export class AgentSession {
|
|
|
10491
10982
|
|
|
10492
10983
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
10493
10984
|
// LLM call would hit the same overflow. "incomplete" is an output-side problem,
|
|
10494
|
-
// so a handoff request on the existing context is still viable.
|
|
10495
|
-
// a local-only strategy: if it cannot run, report the local blocker instead of
|
|
10496
|
-
// silently swapping in a provider-backed summary.
|
|
10985
|
+
// so a handoff request on the existing context is still viable.
|
|
10497
10986
|
let action: "context-full" | "handoff" | "snapcompact" =
|
|
10498
10987
|
compactionSettings.strategy === "snapcompact"
|
|
10499
10988
|
? "snapcompact"
|
|
10500
10989
|
: compactionSettings.strategy === "handoff" && reason !== "overflow" && !suppressHandoff
|
|
10501
10990
|
? "handoff"
|
|
10502
10991
|
: "context-full";
|
|
10992
|
+
if (action === "snapcompact" && this.model && !this.model.input.includes("image")) {
|
|
10993
|
+
this.emitNotice(
|
|
10994
|
+
"warning",
|
|
10995
|
+
`snapcompact needs a vision-capable active model (${this.model.id} is text-only); using context-full auto-compaction instead.`,
|
|
10996
|
+
"compaction",
|
|
10997
|
+
);
|
|
10998
|
+
action = "context-full";
|
|
10999
|
+
}
|
|
10503
11000
|
// Abort any older auto-compaction before installing this run's controller.
|
|
10504
11001
|
this.#autoCompactionAbortController?.abort();
|
|
10505
11002
|
const autoCompactionAbortController = new AbortController();
|
|
@@ -10578,7 +11075,11 @@ export class AgentSession {
|
|
|
10578
11075
|
|
|
10579
11076
|
const pathEntries = this.sessionManager.getBranch();
|
|
10580
11077
|
|
|
10581
|
-
const
|
|
11078
|
+
const autoCompactionCandidates = await this.#runnableCompactionCandidates(
|
|
11079
|
+
this.#getCompactionModelCandidates(availableModels),
|
|
11080
|
+
this.sessionId,
|
|
11081
|
+
);
|
|
11082
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, autoCompactionCandidates);
|
|
10582
11083
|
if (!preparation) {
|
|
10583
11084
|
await this.#emitSessionEvent({
|
|
10584
11085
|
type: "auto_compaction_end",
|
|
@@ -10641,21 +11142,15 @@ export class AgentSession {
|
|
|
10641
11142
|
// + a summary message carrying the imaged archive at FRAME_TOKEN_ESTIMATE
|
|
10642
11143
|
// per frame; #computeSnapcompactMaxFrames sizes the frame cap from the
|
|
10643
11144
|
// live window so we don't run snapcompact just to overflow every threshold
|
|
10644
|
-
// tick. Any local blocker
|
|
10645
|
-
//
|
|
11145
|
+
// tick. Any local blocker (non-ASCII transcript, kept-history too large,
|
|
11146
|
+
// post-render overflow) downgrades auto maintenance to a context-full LLM
|
|
11147
|
+
// summary instead of wedging the session (#3659) — auto runs the default
|
|
11148
|
+
// strategy on the user's behalf, so a fallback that lets the session keep
|
|
11149
|
+
// running is the right behavior. Manual `/compact snapcompact` keeps the
|
|
11150
|
+
// local-only contract (#3599): the user explicitly picked it.
|
|
10646
11151
|
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
|
11152
|
+
let snapcompactBlocker: string | undefined;
|
|
10647
11153
|
if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
|
|
10648
|
-
if (!this.model?.input.includes("image")) {
|
|
10649
|
-
logger.warn("Snapcompact compaction requires a vision-capable model", {
|
|
10650
|
-
model: this.model?.id,
|
|
10651
|
-
});
|
|
10652
|
-
this.emitNotice(
|
|
10653
|
-
"warning",
|
|
10654
|
-
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only). No LLM fallback was attempted.`,
|
|
10655
|
-
"compaction",
|
|
10656
|
-
);
|
|
10657
|
-
throw new Error(`snapcompact cannot run locally: ${this.model?.id ?? "unknown"} is text-only.`);
|
|
10658
|
-
}
|
|
10659
11154
|
const text = snapcompact.serializeConversation(
|
|
10660
11155
|
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
|
10661
11156
|
);
|
|
@@ -10666,55 +11161,46 @@ export class AgentSession {
|
|
|
10666
11161
|
model: this.model?.id,
|
|
10667
11162
|
unrenderableRatio: renderScan.unrenderableRatio,
|
|
10668
11163
|
});
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
throw new Error(
|
|
10675
|
-
`snapcompact cannot render this conversation locally: high non-ASCII rate detected (${percent}%).`,
|
|
10676
|
-
);
|
|
10677
|
-
}
|
|
10678
|
-
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
|
|
10679
|
-
if (maxFrames < 1) {
|
|
10680
|
-
logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
|
|
10681
|
-
model: this.model?.id,
|
|
10682
|
-
});
|
|
10683
|
-
this.emitNotice(
|
|
10684
|
-
"warning",
|
|
10685
|
-
"snapcompact: kept history alone exceeds the context budget. No LLM fallback was attempted.",
|
|
10686
|
-
"compaction",
|
|
10687
|
-
);
|
|
10688
|
-
throw new Error("snapcompact cannot run locally: kept history alone exceeds the context budget.");
|
|
10689
|
-
}
|
|
10690
|
-
snapcompactResult = await snapcompact.compact(preparation, {
|
|
10691
|
-
convertToLlm,
|
|
10692
|
-
model: this.model,
|
|
10693
|
-
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
10694
|
-
maxFrames,
|
|
10695
|
-
});
|
|
10696
|
-
|
|
10697
|
-
if (snapcompactResult) {
|
|
10698
|
-
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
10699
|
-
const budget =
|
|
10700
|
-
ctxWindow > 0
|
|
10701
|
-
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
|
10702
|
-
: Number.POSITIVE_INFINITY;
|
|
10703
|
-
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
|
10704
|
-
if (projected > budget) {
|
|
10705
|
-
logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
|
|
11164
|
+
snapcompactBlocker = `snapcompact disabled: high non-ASCII rate detected (${percent}%); using context-full auto-compaction instead.`;
|
|
11165
|
+
} else {
|
|
11166
|
+
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
|
|
11167
|
+
if (maxFrames < 1) {
|
|
11168
|
+
logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
|
|
10706
11169
|
model: this.model?.id,
|
|
10707
|
-
projected,
|
|
10708
|
-
budget,
|
|
10709
11170
|
});
|
|
10710
|
-
|
|
10711
|
-
"
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
11171
|
+
snapcompactBlocker =
|
|
11172
|
+
"snapcompact: kept history alone exceeds the context budget; using context-full auto-compaction instead.";
|
|
11173
|
+
} else {
|
|
11174
|
+
snapcompactResult = await snapcompact.compact(preparation, {
|
|
11175
|
+
convertToLlm,
|
|
11176
|
+
model: this.model,
|
|
11177
|
+
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
11178
|
+
maxFrames,
|
|
11179
|
+
});
|
|
11180
|
+
if (snapcompactResult) {
|
|
11181
|
+
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
11182
|
+
const budget =
|
|
11183
|
+
ctxWindow > 0
|
|
11184
|
+
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
|
11185
|
+
: Number.POSITIVE_INFINITY;
|
|
11186
|
+
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
|
11187
|
+
if (projected > budget) {
|
|
11188
|
+
logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
|
|
11189
|
+
model: this.model?.id,
|
|
11190
|
+
projected,
|
|
11191
|
+
budget,
|
|
11192
|
+
});
|
|
11193
|
+
snapcompactBlocker =
|
|
11194
|
+
"snapcompact could not bring the context under the limit; using context-full auto-compaction instead.";
|
|
11195
|
+
snapcompactResult = undefined;
|
|
11196
|
+
}
|
|
11197
|
+
}
|
|
10716
11198
|
}
|
|
10717
11199
|
}
|
|
11200
|
+
if (snapcompactBlocker) {
|
|
11201
|
+
this.emitNotice("warning", snapcompactBlocker, "compaction");
|
|
11202
|
+
action = "context-full";
|
|
11203
|
+
}
|
|
10718
11204
|
}
|
|
10719
11205
|
|
|
10720
11206
|
if (compactionPrep.kind === "fromHook") {
|
|
@@ -10766,6 +11252,9 @@ export class AgentSession {
|
|
|
10766
11252
|
// site. Clamped per-model inside compact() via
|
|
10767
11253
|
// resolveCompactionEffort.
|
|
10768
11254
|
thinkingLevel: this.thinkingLevel,
|
|
11255
|
+
tools: this.agent.state.tools,
|
|
11256
|
+
sessionId: this.sessionId,
|
|
11257
|
+
promptCacheKey: this.sessionId,
|
|
10769
11258
|
},
|
|
10770
11259
|
);
|
|
10771
11260
|
break;
|
|
@@ -10775,11 +11264,12 @@ export class AgentSession {
|
|
|
10775
11264
|
}
|
|
10776
11265
|
|
|
10777
11266
|
const message = error instanceof Error ? error.message : String(error);
|
|
11267
|
+
const id = AIError.classify(error, candidate.api);
|
|
10778
11268
|
if (this.#isCompactionAuthFailure(error)) {
|
|
10779
11269
|
lastError = this.#buildCompactionAuthError();
|
|
10780
11270
|
break;
|
|
10781
11271
|
}
|
|
10782
|
-
if (
|
|
11272
|
+
if (AIError.is(id, AIError.Flag.Timeout)) {
|
|
10783
11273
|
logger.warn(
|
|
10784
11274
|
hasMoreCandidates
|
|
10785
11275
|
? "Auto-compaction summarization timed out, trying next model"
|
|
@@ -10798,8 +11288,8 @@ export class AgentSession {
|
|
|
10798
11288
|
retrySettings.enabled &&
|
|
10799
11289
|
attempt < retrySettings.maxRetries &&
|
|
10800
11290
|
(retryAfterMs !== undefined ||
|
|
10801
|
-
|
|
10802
|
-
|
|
11291
|
+
AIError.is(id, AIError.Flag.Transient) ||
|
|
11292
|
+
AIError.is(id, AIError.Flag.UsageLimit));
|
|
10803
11293
|
if (!shouldRetry) {
|
|
10804
11294
|
lastError = error;
|
|
10805
11295
|
break;
|
|
@@ -10881,7 +11371,7 @@ export class AgentSession {
|
|
|
10881
11371
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
10882
11372
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
10883
11373
|
this.#planReferenceSent = false;
|
|
10884
|
-
this.#
|
|
11374
|
+
this.#resetAllAdvisorRuntimes();
|
|
10885
11375
|
this.#syncTodoPhasesFromBranch();
|
|
10886
11376
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10887
11377
|
|
|
@@ -11198,6 +11688,31 @@ export class AgentSession {
|
|
|
11198
11688
|
// Auto-Retry
|
|
11199
11689
|
// =========================================================================
|
|
11200
11690
|
|
|
11691
|
+
/**
|
|
11692
|
+
* Classify retry decisions against the active session model. Test stream
|
|
11693
|
+
* shims and provider adapters can emit generic assistant metadata, but retry
|
|
11694
|
+
* policy belongs to the model that was actually requested for this turn.
|
|
11695
|
+
*/
|
|
11696
|
+
#classifyRetryMessage(message: AssistantMessage): number {
|
|
11697
|
+
const activeModel = this.model;
|
|
11698
|
+
if (!activeModel || message.api === activeModel.api) {
|
|
11699
|
+
return AIError.classifyMessage(message);
|
|
11700
|
+
}
|
|
11701
|
+
|
|
11702
|
+
const id = AIError.classifyMessage({
|
|
11703
|
+
api: activeModel.api,
|
|
11704
|
+
errorId: message.errorId,
|
|
11705
|
+
errorMessage: message.errorMessage,
|
|
11706
|
+
errorStatus: message.errorStatus,
|
|
11707
|
+
});
|
|
11708
|
+
message.errorId = id;
|
|
11709
|
+
return id;
|
|
11710
|
+
}
|
|
11711
|
+
|
|
11712
|
+
#isGenericAbortSentinel(message: AssistantMessage): boolean {
|
|
11713
|
+
return message.errorMessage === "Request was aborted" || message.errorMessage === "Request was aborted.";
|
|
11714
|
+
}
|
|
11715
|
+
|
|
11201
11716
|
/**
|
|
11202
11717
|
* Retry an empty, reason-less provider abort: a turn that ended `aborted`
|
|
11203
11718
|
* with no content and the generic sentinel (bare `abort()`), but only while
|
|
@@ -11210,14 +11725,22 @@ export class AgentSession {
|
|
|
11210
11725
|
* `prompt()`) or silently undo the guard's intended abort.
|
|
11211
11726
|
*/
|
|
11212
11727
|
#isRetryableReasonlessAbort(message: AssistantMessage): boolean {
|
|
11213
|
-
|
|
11214
|
-
message.stopReason
|
|
11215
|
-
message.content.length
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11728
|
+
if (
|
|
11729
|
+
message.stopReason !== "aborted" ||
|
|
11730
|
+
message.content.length !== 0 ||
|
|
11731
|
+
this.#abortInProgress ||
|
|
11732
|
+
this.#isDisposed ||
|
|
11733
|
+
this.#streamingEditAbortTriggered
|
|
11734
|
+
) {
|
|
11735
|
+
return false;
|
|
11736
|
+
}
|
|
11737
|
+
|
|
11738
|
+
const id = this.#classifyRetryMessage(message);
|
|
11739
|
+
if (AIError.is(id, AIError.Flag.Abort)) return true;
|
|
11740
|
+
if (!this.#isGenericAbortSentinel(message)) return false;
|
|
11741
|
+
|
|
11742
|
+
message.errorId = AIError.create(AIError.Flag.Abort);
|
|
11743
|
+
return true;
|
|
11221
11744
|
}
|
|
11222
11745
|
|
|
11223
11746
|
/**
|
|
@@ -11226,21 +11749,15 @@ export class AgentSession {
|
|
|
11226
11749
|
* Usage-limit errors are retryable because the retry handler performs credential switching.
|
|
11227
11750
|
*/
|
|
11228
11751
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
11229
|
-
if (message.stopReason !== "error"
|
|
11752
|
+
if (message.stopReason !== "error") return false;
|
|
11230
11753
|
|
|
11754
|
+
const id = this.#classifyRetryMessage(message);
|
|
11231
11755
|
// Context overflow is handled by compaction, not retry
|
|
11232
11756
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
11233
|
-
if (isContextOverflow(message, contextWindow)) return false;
|
|
11757
|
+
if (AIError.isContextOverflow(message, contextWindow)) return false;
|
|
11234
11758
|
|
|
11235
11759
|
if (this.#isClassifierRefusal(message)) return true;
|
|
11236
|
-
|
|
11237
|
-
if (this.#isMalformedFunctionCallError(message)) return true;
|
|
11238
|
-
if (this.#hasReplayUnsafeToolOutput(message)) return false;
|
|
11239
|
-
if (message.errorMessage.includes(THINKING_LOOP_ERROR_MARKER)) return true;
|
|
11240
|
-
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
11241
|
-
|
|
11242
|
-
const err = message.errorMessage;
|
|
11243
|
-
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
11760
|
+
return AIError.retriable(id, { replayUnsafe: this.#hasReplayUnsafeToolOutput(message) });
|
|
11244
11761
|
}
|
|
11245
11762
|
/**
|
|
11246
11763
|
* Retried turns remove the failed assistant message from active context.
|
|
@@ -11252,73 +11769,12 @@ export class AgentSession {
|
|
|
11252
11769
|
return message.content.some(block => block.type === "toolCall");
|
|
11253
11770
|
}
|
|
11254
11771
|
|
|
11255
|
-
#isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
|
|
11256
|
-
const currentApi = this.model?.api;
|
|
11257
|
-
if (
|
|
11258
|
-
message.api !== "openai-responses" &&
|
|
11259
|
-
message.api !== "openai-codex-responses" &&
|
|
11260
|
-
currentApi !== "openai-responses" &&
|
|
11261
|
-
currentApi !== "openai-codex-responses"
|
|
11262
|
-
) {
|
|
11263
|
-
return false;
|
|
11264
|
-
}
|
|
11265
|
-
|
|
11266
|
-
const errorMessage = message.errorMessage;
|
|
11267
|
-
if (!errorMessage) return false;
|
|
11268
|
-
|
|
11269
|
-
return (
|
|
11270
|
-
/\bItem with id ['"][^'"]+['"] not found\.?/i.test(errorMessage) ||
|
|
11271
|
-
(/previous[ _]?response/i.test(errorMessage) &&
|
|
11272
|
-
/not[ _]?found|invalid|expired|stale|zero[ _-]?data[ _-]?retention/i.test(errorMessage))
|
|
11273
|
-
);
|
|
11274
|
-
}
|
|
11275
|
-
|
|
11276
11772
|
#isClassifierRefusal(message: AssistantMessage): boolean {
|
|
11277
11773
|
if (message.stopReason !== "error") return false;
|
|
11278
11774
|
const stopType = message.stopDetails?.type;
|
|
11279
11775
|
return stopType === "refusal" || stopType === "sensitive";
|
|
11280
11776
|
}
|
|
11281
11777
|
|
|
11282
|
-
#isProviderErrorFinishReasonBeforeToolUse(message: AssistantMessage): boolean {
|
|
11283
|
-
if (!message.errorMessage) return false;
|
|
11284
|
-
if (message.content.some(block => block.type === "toolCall")) return false;
|
|
11285
|
-
return /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i.test(message.errorMessage);
|
|
11286
|
-
}
|
|
11287
|
-
|
|
11288
|
-
#isMalformedFunctionCallError(message: AssistantMessage): boolean {
|
|
11289
|
-
if (!message.errorMessage) return false;
|
|
11290
|
-
return /\bmalformed.?function.?call\b/i.test(message.errorMessage);
|
|
11291
|
-
}
|
|
11292
|
-
|
|
11293
|
-
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
11294
|
-
return (
|
|
11295
|
-
this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
|
|
11296
|
-
);
|
|
11297
|
-
}
|
|
11298
|
-
|
|
11299
|
-
#isTransientEnvelopeErrorMessage(errorMessage: string): boolean {
|
|
11300
|
-
// Match Anthropic stream-envelope failures that indicate a broken stream before any content starts.
|
|
11301
|
-
return /anthropic stream envelope error:/i.test(errorMessage) && /before message_start/i.test(errorMessage);
|
|
11302
|
-
}
|
|
11303
|
-
|
|
11304
|
-
#isCompactionSummarizationTimeoutMessage(errorMessage: string): boolean {
|
|
11305
|
-
return /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i.test(errorMessage);
|
|
11306
|
-
}
|
|
11307
|
-
|
|
11308
|
-
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
|
11309
|
-
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
11310
|
-
// service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
|
|
11311
|
-
// gateway upstream failures, terminated, retry delay exceeded, Bun HTTP/2 stream resets
|
|
11312
|
-
// (RST_STREAM / REFUSED_STREAM / ENHANCE_YOUR_CALM, surfaced verbatim from
|
|
11313
|
-
// src/http/h2_client/dispatch.zig)
|
|
11314
|
-
return (
|
|
11315
|
-
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
11316
|
-
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|malformed.?function.?call/i.test(
|
|
11317
|
-
errorMessage,
|
|
11318
|
-
)
|
|
11319
|
-
);
|
|
11320
|
-
}
|
|
11321
|
-
|
|
11322
11778
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
11323
11779
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
11324
11780
|
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
@@ -11551,20 +12007,15 @@ export class AgentSession {
|
|
|
11551
12007
|
#isFireworksFastFallbackEligible(message: AssistantMessage): boolean {
|
|
11552
12008
|
const model = this.#activeFireworksFastModel();
|
|
11553
12009
|
if (!model) return false;
|
|
11554
|
-
if (message.stopReason !== "error"
|
|
12010
|
+
if (message.stopReason !== "error") return false;
|
|
11555
12011
|
if (message.content.some(block => block.type === "toolCall")) return false;
|
|
11556
12012
|
// A content refusal/sensitivity stop is the model's decision, not a route
|
|
11557
12013
|
// failure — switching to the base model would just re-trigger it.
|
|
11558
12014
|
if (this.#isClassifierRefusal(message)) return false;
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
if (
|
|
11562
|
-
if (
|
|
11563
|
-
/\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i.test(
|
|
11564
|
-
err,
|
|
11565
|
-
)
|
|
11566
|
-
)
|
|
11567
|
-
return false;
|
|
12015
|
+
const id = this.#classifyRetryMessage(message);
|
|
12016
|
+
if (AIError.isContextOverflow(message, model.contextWindow ?? 0)) return false;
|
|
12017
|
+
if (AIError.is(id, AIError.Flag.UsageLimit)) return false;
|
|
12018
|
+
if (AIError.is(id, AIError.Flag.AuthFailed)) return false;
|
|
11568
12019
|
return this.#modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
|
|
11569
12020
|
}
|
|
11570
12021
|
|
|
@@ -11730,7 +12181,8 @@ export class AgentSession {
|
|
|
11730
12181
|
}
|
|
11731
12182
|
|
|
11732
12183
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
11733
|
-
const
|
|
12184
|
+
const id = this.#classifyRetryMessage(message);
|
|
12185
|
+
const staleOpenAIResponsesReplayError = AIError.is(id, AIError.Flag.StaleResponsesItem);
|
|
11734
12186
|
const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
|
|
11735
12187
|
let delayMs = staleOpenAIResponsesReplayError
|
|
11736
12188
|
? 0
|
|
@@ -11745,7 +12197,7 @@ export class AgentSession {
|
|
|
11745
12197
|
this.#resetCurrentResponsesProviderSession("stale replay error");
|
|
11746
12198
|
}
|
|
11747
12199
|
|
|
11748
|
-
if (this.model && !staleOpenAIResponsesReplayError &&
|
|
12200
|
+
if (this.model && !staleOpenAIResponsesReplayError && AIError.is(id, AIError.Flag.UsageLimit)) {
|
|
11749
12201
|
const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
11750
12202
|
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
11751
12203
|
this.model.provider,
|
|
@@ -11852,6 +12304,7 @@ export class AgentSession {
|
|
|
11852
12304
|
maxAttempts: retrySettings.maxRetries,
|
|
11853
12305
|
delayMs,
|
|
11854
12306
|
errorMessage,
|
|
12307
|
+
errorId: message.errorId,
|
|
11855
12308
|
});
|
|
11856
12309
|
|
|
11857
12310
|
// Remove the failed assistant message from active context before retrying.
|
|
@@ -12764,7 +13217,7 @@ export class AgentSession {
|
|
|
12764
13217
|
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
12765
13218
|
this.agent.serviceTier = previousServiceTier;
|
|
12766
13219
|
this.#syncTodoPhasesFromBranch();
|
|
12767
|
-
this.#
|
|
13220
|
+
this.#resetAllAdvisorRuntimes();
|
|
12768
13221
|
this.#reconnectToAgent();
|
|
12769
13222
|
if (restoreMcpError) {
|
|
12770
13223
|
throw restoreMcpError;
|
|
@@ -13166,10 +13619,11 @@ export class AgentSession {
|
|
|
13166
13619
|
let totalInput = 0;
|
|
13167
13620
|
let totalOutput = 0;
|
|
13168
13621
|
let totalCacheRead = 0;
|
|
13622
|
+
let totalReasoning = 0;
|
|
13169
13623
|
let totalCacheWrite = 0;
|
|
13170
13624
|
let totalCost = 0;
|
|
13171
|
-
|
|
13172
13625
|
let totalPremiumRequests = 0;
|
|
13626
|
+
|
|
13173
13627
|
const getTaskToolUsage = (details: unknown): Usage | undefined => {
|
|
13174
13628
|
if (!details || typeof details !== "object") return undefined;
|
|
13175
13629
|
const record = details as Record<string, unknown>;
|
|
@@ -13184,6 +13638,7 @@ export class AgentSession {
|
|
|
13184
13638
|
toolCalls += assistantMsg.content.filter(c => c.type === "toolCall").length;
|
|
13185
13639
|
totalInput += assistantMsg.usage.input;
|
|
13186
13640
|
totalOutput += assistantMsg.usage.output;
|
|
13641
|
+
totalReasoning += assistantMsg.usage.reasoningTokens ?? 0;
|
|
13187
13642
|
totalCacheRead += assistantMsg.usage.cacheRead;
|
|
13188
13643
|
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
|
13189
13644
|
totalPremiumRequests += assistantMsg.usage.premiumRequests ?? 0;
|
|
@@ -13195,6 +13650,7 @@ export class AgentSession {
|
|
|
13195
13650
|
if (usage) {
|
|
13196
13651
|
totalInput += usage.input;
|
|
13197
13652
|
totalOutput += usage.output;
|
|
13653
|
+
totalReasoning += usage.reasoningTokens ?? 0;
|
|
13198
13654
|
totalCacheRead += usage.cacheRead;
|
|
13199
13655
|
totalCacheWrite += usage.cacheWrite;
|
|
13200
13656
|
totalPremiumRequests += usage.premiumRequests ?? 0;
|
|
@@ -13214,6 +13670,7 @@ export class AgentSession {
|
|
|
13214
13670
|
tokens: {
|
|
13215
13671
|
input: totalInput,
|
|
13216
13672
|
output: totalOutput,
|
|
13673
|
+
reasoning: totalReasoning,
|
|
13217
13674
|
cacheRead: totalCacheRead,
|
|
13218
13675
|
cacheWrite: totalCacheWrite,
|
|
13219
13676
|
total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
|
|
@@ -13746,6 +14203,23 @@ export class AgentSession {
|
|
|
13746
14203
|
return this.setAdvisorEnabled(!this.#advisorEnabled);
|
|
13747
14204
|
}
|
|
13748
14205
|
|
|
14206
|
+
/**
|
|
14207
|
+
* Replace the live advisor roster from an edited `WATCHDOG.yml` (the `/advisor
|
|
14208
|
+
* configure` save path). Swaps the configs + shared baseline, then rebuilds the
|
|
14209
|
+
* runtimes in place so the change applies without a restart. When the advisor is
|
|
14210
|
+
* disabled the new configs are simply stored for the next enable.
|
|
14211
|
+
*
|
|
14212
|
+
* @returns the number of advisors active after the rebuild.
|
|
14213
|
+
*/
|
|
14214
|
+
applyAdvisorConfigs(advisors: AdvisorConfig[], sharedInstructions: string | undefined): number {
|
|
14215
|
+
this.#advisorConfigs = advisors;
|
|
14216
|
+
this.#advisorSharedInstructions = sharedInstructions;
|
|
14217
|
+
if (!this.#advisorEnabled) return 0;
|
|
14218
|
+
this.#stopAdvisorRuntime();
|
|
14219
|
+
this.#buildAdvisorRuntime(true);
|
|
14220
|
+
return this.#advisors.length;
|
|
14221
|
+
}
|
|
14222
|
+
|
|
13749
14223
|
/**
|
|
13750
14224
|
* Whether the advisor setting is enabled for this session.
|
|
13751
14225
|
*/
|
|
@@ -13760,7 +14234,28 @@ export class AgentSession {
|
|
|
13760
14234
|
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
|
13761
14235
|
*/
|
|
13762
14236
|
isAdvisorActive(): boolean {
|
|
13763
|
-
return this.#
|
|
14237
|
+
return this.#advisors.length > 0;
|
|
14238
|
+
}
|
|
14239
|
+
|
|
14240
|
+
/**
|
|
14241
|
+
* The names of the tools available to advisors this session (the pool a
|
|
14242
|
+
* `/advisor configure` editor lists). The advisor is a full agent, so this is the
|
|
14243
|
+
* full built tool set; a tool whose optional factory returns null (e.g. lsp with
|
|
14244
|
+
* no servers) is absent.
|
|
14245
|
+
*/
|
|
14246
|
+
getAdvisorAvailableToolNames(): string[] {
|
|
14247
|
+
return (this.#advisorTools ?? []).map(tool => tool.name);
|
|
14248
|
+
}
|
|
14249
|
+
|
|
14250
|
+
/**
|
|
14251
|
+
* The live advisor `Agent`, or `undefined` when no advisor runtime is
|
|
14252
|
+
* attached. Surfaced for diagnostics (`/dump advisor` already serializes
|
|
14253
|
+
* its transcript via {@link formatAdvisorHistoryAsText}) and so callers can
|
|
14254
|
+
* verify the advisor inherits the session's provider-shaping options
|
|
14255
|
+
* (`streamFn`, `promptCacheKey`, `providerSessionState`, ...).
|
|
14256
|
+
*/
|
|
14257
|
+
getAdvisorAgent(): Agent | undefined {
|
|
14258
|
+
return this.#advisors[0]?.agent;
|
|
13764
14259
|
}
|
|
13765
14260
|
|
|
13766
14261
|
/**
|
|
@@ -13768,23 +14263,59 @@ export class AgentSession {
|
|
|
13768
14263
|
*/
|
|
13769
14264
|
getAdvisorStats(): AdvisorStats {
|
|
13770
14265
|
const configured = this.#advisorEnabled;
|
|
13771
|
-
const
|
|
13772
|
-
if (
|
|
14266
|
+
const advisors = this.#advisors.map(a => this.#computeAdvisorStat(a));
|
|
14267
|
+
if (advisors.length === 0) {
|
|
13773
14268
|
return {
|
|
13774
14269
|
configured,
|
|
13775
14270
|
active: false,
|
|
13776
14271
|
contextWindow: 0,
|
|
13777
14272
|
contextTokens: 0,
|
|
13778
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14273
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
13779
14274
|
cost: 0,
|
|
13780
14275
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
14276
|
+
advisors: [],
|
|
13781
14277
|
};
|
|
13782
14278
|
}
|
|
13783
|
-
const
|
|
13784
|
-
const messages =
|
|
14279
|
+
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
14280
|
+
const messages = { user: 0, assistant: 0, total: 0 };
|
|
14281
|
+
let cost = 0;
|
|
14282
|
+
let contextTokens = 0;
|
|
14283
|
+
for (const a of advisors) {
|
|
14284
|
+
tokens.input += a.tokens.input;
|
|
14285
|
+
tokens.output += a.tokens.output;
|
|
14286
|
+
tokens.reasoning += a.tokens.reasoning;
|
|
14287
|
+
tokens.cacheRead += a.tokens.cacheRead;
|
|
14288
|
+
tokens.cacheWrite += a.tokens.cacheWrite;
|
|
14289
|
+
tokens.total += a.tokens.total;
|
|
14290
|
+
messages.user += a.messages.user;
|
|
14291
|
+
messages.assistant += a.messages.assistant;
|
|
14292
|
+
messages.total += a.messages.total;
|
|
14293
|
+
cost += a.cost;
|
|
14294
|
+
contextTokens += a.contextTokens;
|
|
14295
|
+
}
|
|
14296
|
+
// Single-advisor displays read the top-level model/window directly; surface the
|
|
14297
|
+
// first advisor's so the legacy status line stays byte-identical.
|
|
14298
|
+
return {
|
|
14299
|
+
configured,
|
|
14300
|
+
active: true,
|
|
14301
|
+
model: advisors[0].model,
|
|
14302
|
+
contextWindow: advisors[0].contextWindow,
|
|
14303
|
+
contextTokens,
|
|
14304
|
+
tokens,
|
|
14305
|
+
cost,
|
|
14306
|
+
messages,
|
|
14307
|
+
advisors,
|
|
14308
|
+
};
|
|
14309
|
+
}
|
|
14310
|
+
|
|
14311
|
+
/** Compute one advisor's stats slice (tokens, cost, context, message counts). */
|
|
14312
|
+
#computeAdvisorStat(advisor: ActiveAdvisor): PerAdvisorStat {
|
|
14313
|
+
const model = advisor.agent.state.model;
|
|
14314
|
+
const messages = advisor.agent.state.messages;
|
|
13785
14315
|
const contextTokens = this.#estimateAdvisorContextTokens(messages);
|
|
13786
14316
|
let input = 0;
|
|
13787
14317
|
let output = 0;
|
|
14318
|
+
let reasoning = 0;
|
|
13788
14319
|
let cacheRead = 0;
|
|
13789
14320
|
let cacheWrite = 0;
|
|
13790
14321
|
let cost = 0;
|
|
@@ -13797,24 +14328,18 @@ export class AgentSession {
|
|
|
13797
14328
|
const assistantMsg = message as AssistantMessage;
|
|
13798
14329
|
input += assistantMsg.usage.input;
|
|
13799
14330
|
output += assistantMsg.usage.output;
|
|
14331
|
+
reasoning += assistantMsg.usage.reasoningTokens ?? 0;
|
|
13800
14332
|
cacheRead += assistantMsg.usage.cacheRead;
|
|
13801
14333
|
cacheWrite += assistantMsg.usage.cacheWrite;
|
|
13802
14334
|
cost += assistantMsg.usage.cost.total;
|
|
13803
14335
|
}
|
|
13804
14336
|
}
|
|
13805
14337
|
return {
|
|
13806
|
-
|
|
13807
|
-
active: true,
|
|
14338
|
+
name: advisor.name,
|
|
13808
14339
|
model,
|
|
13809
14340
|
contextWindow: model.contextWindow ?? 0,
|
|
13810
14341
|
contextTokens,
|
|
13811
|
-
tokens: {
|
|
13812
|
-
input,
|
|
13813
|
-
output,
|
|
13814
|
-
cacheRead,
|
|
13815
|
-
cacheWrite,
|
|
13816
|
-
total: input + output + cacheRead + cacheWrite,
|
|
13817
|
-
},
|
|
14342
|
+
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: input + output + cacheRead + cacheWrite },
|
|
13818
14343
|
cost,
|
|
13819
14344
|
messages: { user, assistant, total: messages.length },
|
|
13820
14345
|
};
|
|
@@ -13830,19 +14355,30 @@ export class AgentSession {
|
|
|
13830
14355
|
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
13831
14356
|
: "Advisor is disabled.";
|
|
13832
14357
|
}
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
`${
|
|
13840
|
-
`${
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
14358
|
+
if (stats.advisors.length <= 1) {
|
|
14359
|
+
const s = stats.advisors[0];
|
|
14360
|
+
const contextLine =
|
|
14361
|
+
s.contextWindow > 0
|
|
14362
|
+
? `Context: ${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} tokens (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
14363
|
+
: `Context: ${s.contextTokens.toLocaleString()} tokens`;
|
|
14364
|
+
const spendParts = [`${s.tokens.input.toLocaleString()} input`, `${s.tokens.output.toLocaleString()} output`];
|
|
14365
|
+
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
14366
|
+
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
14367
|
+
const spendLine = `Spend: ${spendParts.join(", ")}, $${s.cost.toFixed(4)}`;
|
|
14368
|
+
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
14369
|
+
}
|
|
14370
|
+
const lines = [`Advisors enabled (${stats.advisors.length}):`];
|
|
14371
|
+
for (const s of stats.advisors) {
|
|
14372
|
+
const ctx =
|
|
14373
|
+
s.contextWindow > 0
|
|
14374
|
+
? `${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
14375
|
+
: `${s.contextTokens.toLocaleString()}`;
|
|
14376
|
+
lines.push(` • ${s.name} (${s.model.provider}/${s.model.id}) — context ${ctx} tokens, $${s.cost.toFixed(4)}`);
|
|
14377
|
+
}
|
|
14378
|
+
lines.push(
|
|
14379
|
+
`Totals: ${stats.tokens.input.toLocaleString()} input, ${stats.tokens.output.toLocaleString()} output, $${stats.cost.toFixed(4)}.`,
|
|
14380
|
+
);
|
|
14381
|
+
return lines.join("\n");
|
|
13846
14382
|
}
|
|
13847
14383
|
|
|
13848
14384
|
/**
|
|
@@ -13886,18 +14422,21 @@ export class AgentSession {
|
|
|
13886
14422
|
* {@link formatSessionAsText}. Returns null when no advisor is active.
|
|
13887
14423
|
*/
|
|
13888
14424
|
formatAdvisorHistoryAsText(options?: { compact?: boolean }): string | null {
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
14425
|
+
if (this.#advisors.length === 0) return null;
|
|
14426
|
+
const dump = (a: ActiveAdvisor): string =>
|
|
14427
|
+
options?.compact
|
|
14428
|
+
? formatSessionHistoryMarkdown(a.agent.state.messages)
|
|
14429
|
+
: formatSessionDumpText({
|
|
14430
|
+
messages: a.agent.state.messages,
|
|
14431
|
+
systemPrompt: a.agent.state.systemPrompt,
|
|
14432
|
+
model: a.agent.state.model,
|
|
14433
|
+
thinkingLevel: a.agent.state.thinkingLevel,
|
|
14434
|
+
tools: a.agent.state.tools,
|
|
14435
|
+
});
|
|
14436
|
+
if (this.#advisors.length === 1) return dump(this.#advisors[0]);
|
|
14437
|
+
return this.#advisors
|
|
14438
|
+
.map(a => `### Advisor: ${a.name} (${a.agent.state.model.provider}/${a.agent.state.model.id})\n\n${dump(a)}`)
|
|
14439
|
+
.join("\n\n");
|
|
13901
14440
|
}
|
|
13902
14441
|
|
|
13903
14442
|
// =========================================================================
|