@schlenkr/ragents 0.1.6 → 0.1.8
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/apps/server/src/chat-handler.ts +2 -0
- package/apps/server/src/config.ts +6 -0
- package/apps/server/src/host-api.json +1 -0
- package/apps/server/src/layout.ts +0 -15
- package/apps/server/src/plugin-support/agent-tool.ts +2 -3
- package/apps/server/src/plugin-support/plugin-folder.ts +2 -1
- package/apps/server/src/provider.ts +34 -11
- package/apps/server/src/ragents/actor-chat-history.ts +1 -0
- package/apps/server/src/ragents/engine.ts +23 -24
- package/apps/server/src/ragents/journal-chat-events.ts +2 -0
- package/apps/server/src/ragents/run-owner.ts +3 -2
- package/apps/server/src/ragents/session.ts +3 -3
- package/apps/server/src/run-preparation.ts +14 -18
- package/apps/server/src/run-transfer.ts +65 -4
- package/apps/server/src/settings.ts +2 -4
- package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-B9qYBARS.js → highlighted-body-KPVGNVTW-xXXhTDbh.js} +1 -1
- package/apps/web/dist/assets/{main-BCjvD2Hh.js → main-C7JeZFjs.js} +1 -1
- package/apps/web/dist/assets/{mermaid-HWGCJPDP-CvYYFK_n.js → mermaid-HWGCJPDP-DfsU5ofC.js} +69 -69
- package/apps/web/dist/assets/{runPanel-CG7kRMJh.js → runPanel-ZT6TPDLM.js} +1 -1
- package/apps/web/dist/help/guide-clients.html +13 -4
- package/apps/web/dist/help/guide-clients.md +14 -4
- package/apps/web/dist/help/guide-distributed.html +8 -3
- package/apps/web/dist/help/guide-distributed.md +8 -3
- package/apps/web/dist/help/guide-runtime.html +18 -12
- package/apps/web/dist/help/guide-runtime.md +19 -12
- package/apps/web/dist/help/mini-app.css +23 -3
- package/apps/web/dist/host-web.json +4 -4
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/run-panel.html +2 -2
- package/apps/web/src/SessionList.tsx +16 -8
- package/apps/web/src/StartTiles.tsx +1 -1
- package/apps/web/src/api.ts +2 -0
- package/apps/web/src/panel/PanelPage.tsx +1 -1
- package/apps/web/src/panel/RunLine.tsx +4 -2
- package/apps/web/src/panel/contract.ts +2 -0
- package/bundles/ragents.activity/ragents-bundle.json +1 -1
- package/bundles/ragents.actor-programs/ragents-bundle.json +1 -1
- package/bundles/ragents.ask/ragents-bundle.json +1 -1
- package/bundles/ragents.browser/ragents-bundle.json +1 -1
- package/bundles/ragents.documents/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
- package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
- package/bundles/ragents.orchestration/ragents-bundle.json +1 -1
- package/bundles/ragents.overseer/ragents-bundle.json +1 -1
- package/bundles/ragents.processes/ragents-bundle.json +1 -1
- package/bundles/ragents.product/ragents-bundle.json +1 -1
- package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
- package/bundles/ragents.reference/ragents-bundle.json +2 -2
- package/bundles/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
- package/bundles/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
- package/bundles/ragents.todo/ragents-bundle.json +1 -1
- package/bundles/ragents.transcript/ragents-bundle.json +1 -1
- package/bundles/ragents.watch/ragents-bundle.json +1 -1
- package/bundles/ragents.workspace/ragents-bundle.json +1 -1
- package/package.json +2 -2
- package/packages/agent/dist/core/agent-settings.d.ts +2 -2
- package/packages/agent/dist/core/compaction/compaction.d.ts +16 -50
- package/packages/agent/dist/core/context-log.d.ts +25 -0
- package/packages/agent/dist/core/messages.d.ts +3 -10
- package/packages/agent/dist/core/tool-definition.d.ts +17 -0
- package/packages/agent/dist/core/tools/bash.d.ts +2 -7
- package/packages/agent/dist/core/tools/edit.d.ts +1 -1
- package/packages/agent/dist/core/tools/read.d.ts +1 -1
- package/packages/agent/dist/core/tools/write.d.ts +1 -1
- package/packages/agent/dist/index.d.ts +7 -8
- package/packages/agent/dist/loop/agent.d.ts +2 -6
- package/packages/agent/package.json +1 -1
- package/packages/agent/src/core/agent-settings.ts +2 -2
- package/packages/agent/src/core/compaction/compaction.ts +49 -175
- package/packages/agent/src/core/context-log.ts +49 -0
- package/packages/agent/src/core/messages.ts +5 -11
- package/packages/agent/src/core/tool-definition.ts +29 -0
- package/packages/agent/src/core/tools/bash.ts +2 -9
- package/packages/agent/src/core/tools/edit.ts +2 -11
- package/packages/agent/src/core/tools/read.ts +3 -18
- package/packages/agent/src/core/tools/write.ts +1 -5
- package/packages/agent/src/index.ts +28 -14
- package/packages/agent/src/loop/agent-loop.ts +1 -2
- package/packages/agent/src/loop/agent.ts +8 -2
- package/packages/ai/dist/api/ai-sdk.d.ts +2 -0
- package/packages/ai/dist/types.d.ts +2 -0
- package/packages/ai/src/api/ai-sdk-messages.ts +15 -10
- package/packages/ai/src/api/ai-sdk.ts +11 -0
- package/packages/ai/src/api/transform-messages.ts +1 -1
- package/packages/ai/src/types.ts +2 -0
- package/packages/ai/src/utils/retry.ts +14 -0
- package/packages/ragents/src/agents/actor-input.ts +15 -0
- package/packages/ragents/src/agents/model-context.ts +360 -0
- package/packages/ragents/src/agents/plugins.ts +5 -0
- package/packages/ragents/src/agents/scheduler.ts +3 -7
- package/packages/ragents/src/agents/tools.ts +2 -2
- package/packages/ragents/src/agents/toolset.ts +47 -20
- package/packages/ragents/src/agents/workspaces.ts +0 -6
- package/packages/ragents/src/domain/event-semantics.ts +51 -0
- package/packages/ragents/src/domain/event-validation.ts +166 -0
- package/packages/ragents/src/domain/events.ts +72 -0
- package/packages/ragents/src/domain/model.ts +4 -2
- package/packages/ragents/src/domain/projection.ts +9 -0
- package/packages/ragents/src/domain/tool-result-text.ts +9 -0
- package/packages/ragents/src/drivers/agent-hooks.ts +25 -38
- package/packages/ragents/src/drivers/agent-runtime.ts +118 -933
- package/packages/ragents/src/drivers/agent-turn.ts +735 -0
- package/packages/ragents/src/drivers/agent.ts +9 -7
- package/packages/ragents/src/drivers/skill-preload.ts +53 -63
- package/packages/ragents/src/drivers/types.ts +21 -5
- package/packages/ragents/src/index.ts +8 -6
- package/packages/ragents/src/plugin-host.ts +60 -13
- package/packages/ragents/src/plugin-types.ts +7 -2
- package/packages/ragents/src/runtime/decisions/actors.ts +10 -0
- package/packages/ragents/src/runtime/decisions/turns.ts +66 -7
- package/packages/ragents/src/runtime/journal-storage.ts +7 -4
- package/packages/ragents/src/runtime/journal.ts +35 -0
- package/packages/ragents/src/runtime/orchestration.ts +140 -9
- package/packages/workspace-executor/src/sandbox-tools.ts +10 -11
- package/plugins/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
- package/plugins/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
- package/scripts/agent/journal.ts +5 -3
- package/scripts/package/publish-package.ts +4 -3
- package/packages/agent/dist/core/agent-session-runtime.d.ts +0 -33
- package/packages/agent/dist/core/agent-session-services.d.ts +0 -35
- package/packages/agent/dist/core/agent-session.d.ts +0 -234
- package/packages/agent/dist/core/auth-guidance.d.ts +0 -4
- package/packages/agent/dist/core/defaults.d.ts +0 -3
- package/packages/agent/dist/core/extensions/index.d.ts +0 -11
- package/packages/agent/dist/core/extensions/loader.d.ts +0 -12
- package/packages/agent/dist/core/extensions/runner.d.ts +0 -39
- package/packages/agent/dist/core/extensions/types.d.ts +0 -156
- package/packages/agent/dist/core/extensions/wrapper.d.ts +0 -12
- package/packages/agent/dist/core/resource-loader.d.ts +0 -39
- package/packages/agent/dist/core/sdk.d.ts +0 -38
- package/packages/agent/dist/core/session-manager.d.ts +0 -189
- package/packages/agent/dist/core/source-info.d.ts +0 -16
- package/packages/agent/dist/core/system-prompt.d.ts +0 -21
- package/packages/agent/src/core/agent-session-runtime.ts +0 -94
- package/packages/agent/src/core/agent-session-services.ts +0 -74
- package/packages/agent/src/core/agent-session.ts +0 -1108
- package/packages/agent/src/core/auth-guidance.ts +0 -14
- package/packages/agent/src/core/defaults.ts +0 -3
- package/packages/agent/src/core/extensions/index.ts +0 -38
- package/packages/agent/src/core/extensions/loader.ts +0 -125
- package/packages/agent/src/core/extensions/runner.ts +0 -211
- package/packages/agent/src/core/extensions/types.ts +0 -197
- package/packages/agent/src/core/extensions/wrapper.ts +0 -24
- package/packages/agent/src/core/resource-loader.ts +0 -142
- package/packages/agent/src/core/sdk.ts +0 -123
- package/packages/agent/src/core/session-manager.ts +0 -743
- package/packages/agent/src/core/source-info.ts +0 -28
- package/packages/agent/src/core/system-prompt.ts +0 -108
|
@@ -1,1108 +0,0 @@
|
|
|
1
|
-
/** One agent: model and thinking level, prompts, persistence, auto-compaction and retry, extension hooks and tools. */
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
Agent,
|
|
5
|
-
AgentEvent,
|
|
6
|
-
AgentMessage,
|
|
7
|
-
AgentTool,
|
|
8
|
-
PrepareNextTurnContext,
|
|
9
|
-
ThinkingLevel,
|
|
10
|
-
} from "../loop/index.ts";
|
|
11
|
-
import type { AssistantMessage, AuthResult, Model, ProviderHeaders, UserAttachment, UserContent } from "@ragents/ai";
|
|
12
|
-
import {
|
|
13
|
-
clampThinkingLevel,
|
|
14
|
-
getSupportedThinkingLevels,
|
|
15
|
-
isContextOverflow,
|
|
16
|
-
isRetryableAssistantError,
|
|
17
|
-
streamSimple,
|
|
18
|
-
} from "@ragents/ai";
|
|
19
|
-
import { sleep } from "../utils/sleep.ts";
|
|
20
|
-
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.ts";
|
|
21
|
-
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
|
22
|
-
import {
|
|
23
|
-
type CompactionResult,
|
|
24
|
-
calculateContextTokens,
|
|
25
|
-
compact,
|
|
26
|
-
estimateContextTokens,
|
|
27
|
-
estimateTokens,
|
|
28
|
-
prepareCompaction,
|
|
29
|
-
shouldCompact,
|
|
30
|
-
} from "./compaction/index.ts";
|
|
31
|
-
import {
|
|
32
|
-
type ExtensionErrorListener,
|
|
33
|
-
ExtensionRunner,
|
|
34
|
-
type ToolDefinition,
|
|
35
|
-
type ToolInfo,
|
|
36
|
-
wrapRegisteredTools,
|
|
37
|
-
} from "./extensions/index.ts";
|
|
38
|
-
import type { ModelRuntime } from "./model-runtime.ts";
|
|
39
|
-
import type { ResourceLoader } from "./resource-loader.ts";
|
|
40
|
-
import type { SessionManager } from "./session-manager.ts";
|
|
41
|
-
import { getLatestCompactionEntry } from "./session-manager.ts";
|
|
42
|
-
import type { AgentSettings } from "./agent-settings.ts";
|
|
43
|
-
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
|
44
|
-
import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.ts";
|
|
45
|
-
|
|
46
|
-
/** Session-specific events that extend the core AgentEvent */
|
|
47
|
-
export type AgentSessionEvent =
|
|
48
|
-
| Exclude<AgentEvent, { type: "agent_end" }>
|
|
49
|
-
| {
|
|
50
|
-
type: "agent_end";
|
|
51
|
-
messages: AgentMessage[];
|
|
52
|
-
willRetry: boolean;
|
|
53
|
-
}
|
|
54
|
-
| { type: "agent_settled" }
|
|
55
|
-
| { type: "compaction_start"; reason: "threshold" | "overflow" }
|
|
56
|
-
| { type: "thinking_level_changed"; level: ThinkingLevel }
|
|
57
|
-
| {
|
|
58
|
-
type: "compaction_end";
|
|
59
|
-
reason: "threshold" | "overflow";
|
|
60
|
-
result: CompactionResult | undefined;
|
|
61
|
-
aborted: boolean;
|
|
62
|
-
willRetry: boolean;
|
|
63
|
-
errorMessage?: string;
|
|
64
|
-
}
|
|
65
|
-
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
66
|
-
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string };
|
|
67
|
-
|
|
68
|
-
/** Listener function for agent session events */
|
|
69
|
-
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
70
|
-
|
|
71
|
-
function withoutDeletedHeaders(headers: ProviderHeaders | undefined): Record<string, string> | undefined {
|
|
72
|
-
return headers
|
|
73
|
-
? Object.fromEntries(Object.entries(headers).filter((entry): entry is [string, string] => entry[1] !== null))
|
|
74
|
-
: undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface AgentSessionConfig {
|
|
78
|
-
agent: Agent;
|
|
79
|
-
sessionManager: SessionManager;
|
|
80
|
-
settings: AgentSettings;
|
|
81
|
-
cwd: string;
|
|
82
|
-
/** Resource loader for extensions, skills and the system prompt */
|
|
83
|
-
resourceLoader: ResourceLoader;
|
|
84
|
-
/** SDK custom tools registered outside extensions */
|
|
85
|
-
customTools?: ToolDefinition[];
|
|
86
|
-
/** Canonical model/auth runtime. */
|
|
87
|
-
modelRuntime: ModelRuntime;
|
|
88
|
-
/** Optional allowlist of tool names. When provided, only these tool names are exposed. */
|
|
89
|
-
allowedToolNames?: string[];
|
|
90
|
-
/** Mutable ref used by Agent to access the current ExtensionRunner */
|
|
91
|
-
extensionRunnerRef?: { current?: ExtensionRunner };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface ExtensionBindings {
|
|
95
|
-
onError?: ExtensionErrorListener;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Options for AgentSession.prompt() */
|
|
99
|
-
export interface PromptOptions {
|
|
100
|
-
attachments?: UserAttachment[];
|
|
101
|
-
/** Observes whether the prompt was accepted or rejected before the agent run starts. */
|
|
102
|
-
preflightResult?: (success: boolean) => void;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface ToolDefinitionEntry {
|
|
106
|
-
definition: ToolDefinition;
|
|
107
|
-
sourceInfo: SourceInfo;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function estimateMessagesTokens(messages: AgentMessage[]): number {
|
|
111
|
-
let tokens = 0;
|
|
112
|
-
for (const message of messages) {
|
|
113
|
-
tokens += estimateTokens(message);
|
|
114
|
-
}
|
|
115
|
-
return tokens;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Standard thinking levels */
|
|
119
|
-
const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"];
|
|
120
|
-
|
|
121
|
-
export class AgentSession {
|
|
122
|
-
readonly agent: Agent;
|
|
123
|
-
readonly sessionManager: SessionManager;
|
|
124
|
-
readonly settings: AgentSettings;
|
|
125
|
-
|
|
126
|
-
// Event subscription state
|
|
127
|
-
private _unsubscribeAgent?: () => void;
|
|
128
|
-
private _eventListeners: AgentSessionEventListener[] = [];
|
|
129
|
-
private _isAgentRunActive = false;
|
|
130
|
-
private _disposed = false;
|
|
131
|
-
private _idleWaitPromise: Promise<void> | undefined;
|
|
132
|
-
private _resolveIdleWait: (() => void) | undefined;
|
|
133
|
-
|
|
134
|
-
// Compaction state
|
|
135
|
-
private _autoCompactionAbortController: AbortController | undefined = undefined;
|
|
136
|
-
private _overflowRecoveryAttempted = false;
|
|
137
|
-
|
|
138
|
-
// Retry state
|
|
139
|
-
private _retryAbortController: AbortController | undefined = undefined;
|
|
140
|
-
private _retryAttempt = 0;
|
|
141
|
-
|
|
142
|
-
// Extension system
|
|
143
|
-
private _extensionRunner!: ExtensionRunner;
|
|
144
|
-
|
|
145
|
-
private _resourceLoader: ResourceLoader;
|
|
146
|
-
private _customTools: ToolDefinition[];
|
|
147
|
-
private _cwd: string;
|
|
148
|
-
private _extensionRunnerRef?: { current?: ExtensionRunner };
|
|
149
|
-
private _allowedToolNames?: Set<string>;
|
|
150
|
-
private _extensionErrorUnsubscriber?: () => void;
|
|
151
|
-
|
|
152
|
-
private _modelRuntime: ModelRuntime;
|
|
153
|
-
|
|
154
|
-
// Tool registry for extension getAllTools/setActiveTools
|
|
155
|
-
private _toolRegistry: Map<string, AgentTool> = new Map();
|
|
156
|
-
private _toolDefinitions: Map<string, ToolDefinitionEntry> = new Map();
|
|
157
|
-
private _toolPromptSnippets: Map<string, string> = new Map();
|
|
158
|
-
private _toolPromptGuidelines: Map<string, string[]> = new Map();
|
|
159
|
-
|
|
160
|
-
// Base system prompt (without per-run extension changes)
|
|
161
|
-
private _baseSystemPrompt = "";
|
|
162
|
-
private _baseSystemPromptOptions!: BuildSystemPromptOptions;
|
|
163
|
-
private _systemPromptOverride?: string;
|
|
164
|
-
|
|
165
|
-
constructor(config: AgentSessionConfig) {
|
|
166
|
-
this.agent = config.agent;
|
|
167
|
-
this.sessionManager = config.sessionManager;
|
|
168
|
-
this.settings = config.settings;
|
|
169
|
-
this._resourceLoader = config.resourceLoader;
|
|
170
|
-
this._customTools = config.customTools ?? [];
|
|
171
|
-
this._cwd = config.cwd;
|
|
172
|
-
this._modelRuntime = config.modelRuntime;
|
|
173
|
-
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
174
|
-
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
175
|
-
|
|
176
|
-
// Always subscribe to agent events for internal handling
|
|
177
|
-
// (session persistence, auto-compaction, retry logic)
|
|
178
|
-
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
179
|
-
this._installAgentToolHooks();
|
|
180
|
-
this._installAgentNextTurnRefresh();
|
|
181
|
-
|
|
182
|
-
this._buildRuntime();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
private async _getRequiredRequestAuth(model: Model<any>): Promise<{
|
|
187
|
-
apiKey: string;
|
|
188
|
-
headers?: Record<string, string>;
|
|
189
|
-
env?: Record<string, string>;
|
|
190
|
-
}> {
|
|
191
|
-
let result: AuthResult | undefined;
|
|
192
|
-
try {
|
|
193
|
-
result = await this._modelRuntime.getAuth(model);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
const cause = error instanceof Error ? error.cause : undefined;
|
|
196
|
-
if (cause instanceof Error && cause.message === "authHeader requires a resolved API key") {
|
|
197
|
-
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
198
|
-
}
|
|
199
|
-
throw error;
|
|
200
|
-
}
|
|
201
|
-
if (result?.auth.apiKey) {
|
|
202
|
-
return {
|
|
203
|
-
apiKey: result.auth.apiKey,
|
|
204
|
-
headers: withoutDeletedHeaders(result.auth.headers),
|
|
205
|
-
env: result.env,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private async _getSummarizationRequestAuth(model: Model<any>): Promise<{
|
|
213
|
-
apiKey?: string;
|
|
214
|
-
headers?: Record<string, string>;
|
|
215
|
-
env?: Record<string, string>;
|
|
216
|
-
}> {
|
|
217
|
-
if (this.agent.streamFn === streamSimple) {
|
|
218
|
-
return this._getRequiredRequestAuth(model);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
const result = await this._modelRuntime.getAuth(model);
|
|
223
|
-
return result
|
|
224
|
-
? { apiKey: result.auth.apiKey, headers: withoutDeletedHeaders(result.auth.headers), env: result.env }
|
|
225
|
-
: {};
|
|
226
|
-
} catch {
|
|
227
|
-
return {};
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** The tool result hook reads `this._extensionRunner` at execution time. */
|
|
232
|
-
private _installAgentToolHooks(): void {
|
|
233
|
-
this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {
|
|
234
|
-
const runner = this._extensionRunner;
|
|
235
|
-
if (!runner.hasHandlers("tool_result")) {
|
|
236
|
-
return undefined;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const hookResult = await runner.emitToolResult({
|
|
240
|
-
type: "tool_result",
|
|
241
|
-
toolName: toolCall.name,
|
|
242
|
-
toolCallId: toolCall.id,
|
|
243
|
-
input: args as Record<string, unknown>,
|
|
244
|
-
content: result.content,
|
|
245
|
-
details: result.details,
|
|
246
|
-
isError,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
if (!hookResult) {
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
content: hookResult.content,
|
|
255
|
-
details: hookResult.details,
|
|
256
|
-
isError: hookResult.isError ?? isError,
|
|
257
|
-
};
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private _installAgentNextTurnRefresh(): void {
|
|
262
|
-
const previousPrepareNextTurnWithContext =
|
|
263
|
-
this.agent.prepareNextTurnWithContext ??
|
|
264
|
-
(this.agent.prepareNextTurn
|
|
265
|
-
? async (_turn: PrepareNextTurnContext, signal?: AbortSignal) => await this.agent.prepareNextTurn?.(signal)
|
|
266
|
-
: undefined);
|
|
267
|
-
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
268
|
-
const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
|
|
269
|
-
const previousContext = previousSnapshot?.context ?? turn.context;
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
...previousSnapshot,
|
|
273
|
-
context: {
|
|
274
|
-
...previousContext,
|
|
275
|
-
systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
|
|
276
|
-
tools: this.agent.state.tools.slice(),
|
|
277
|
-
},
|
|
278
|
-
model: this.agent.state.model,
|
|
279
|
-
thinkingLevel: this.agent.state.thinkingLevel,
|
|
280
|
-
};
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** Emit an event to all listeners */
|
|
285
|
-
private _emit(event: AgentSessionEvent): void {
|
|
286
|
-
for (const l of this._eventListeners) {
|
|
287
|
-
l(event);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private _getIdleWaitPromise(): Promise<void> {
|
|
292
|
-
if (!this._idleWaitPromise) {
|
|
293
|
-
this._idleWaitPromise = new Promise((resolve) => {
|
|
294
|
-
this._resolveIdleWait = resolve;
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
return this._idleWaitPromise;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private _resolveIdleWaitIfIdle(): void {
|
|
301
|
-
if (this._isAgentRunActive || !this._resolveIdleWait) {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
const resolve = this._resolveIdleWait;
|
|
305
|
-
this._idleWaitPromise = undefined;
|
|
306
|
-
this._resolveIdleWait = undefined;
|
|
307
|
-
resolve();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
private _emitAgentSettled(): void {
|
|
311
|
-
this._isAgentRunActive = false;
|
|
312
|
-
try {
|
|
313
|
-
this._emit({ type: "agent_settled" });
|
|
314
|
-
} finally {
|
|
315
|
-
this._resolveIdleWaitIfIdle();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
private _lastAssistantMessage: AssistantMessage | undefined = undefined;
|
|
320
|
-
|
|
321
|
-
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
322
|
-
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
323
|
-
if (event.type === "message_start" && event.message.role === "user") {
|
|
324
|
-
this._overflowRecoveryAttempted = false;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Notify all listeners
|
|
328
|
-
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
|
329
|
-
|
|
330
|
-
// Handle session persistence
|
|
331
|
-
if (event.type === "message_end") {
|
|
332
|
-
if (event.message.role === "user" || event.message.role === "assistant" || event.message.role === "toolResult") {
|
|
333
|
-
this.sessionManager.appendMessage(event.message);
|
|
334
|
-
}
|
|
335
|
-
// Compaction summaries are persisted as compaction entries
|
|
336
|
-
|
|
337
|
-
// Track assistant message for auto-compaction (checked on agent_end)
|
|
338
|
-
if (event.message.role === "assistant") {
|
|
339
|
-
this._lastAssistantMessage = event.message;
|
|
340
|
-
|
|
341
|
-
const assistantMsg = event.message as AssistantMessage;
|
|
342
|
-
if (assistantMsg.stopReason !== "error") {
|
|
343
|
-
this._overflowRecoveryAttempted = false;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Reset retry counter immediately on successful assistant response
|
|
347
|
-
// This prevents accumulation across multiple LLM calls within a turn
|
|
348
|
-
if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
|
|
349
|
-
this._emit({
|
|
350
|
-
type: "auto_retry_end",
|
|
351
|
-
success: true,
|
|
352
|
-
attempt: this._retryAttempt,
|
|
353
|
-
});
|
|
354
|
-
this._retryAttempt = 0;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
private _willRetryAfterAgentEnd(event: Extract<AgentEvent, { type: "agent_end" }>): boolean {
|
|
361
|
-
const settings = this.settings.retry;
|
|
362
|
-
if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
367
|
-
const message = event.messages[i];
|
|
368
|
-
if (message.role === "assistant") {
|
|
369
|
-
return this._isRetryableError(message as AssistantMessage);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** Find the last assistant message in agent state (including aborted ones) */
|
|
376
|
-
private _findLastAssistantMessage(): AssistantMessage | undefined {
|
|
377
|
-
const messages = this.agent.state.messages;
|
|
378
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
379
|
-
const msg = messages[i];
|
|
380
|
-
if (msg.role === "assistant") {
|
|
381
|
-
return msg as AssistantMessage;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return undefined;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Subscribe to agent events.
|
|
389
|
-
* Session persistence is handled internally (saves messages on message_end).
|
|
390
|
-
* Multiple listeners can be added. Returns unsubscribe function for this listener.
|
|
391
|
-
*/
|
|
392
|
-
subscribe(listener: AgentSessionEventListener): () => void {
|
|
393
|
-
this._eventListeners.push(listener);
|
|
394
|
-
|
|
395
|
-
// Return unsubscribe function for this specific listener
|
|
396
|
-
return () => {
|
|
397
|
-
const index = this._eventListeners.indexOf(listener);
|
|
398
|
-
if (index !== -1) {
|
|
399
|
-
this._eventListeners.splice(index, 1);
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Remove all listeners and disconnect from agent.
|
|
406
|
-
* Call this when completely done with the session.
|
|
407
|
-
*/
|
|
408
|
-
dispose(): void {
|
|
409
|
-
if (this._disposed) return;
|
|
410
|
-
this._disposed = true;
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
this.abortRetry();
|
|
414
|
-
this._autoCompactionAbortController?.abort();
|
|
415
|
-
this.agent.abort();
|
|
416
|
-
} catch {
|
|
417
|
-
// Dispose must succeed even if an abort hook throws.
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
this._extensionRunner.invalidate();
|
|
421
|
-
this._unsubscribeAgent?.();
|
|
422
|
-
this._unsubscribeAgent = undefined;
|
|
423
|
-
this._eventListeners = [];
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
waitForSettlement(): Promise<void> {
|
|
427
|
-
return this._resourceLoader.waitForSettlement?.() ?? Promise.resolve();
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/** Current model (may be undefined if not yet selected) */
|
|
431
|
-
get model(): Model<any> | undefined {
|
|
432
|
-
return this.agent.state.model;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** Current thinking level */
|
|
436
|
-
get thinkingLevel(): ThinkingLevel {
|
|
437
|
-
return this.agent.state.thinkingLevel;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/** Whether the session is currently processing an agent run or post-run continuation. */
|
|
441
|
-
get isStreaming(): boolean {
|
|
442
|
-
return this._isAgentRunActive;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Whether the session has no active agent run, retry, auto-compaction, or queued continuation. */
|
|
446
|
-
get isIdle(): boolean {
|
|
447
|
-
return !this._isAgentRunActive;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** Replace the custom system prompt and rebuild the effective prompt for the next model call. */
|
|
451
|
-
setSystemPrompt(prompt: string): void {
|
|
452
|
-
this._resourceLoader.setSystemPrompt(prompt);
|
|
453
|
-
this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
|
|
454
|
-
this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Get the names of currently active tools.
|
|
459
|
-
* Returns the names of tools currently set on the agent.
|
|
460
|
-
*/
|
|
461
|
-
getActiveToolNames(): string[] {
|
|
462
|
-
return this.agent.state.tools.map((t) => t.name);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Get all configured tools with name, description, parameter schema, prompt guidelines, and source metadata.
|
|
467
|
-
*/
|
|
468
|
-
getAllTools(): ToolInfo[] {
|
|
469
|
-
return Array.from(this._toolDefinitions.values()).map(({ definition, sourceInfo }) => ({
|
|
470
|
-
name: definition.name,
|
|
471
|
-
description: definition.description,
|
|
472
|
-
parameters: definition.parameters,
|
|
473
|
-
promptGuidelines: definition.promptGuidelines,
|
|
474
|
-
sourceInfo,
|
|
475
|
-
}));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Set active tools by name.
|
|
480
|
-
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
481
|
-
* Also rebuilds the system prompt to reflect the new tool set.
|
|
482
|
-
* Changes take effect on the next agent turn.
|
|
483
|
-
*/
|
|
484
|
-
setActiveToolsByName(toolNames: string[]): void {
|
|
485
|
-
const tools: AgentTool[] = [];
|
|
486
|
-
const validToolNames: string[] = [];
|
|
487
|
-
for (const name of toolNames) {
|
|
488
|
-
const tool = this._toolRegistry.get(name);
|
|
489
|
-
if (tool) {
|
|
490
|
-
tools.push(tool);
|
|
491
|
-
validToolNames.push(name);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
this.agent.state.tools = tools;
|
|
495
|
-
|
|
496
|
-
// Rebuild base system prompt with new tool set
|
|
497
|
-
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
|
498
|
-
this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/** All messages including custom types */
|
|
502
|
-
get messages(): AgentMessage[] {
|
|
503
|
-
return this.agent.state.messages;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/** Current session file path, or undefined if sessions are disabled */
|
|
507
|
-
get sessionFile(): string | undefined {
|
|
508
|
-
return this.sessionManager.getSessionFile();
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/** Current session ID */
|
|
512
|
-
get sessionId(): string {
|
|
513
|
-
return this.sessionManager.getSessionId();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
private _normalizePromptSnippet(text: string | undefined): string | undefined {
|
|
517
|
-
if (!text) return undefined;
|
|
518
|
-
const oneLine = text
|
|
519
|
-
.replace(/[\r\n]+/g, " ")
|
|
520
|
-
.replace(/\s+/g, " ")
|
|
521
|
-
.trim();
|
|
522
|
-
return oneLine.length > 0 ? oneLine : undefined;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
private _normalizePromptGuidelines(guidelines: string[] | undefined): string[] {
|
|
526
|
-
if (!guidelines || guidelines.length === 0) {
|
|
527
|
-
return [];
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const unique = new Set<string>();
|
|
531
|
-
for (const guideline of guidelines) {
|
|
532
|
-
const normalized = guideline.trim();
|
|
533
|
-
if (normalized.length > 0) {
|
|
534
|
-
unique.add(normalized);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
return Array.from(unique);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
private _rebuildSystemPrompt(toolNames: string[]): string {
|
|
541
|
-
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
542
|
-
const toolSnippets: Record<string, string> = {};
|
|
543
|
-
const promptGuidelines: string[] = [];
|
|
544
|
-
for (const name of validToolNames) {
|
|
545
|
-
const snippet = this._toolPromptSnippets.get(name);
|
|
546
|
-
if (snippet) {
|
|
547
|
-
toolSnippets[name] = snippet;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const toolGuidelines = this._toolPromptGuidelines.get(name);
|
|
551
|
-
if (toolGuidelines) {
|
|
552
|
-
promptGuidelines.push(...toolGuidelines);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
this._baseSystemPromptOptions = {
|
|
557
|
-
cwd: this._cwd,
|
|
558
|
-
skills: this._resourceLoader.getSkills(),
|
|
559
|
-
customPrompt: this._resourceLoader.getSystemPrompt(),
|
|
560
|
-
selectedTools: validToolNames,
|
|
561
|
-
toolSnippets,
|
|
562
|
-
promptGuidelines,
|
|
563
|
-
};
|
|
564
|
-
return buildSystemPrompt(this._baseSystemPromptOptions);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {
|
|
568
|
-
this._isAgentRunActive = true;
|
|
569
|
-
try {
|
|
570
|
-
await this.agent.prompt(messages);
|
|
571
|
-
while (await this._handlePostAgentRun()) {
|
|
572
|
-
await this.agent.continue();
|
|
573
|
-
}
|
|
574
|
-
} finally {
|
|
575
|
-
this._systemPromptOverride = undefined;
|
|
576
|
-
this._emitAgentSettled();
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
private async _handlePostAgentRun(): Promise<boolean> {
|
|
581
|
-
const msg = this._lastAssistantMessage;
|
|
582
|
-
this._lastAssistantMessage = undefined;
|
|
583
|
-
if (!msg) {
|
|
584
|
-
return false;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
|
588
|
-
return true;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (msg.stopReason === "error" && this._retryAttempt > 0) {
|
|
592
|
-
this._emit({
|
|
593
|
-
type: "auto_retry_end",
|
|
594
|
-
success: false,
|
|
595
|
-
attempt: this._retryAttempt,
|
|
596
|
-
finalError: msg.errorMessage,
|
|
597
|
-
});
|
|
598
|
-
this._retryAttempt = 0;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return await this._checkCompaction(msg);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/** Run a prompt; while the agent runs, or without model or key, it throws. */
|
|
605
|
-
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
606
|
-
const preflightResult = options?.preflightResult;
|
|
607
|
-
const attachments = options?.attachments;
|
|
608
|
-
let messages: AgentMessage[] | undefined;
|
|
609
|
-
|
|
610
|
-
try {
|
|
611
|
-
this._assertNotDisposed();
|
|
612
|
-
|
|
613
|
-
if (this.isStreaming) {
|
|
614
|
-
throw new Error("Agent is already processing.");
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Validate model
|
|
618
|
-
if (!this.model) {
|
|
619
|
-
throw new Error(formatNoModelSelectedMessage());
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const auth = await this._modelRuntime.checkAuth(this.model.provider);
|
|
623
|
-
this._assertNotDisposed();
|
|
624
|
-
if (!auth) {
|
|
625
|
-
throw new Error(formatNoApiKeyFoundMessage(this.model.provider));
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Check if we need to compact before sending (catches aborted responses).
|
|
629
|
-
// The user's new prompt is sent below, so do not call agent.continue() here.
|
|
630
|
-
const lastAssistant = this._findLastAssistantMessage();
|
|
631
|
-
if (lastAssistant) {
|
|
632
|
-
await this._checkCompaction(lastAssistant, false);
|
|
633
|
-
this._assertNotDisposed();
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const userContent: UserContent[] = [{ type: "text", text }];
|
|
637
|
-
if (attachments) userContent.push(...attachments);
|
|
638
|
-
messages = [{ role: "user", content: userContent, timestamp: Date.now() }];
|
|
639
|
-
|
|
640
|
-
const systemPrompt = await this._extensionRunner.emitBeforeAgentStart(
|
|
641
|
-
text,
|
|
642
|
-
this._baseSystemPrompt,
|
|
643
|
-
this._baseSystemPromptOptions,
|
|
644
|
-
attachments,
|
|
645
|
-
);
|
|
646
|
-
this._assertNotDisposed();
|
|
647
|
-
// Apply extension-modified system prompt, or reset to base
|
|
648
|
-
this._systemPromptOverride = systemPrompt;
|
|
649
|
-
this.agent.state.systemPrompt = systemPrompt ?? this._baseSystemPrompt;
|
|
650
|
-
} catch (error) {
|
|
651
|
-
preflightResult?.(false);
|
|
652
|
-
throw error;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
preflightResult?.(true);
|
|
656
|
-
await this._runAgentPrompt(messages);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
private _assertNotDisposed(): void {
|
|
660
|
-
if (this._disposed) {
|
|
661
|
-
throw new Error("Agent session is disposed.");
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Abort current operation and wait for agent to become idle.
|
|
667
|
-
*/
|
|
668
|
-
async abort(): Promise<void> {
|
|
669
|
-
this.abortRetry();
|
|
670
|
-
this.agent.abort();
|
|
671
|
-
await this.waitForIdle();
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
async waitForIdle(): Promise<void> {
|
|
675
|
-
if (this.isIdle) {
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
await this._getIdleWaitPromise();
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Set model directly.
|
|
683
|
-
* Validates that auth is configured, saves to session and settings.
|
|
684
|
-
* @throws Error if no auth is configured for the model
|
|
685
|
-
*/
|
|
686
|
-
async setModel(model: Model<any>): Promise<void> {
|
|
687
|
-
if (!(await this._modelRuntime.checkAuth(model.provider))) {
|
|
688
|
-
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const thinkingLevel = this._getThinkingLevelForModelSwitch();
|
|
692
|
-
this.agent.state.model = model;
|
|
693
|
-
this.sessionManager.appendModelChange(model.provider, model.id);
|
|
694
|
-
|
|
695
|
-
// Re-clamp thinking level for new model's capabilities
|
|
696
|
-
this.setThinkingLevel(thinkingLevel);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Set thinking level.
|
|
701
|
-
* Clamps to model capabilities based on available thinking levels.
|
|
702
|
-
* Saves to session and settings only if the level actually changes.
|
|
703
|
-
*/
|
|
704
|
-
setThinkingLevel(level: ThinkingLevel): void {
|
|
705
|
-
const availableLevels = this.getAvailableThinkingLevels();
|
|
706
|
-
const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
|
|
707
|
-
|
|
708
|
-
// Only persist if actually changing
|
|
709
|
-
const previousLevel = this.agent.state.thinkingLevel;
|
|
710
|
-
const isChanging = effectiveLevel !== previousLevel;
|
|
711
|
-
|
|
712
|
-
this.agent.state.thinkingLevel = effectiveLevel;
|
|
713
|
-
|
|
714
|
-
if (isChanging) {
|
|
715
|
-
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
716
|
-
this._emit({ type: "thinking_level_changed", level: effectiveLevel });
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Get available thinking levels for current model.
|
|
722
|
-
* The provider will clamp to what the specific model supports internally.
|
|
723
|
-
*/
|
|
724
|
-
getAvailableThinkingLevels(): ThinkingLevel[] {
|
|
725
|
-
if (!this.model) return THINKING_LEVELS;
|
|
726
|
-
return getSupportedThinkingLevels(this.model) as ThinkingLevel[];
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Check if current model supports thinking/reasoning.
|
|
731
|
-
*/
|
|
732
|
-
private supportsThinking(): boolean {
|
|
733
|
-
return !!this.model?.reasoning;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
private _getThinkingLevelForModelSwitch(explicitLevel?: ThinkingLevel): ThinkingLevel {
|
|
737
|
-
if (explicitLevel !== undefined) {
|
|
738
|
-
return explicitLevel;
|
|
739
|
-
}
|
|
740
|
-
if (!this.supportsThinking()) {
|
|
741
|
-
return DEFAULT_THINKING_LEVEL;
|
|
742
|
-
}
|
|
743
|
-
return this.thinkingLevel;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
private _clampThinkingLevel(level: ThinkingLevel, _availableLevels: ThinkingLevel[]): ThinkingLevel {
|
|
747
|
-
return this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : "off";
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* Check if compaction is needed and run it.
|
|
752
|
-
* Called after agent_end and before prompt submission.
|
|
753
|
-
*
|
|
754
|
-
* Two cases:
|
|
755
|
-
* 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry
|
|
756
|
-
* 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
757
|
-
*
|
|
758
|
-
* @param assistantMessage The assistant message to check
|
|
759
|
-
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
760
|
-
*/
|
|
761
|
-
private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
|
|
762
|
-
const settings = this.settings.compaction;
|
|
763
|
-
if (!settings.enabled) return false;
|
|
764
|
-
|
|
765
|
-
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
766
|
-
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;
|
|
767
|
-
|
|
768
|
-
const contextWindow = this.model?.contextWindow ?? 0;
|
|
769
|
-
|
|
770
|
-
// Skip overflow check if the message came from a different model.
|
|
771
|
-
// This handles the case where user switched from a smaller-context model (e.g. opus)
|
|
772
|
-
// to a larger-context model (e.g. codex) - the overflow error from the old model
|
|
773
|
-
// shouldn't trigger compaction for the new model.
|
|
774
|
-
const sameModel =
|
|
775
|
-
this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
|
|
776
|
-
|
|
777
|
-
// Skip compaction checks if this assistant message is older than the latest
|
|
778
|
-
// compaction boundary. This prevents a stale pre-compaction usage/error
|
|
779
|
-
// from retriggering compaction on the first prompt after compaction.
|
|
780
|
-
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
781
|
-
const assistantIsFromBeforeCompaction =
|
|
782
|
-
compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
|
|
783
|
-
if (assistantIsFromBeforeCompaction) {
|
|
784
|
-
return false;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Case 1: Overflow - LLM returned context overflow error, or reported usage exceeded
|
|
788
|
-
// the configured window. A successful response over the configured window should compact
|
|
789
|
-
// but must not retry: the assistant answer already completed and agent.continue() cannot
|
|
790
|
-
// continue from an assistant message.
|
|
791
|
-
if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
|
|
792
|
-
const willRetry = assistantMessage.stopReason !== "stop";
|
|
793
|
-
|
|
794
|
-
if (!willRetry) {
|
|
795
|
-
return await this._runAutoCompaction("overflow", false);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (this._overflowRecoveryAttempted) {
|
|
799
|
-
this._emit({
|
|
800
|
-
type: "compaction_end",
|
|
801
|
-
reason: "overflow",
|
|
802
|
-
result: undefined,
|
|
803
|
-
aborted: false,
|
|
804
|
-
willRetry: false,
|
|
805
|
-
errorMessage:
|
|
806
|
-
"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
|
|
807
|
-
});
|
|
808
|
-
return false;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
this._overflowRecoveryAttempted = true;
|
|
812
|
-
// Remove the error message from agent state (it IS saved to session for history,
|
|
813
|
-
// but we don't want it in context for the retry)
|
|
814
|
-
const messages = this.agent.state.messages;
|
|
815
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
816
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
817
|
-
}
|
|
818
|
-
return await this._runAutoCompaction("overflow", willRetry);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Case 2: Threshold - context is getting large
|
|
822
|
-
// For error messages or all-zero usage messages, estimate from the last valid response.
|
|
823
|
-
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
|
824
|
-
// responses can still compact and do not reset context accounting.
|
|
825
|
-
let contextTokens: number;
|
|
826
|
-
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
|
827
|
-
if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
|
828
|
-
const messages = this.agent.state.messages;
|
|
829
|
-
const estimate = estimateContextTokens(messages);
|
|
830
|
-
if (estimate.lastUsageIndex === null) return false; // No usage data at all
|
|
831
|
-
// Verify the usage source is post-compaction. Kept pre-compaction messages
|
|
832
|
-
// have stale usage reflecting the old (larger) context and would falsely
|
|
833
|
-
// trigger compaction right after one just finished.
|
|
834
|
-
const usageMsg = messages[estimate.lastUsageIndex];
|
|
835
|
-
if (
|
|
836
|
-
compactionEntry &&
|
|
837
|
-
usageMsg.role === "assistant" &&
|
|
838
|
-
(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()
|
|
839
|
-
) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
contextTokens = estimate.tokens;
|
|
843
|
-
} else {
|
|
844
|
-
contextTokens = directContextTokens;
|
|
845
|
-
}
|
|
846
|
-
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
|
847
|
-
return await this._runAutoCompaction("threshold", false);
|
|
848
|
-
}
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* Internal: Run auto-compaction with events.
|
|
854
|
-
*/
|
|
855
|
-
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<boolean> {
|
|
856
|
-
const settings = this.settings.compaction;
|
|
857
|
-
let started = false;
|
|
858
|
-
|
|
859
|
-
try {
|
|
860
|
-
if (!this.model) {
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
let apiKey: string | undefined;
|
|
865
|
-
let headers: Record<string, string> | undefined;
|
|
866
|
-
let env: Record<string, string> | undefined;
|
|
867
|
-
if (this.agent.streamFn === streamSimple) {
|
|
868
|
-
const authResult = await this._modelRuntime.getAuth(this.model);
|
|
869
|
-
if (!authResult?.auth.apiKey) return false;
|
|
870
|
-
apiKey = authResult.auth.apiKey;
|
|
871
|
-
headers = withoutDeletedHeaders(authResult.auth.headers);
|
|
872
|
-
env = authResult.env;
|
|
873
|
-
} else {
|
|
874
|
-
({ apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model));
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const pathEntries = this.sessionManager.getBranch();
|
|
878
|
-
|
|
879
|
-
const preparation = prepareCompaction(pathEntries, settings);
|
|
880
|
-
if (!preparation) {
|
|
881
|
-
return false;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
this._emit({ type: "compaction_start", reason });
|
|
885
|
-
this._autoCompactionAbortController = new AbortController();
|
|
886
|
-
started = true;
|
|
887
|
-
|
|
888
|
-
const { summary, firstKeptEntryId, tokensBefore, details } = await compact(
|
|
889
|
-
preparation,
|
|
890
|
-
this.model,
|
|
891
|
-
apiKey,
|
|
892
|
-
headers,
|
|
893
|
-
undefined,
|
|
894
|
-
this._autoCompactionAbortController.signal,
|
|
895
|
-
this.thinkingLevel,
|
|
896
|
-
this.agent.streamFn,
|
|
897
|
-
env,
|
|
898
|
-
);
|
|
899
|
-
|
|
900
|
-
if (this._autoCompactionAbortController.signal.aborted) {
|
|
901
|
-
this._emit({
|
|
902
|
-
type: "compaction_end",
|
|
903
|
-
reason,
|
|
904
|
-
result: undefined,
|
|
905
|
-
aborted: true,
|
|
906
|
-
willRetry: false,
|
|
907
|
-
});
|
|
908
|
-
return false;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, false);
|
|
912
|
-
const sessionContext = this.sessionManager.buildSessionContext();
|
|
913
|
-
this.agent.state.messages = sessionContext.messages;
|
|
914
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
915
|
-
|
|
916
|
-
const result: CompactionResult = {
|
|
917
|
-
summary,
|
|
918
|
-
firstKeptEntryId,
|
|
919
|
-
tokensBefore,
|
|
920
|
-
estimatedTokensAfter,
|
|
921
|
-
details,
|
|
922
|
-
};
|
|
923
|
-
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
|
|
924
|
-
|
|
925
|
-
if (willRetry) {
|
|
926
|
-
const messages = this.agent.state.messages;
|
|
927
|
-
const lastMsg = messages[messages.length - 1];
|
|
928
|
-
if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") {
|
|
929
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
930
|
-
}
|
|
931
|
-
return true;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
return false;
|
|
935
|
-
} catch (error) {
|
|
936
|
-
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
937
|
-
if (started) {
|
|
938
|
-
this._emit({
|
|
939
|
-
type: "compaction_end",
|
|
940
|
-
reason,
|
|
941
|
-
result: undefined,
|
|
942
|
-
aborted: false,
|
|
943
|
-
willRetry: false,
|
|
944
|
-
errorMessage:
|
|
945
|
-
reason === "overflow"
|
|
946
|
-
? `Context overflow recovery failed: ${errorMessage}`
|
|
947
|
-
: `Auto-compaction failed: ${errorMessage}`,
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
return false;
|
|
951
|
-
} finally {
|
|
952
|
-
this._autoCompactionAbortController = undefined;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
async bindExtensions(bindings: ExtensionBindings): Promise<void> {
|
|
957
|
-
this._extensionErrorUnsubscriber?.();
|
|
958
|
-
this._extensionErrorUnsubscriber = bindings.onError ? this._extensionRunner.onError(bindings.onError) : undefined;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
private _bindExtensionCore(runner: ExtensionRunner): void {
|
|
962
|
-
runner.bindCore(
|
|
963
|
-
{
|
|
964
|
-
appendEntry: (customType, data) => {
|
|
965
|
-
this.sessionManager.appendCustomEntry(customType, data);
|
|
966
|
-
},
|
|
967
|
-
getAllTools: () => this.getAllTools(),
|
|
968
|
-
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
|
|
969
|
-
refreshTools: () => this._refreshToolRegistry(),
|
|
970
|
-
},
|
|
971
|
-
{
|
|
972
|
-
getModel: () => this.model,
|
|
973
|
-
getSignal: () => this.agent.signal,
|
|
974
|
-
},
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
private _refreshToolRegistry(options?: { activateAll?: boolean }): void {
|
|
979
|
-
const previousRegistryNames = new Set(this._toolRegistry.keys());
|
|
980
|
-
const previousActiveToolNames = this.getActiveToolNames();
|
|
981
|
-
const allowedToolNames = this._allowedToolNames;
|
|
982
|
-
const isAllowedTool = (name: string): boolean => !allowedToolNames || allowedToolNames.has(name);
|
|
983
|
-
|
|
984
|
-
const tools = [
|
|
985
|
-
...this._extensionRunner.getAllRegisteredTools(),
|
|
986
|
-
...this._customTools.map((definition) => ({
|
|
987
|
-
definition,
|
|
988
|
-
sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
|
|
989
|
-
})),
|
|
990
|
-
].filter((tool) => isAllowedTool(tool.definition.name));
|
|
991
|
-
const definitionRegistry = new Map<string, ToolDefinitionEntry>(
|
|
992
|
-
tools.map((tool) => [tool.definition.name, { definition: tool.definition, sourceInfo: tool.sourceInfo }]),
|
|
993
|
-
);
|
|
994
|
-
this._toolDefinitions = definitionRegistry;
|
|
995
|
-
this._toolPromptSnippets = new Map(
|
|
996
|
-
Array.from(definitionRegistry.values())
|
|
997
|
-
.map(({ definition }) => {
|
|
998
|
-
const snippet = this._normalizePromptSnippet(definition.promptSnippet);
|
|
999
|
-
return snippet ? ([definition.name, snippet] as const) : undefined;
|
|
1000
|
-
})
|
|
1001
|
-
.filter((entry): entry is readonly [string, string] => entry !== undefined),
|
|
1002
|
-
);
|
|
1003
|
-
this._toolPromptGuidelines = new Map(
|
|
1004
|
-
Array.from(definitionRegistry.values())
|
|
1005
|
-
.map(({ definition }) => {
|
|
1006
|
-
const guidelines = this._normalizePromptGuidelines(definition.promptGuidelines);
|
|
1007
|
-
return guidelines.length > 0 ? ([definition.name, guidelines] as const) : undefined;
|
|
1008
|
-
})
|
|
1009
|
-
.filter((entry): entry is readonly [string, string[]] => entry !== undefined),
|
|
1010
|
-
);
|
|
1011
|
-
this._toolRegistry = new Map(
|
|
1012
|
-
wrapRegisteredTools(tools, this._extensionRunner).map((tool) => [tool.name, tool] as const),
|
|
1013
|
-
);
|
|
1014
|
-
|
|
1015
|
-
const registryNames = [...this._toolRegistry.keys()];
|
|
1016
|
-
const nextActiveToolNames = options?.activateAll || allowedToolNames
|
|
1017
|
-
? registryNames
|
|
1018
|
-
: [
|
|
1019
|
-
...previousActiveToolNames.filter((name) => this._toolRegistry.has(name)),
|
|
1020
|
-
...registryNames.filter((name) => !previousRegistryNames.has(name)),
|
|
1021
|
-
];
|
|
1022
|
-
|
|
1023
|
-
this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
private _buildRuntime(): void {
|
|
1027
|
-
const extensionsResult = this._resourceLoader.getExtensions();
|
|
1028
|
-
this._extensionRunner = new ExtensionRunner(extensionsResult.extensions, extensionsResult.runtime, this.sessionManager);
|
|
1029
|
-
if (this._extensionRunnerRef) {
|
|
1030
|
-
this._extensionRunnerRef.current = this._extensionRunner;
|
|
1031
|
-
}
|
|
1032
|
-
this._bindExtensionCore(this._extensionRunner);
|
|
1033
|
-
this._refreshToolRegistry({ activateAll: true });
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Check if an error is retryable (overloaded, rate limit, server errors).
|
|
1038
|
-
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
1039
|
-
*/
|
|
1040
|
-
private _isRetryableError(message: AssistantMessage): boolean {
|
|
1041
|
-
// Context overflow is handled by compaction, not retry.
|
|
1042
|
-
if (isContextOverflow(message, this.model?.contextWindow ?? 0)) return false;
|
|
1043
|
-
return isRetryableAssistantError(message);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* Prepare a retryable error for continuation with exponential backoff.
|
|
1048
|
-
* @returns true if the caller should continue the agent, false otherwise
|
|
1049
|
-
*/
|
|
1050
|
-
private async _prepareRetry(message: AssistantMessage): Promise<boolean> {
|
|
1051
|
-
const settings = this.settings.retry;
|
|
1052
|
-
if (!settings.enabled) {
|
|
1053
|
-
return false;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
this._retryAttempt++;
|
|
1057
|
-
|
|
1058
|
-
if (this._retryAttempt > settings.maxRetries) {
|
|
1059
|
-
// Preserve the completed attempt count so post-run handling can emit the final failure.
|
|
1060
|
-
this._retryAttempt--;
|
|
1061
|
-
return false;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
|
|
1065
|
-
|
|
1066
|
-
this._emit({
|
|
1067
|
-
type: "auto_retry_start",
|
|
1068
|
-
attempt: this._retryAttempt,
|
|
1069
|
-
maxAttempts: settings.maxRetries,
|
|
1070
|
-
delayMs,
|
|
1071
|
-
errorMessage: message.errorMessage || "Unknown error",
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
// Remove error message from agent state (keep in session for history)
|
|
1075
|
-
const messages = this.agent.state.messages;
|
|
1076
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
1077
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Wait with exponential backoff (abortable)
|
|
1081
|
-
this._retryAbortController = new AbortController();
|
|
1082
|
-
try {
|
|
1083
|
-
await sleep(delayMs, this._retryAbortController.signal);
|
|
1084
|
-
} catch {
|
|
1085
|
-
// Aborted during sleep - emit end event so UI can clean up
|
|
1086
|
-
const attempt = this._retryAttempt;
|
|
1087
|
-
this._retryAttempt = 0;
|
|
1088
|
-
this._emit({
|
|
1089
|
-
type: "auto_retry_end",
|
|
1090
|
-
success: false,
|
|
1091
|
-
attempt,
|
|
1092
|
-
finalError: "Retry cancelled",
|
|
1093
|
-
});
|
|
1094
|
-
return false;
|
|
1095
|
-
} finally {
|
|
1096
|
-
this._retryAbortController = undefined;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
return true;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
/**
|
|
1103
|
-
* Cancel in-progress retry.
|
|
1104
|
-
*/
|
|
1105
|
-
abortRetry(): void {
|
|
1106
|
-
this._retryAbortController?.abort();
|
|
1107
|
-
}
|
|
1108
|
-
}
|