@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line/separators.ts +4 -4
- package/src/modes/interactive/components/status-line.ts +45 -35
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +644 -113
- package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
- package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
- package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
- package/src/modes/interactive/theme/defaults/basalt.json +90 -0
- package/src/modes/interactive/theme/defaults/birch.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
- package/src/modes/interactive/theme/defaults/graphite.json +99 -0
- package/src/modes/interactive/theme/defaults/index.ts +128 -0
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
- package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
- package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
- package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
- package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
- package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
- package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
- package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
- package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
- package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
- package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
- package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
- package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
- package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
- package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
- package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
- package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
- package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
- package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
- package/src/modes/interactive/theme/defaults/limestone.json +100 -0
- package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
- package/src/modes/interactive/theme/defaults/marble.json +99 -0
- package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
- package/src/modes/interactive/theme/defaults/onyx.json +90 -0
- package/src/modes/interactive/theme/defaults/pearl.json +99 -0
- package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
- package/src/modes/interactive/theme/defaults/quartz.json +102 -0
- package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
- package/src/modes/interactive/theme/defaults/titanium.json +89 -0
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
16
|
+
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
17
|
import type { AssistantMessage, ImageContent, Message, Model, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
18
18
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
19
19
|
import type { Rule } from "../capability/rule";
|
|
20
20
|
import { getAuthPath } from "../config";
|
|
21
|
+
import { theme } from "../modes/interactive/theme/theme";
|
|
21
22
|
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor";
|
|
22
23
|
import {
|
|
23
24
|
type CompactionResult,
|
|
@@ -29,11 +30,11 @@ import {
|
|
|
29
30
|
shouldCompact,
|
|
30
31
|
} from "./compaction/index";
|
|
31
32
|
import type { LoadedCustomCommand } from "./custom-commands/index";
|
|
32
|
-
import type { CustomToolContext, CustomToolSessionEvent, LoadedCustomTool } from "./custom-tools/index";
|
|
33
33
|
import { exportSessionToHtml } from "./export-html/index";
|
|
34
|
-
import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
|
|
35
34
|
import type {
|
|
36
|
-
|
|
35
|
+
ExtensionCommandContext,
|
|
36
|
+
ExtensionRunner,
|
|
37
|
+
ExtensionUIContext,
|
|
37
38
|
SessionBeforeBranchResult,
|
|
38
39
|
SessionBeforeCompactResult,
|
|
39
40
|
SessionBeforeSwitchResult,
|
|
@@ -41,13 +42,15 @@ import type {
|
|
|
41
42
|
TreePreparation,
|
|
42
43
|
TurnEndEvent,
|
|
43
44
|
TurnStartEvent,
|
|
44
|
-
} from "./
|
|
45
|
-
import {
|
|
46
|
-
import type {
|
|
45
|
+
} from "./extensions";
|
|
46
|
+
import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
|
|
47
|
+
import type { HookCommandContext } from "./hooks/types";
|
|
48
|
+
import type { BashExecutionMessage, CustomMessage } from "./messages";
|
|
47
49
|
import type { ModelRegistry } from "./model-registry";
|
|
50
|
+
import { parseModelString } from "./model-resolver";
|
|
51
|
+
import { expandPromptTemplate, type PromptTemplate, parseCommandArgs } from "./prompt-templates";
|
|
48
52
|
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
|
|
49
53
|
import type { SettingsManager, SkillsSettings } from "./settings-manager";
|
|
50
|
-
import { expandSlashCommand, type FileSlashCommand, parseCommandArgs } from "./slash-commands";
|
|
51
54
|
import type { TtsrManager } from "./ttsr";
|
|
52
55
|
|
|
53
56
|
/** Session-specific events that extend the core AgentEvent */
|
|
@@ -72,27 +75,31 @@ export interface AgentSessionConfig {
|
|
|
72
75
|
settingsManager: SettingsManager;
|
|
73
76
|
/** Models to cycle through with Ctrl+P (from --models flag) */
|
|
74
77
|
scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
/** Custom tools for session lifecycle events */
|
|
80
|
-
customTools?: LoadedCustomTool[];
|
|
78
|
+
/** Prompt templates for expansion */
|
|
79
|
+
promptTemplates?: PromptTemplate[];
|
|
80
|
+
/** Extension runner (created in main.ts with wrapped tools) */
|
|
81
|
+
extensionRunner?: ExtensionRunner;
|
|
81
82
|
/** Custom commands (TypeScript slash commands) */
|
|
82
83
|
customCommands?: LoadedCustomCommand[];
|
|
83
84
|
skillsSettings?: Required<SkillsSettings>;
|
|
84
85
|
/** Model registry for API key resolution and model discovery */
|
|
85
86
|
modelRegistry: ModelRegistry;
|
|
87
|
+
/** Tool registry for LSP and settings */
|
|
88
|
+
toolRegistry?: Map<string, AgentTool>;
|
|
89
|
+
/** System prompt builder that can consider tool availability */
|
|
90
|
+
rebuildSystemPrompt?: (toolNames: string[]) => string;
|
|
86
91
|
/** TTSR manager for time-traveling stream rules */
|
|
87
92
|
ttsrManager?: TtsrManager;
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
/** Options for AgentSession.prompt() */
|
|
91
96
|
export interface PromptOptions {
|
|
92
|
-
/** Whether to expand file-based
|
|
93
|
-
|
|
97
|
+
/** Whether to expand file-based prompt templates (default: true) */
|
|
98
|
+
expandPromptTemplates?: boolean;
|
|
94
99
|
/** Image attachments */
|
|
95
100
|
images?: ImageContent[];
|
|
101
|
+
/** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). */
|
|
102
|
+
streamingBehavior?: "steer" | "followUp";
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
/** Result from cycleModel() */
|
|
@@ -103,6 +110,13 @@ export interface ModelCycleResult {
|
|
|
103
110
|
isScoped: boolean;
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
/** Result from cycleRoleModels() */
|
|
114
|
+
export interface RoleModelCycleResult {
|
|
115
|
+
model: Model<any>;
|
|
116
|
+
thinkingLevel: ThinkingLevel;
|
|
117
|
+
role: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
/** Session statistics for /session command */
|
|
107
121
|
export interface SessionStats {
|
|
108
122
|
sessionFile: string | undefined;
|
|
@@ -133,6 +147,23 @@ const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "hi
|
|
|
133
147
|
/** Thinking levels including xhigh (for supported models) */
|
|
134
148
|
const THINKING_LEVELS_WITH_XHIGH: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
135
149
|
|
|
150
|
+
const noOpUIContext: ExtensionUIContext = {
|
|
151
|
+
select: async () => undefined,
|
|
152
|
+
confirm: async () => false,
|
|
153
|
+
input: async () => undefined,
|
|
154
|
+
notify: () => {},
|
|
155
|
+
setStatus: () => {},
|
|
156
|
+
setWidget: () => {},
|
|
157
|
+
setTitle: () => {},
|
|
158
|
+
custom: async () => undefined as never,
|
|
159
|
+
setEditorText: () => {},
|
|
160
|
+
getEditorText: () => "",
|
|
161
|
+
editor: async () => undefined,
|
|
162
|
+
get theme() {
|
|
163
|
+
return theme;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
136
167
|
// ============================================================================
|
|
137
168
|
// AgentSession Class
|
|
138
169
|
// ============================================================================
|
|
@@ -143,14 +174,18 @@ export class AgentSession {
|
|
|
143
174
|
readonly settingsManager: SettingsManager;
|
|
144
175
|
|
|
145
176
|
private _scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
|
|
146
|
-
private
|
|
177
|
+
private _promptTemplates: PromptTemplate[];
|
|
147
178
|
|
|
148
179
|
// Event subscription state
|
|
149
180
|
private _unsubscribeAgent?: () => void;
|
|
150
181
|
private _eventListeners: AgentSessionEventListener[] = [];
|
|
151
182
|
|
|
152
|
-
|
|
153
|
-
private
|
|
183
|
+
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
184
|
+
private _steeringMessages: string[] = [];
|
|
185
|
+
/** Tracks pending follow-up messages for UI display. Removed when delivered. */
|
|
186
|
+
private _followUpMessages: string[] = [];
|
|
187
|
+
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
188
|
+
private _pendingNextTurnMessages: CustomMessage[] = [];
|
|
154
189
|
|
|
155
190
|
// Compaction state
|
|
156
191
|
private _compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -169,13 +204,10 @@ export class AgentSession {
|
|
|
169
204
|
private _bashAbortController: AbortController | undefined = undefined;
|
|
170
205
|
private _pendingBashMessages: BashExecutionMessage[] = [];
|
|
171
206
|
|
|
172
|
-
//
|
|
173
|
-
private
|
|
207
|
+
// Extension system
|
|
208
|
+
private _extensionRunner: ExtensionRunner | undefined = undefined;
|
|
174
209
|
private _turnIndex = 0;
|
|
175
210
|
|
|
176
|
-
// Custom tools for session lifecycle
|
|
177
|
-
private _customTools: LoadedCustomTool[] = [];
|
|
178
|
-
|
|
179
211
|
// Custom commands (TypeScript slash commands)
|
|
180
212
|
private _customCommands: LoadedCustomCommand[] = [];
|
|
181
213
|
|
|
@@ -184,6 +216,11 @@ export class AgentSession {
|
|
|
184
216
|
// Model registry for API key resolution
|
|
185
217
|
private _modelRegistry: ModelRegistry;
|
|
186
218
|
|
|
219
|
+
// Tool registry and prompt builder for extensions
|
|
220
|
+
private _toolRegistry: Map<string, AgentTool>;
|
|
221
|
+
private _rebuildSystemPrompt: ((toolNames: string[]) => string) | undefined;
|
|
222
|
+
private _baseSystemPrompt: string;
|
|
223
|
+
|
|
187
224
|
// TTSR manager for time-traveling stream rules
|
|
188
225
|
private _ttsrManager: TtsrManager | undefined = undefined;
|
|
189
226
|
private _pendingTtsrInjections: Rule[] = [];
|
|
@@ -194,12 +231,14 @@ export class AgentSession {
|
|
|
194
231
|
this.sessionManager = config.sessionManager;
|
|
195
232
|
this.settingsManager = config.settingsManager;
|
|
196
233
|
this._scopedModels = config.scopedModels ?? [];
|
|
197
|
-
this.
|
|
198
|
-
this.
|
|
199
|
-
this._customTools = config.customTools ?? [];
|
|
234
|
+
this._promptTemplates = config.promptTemplates ?? [];
|
|
235
|
+
this._extensionRunner = config.extensionRunner;
|
|
200
236
|
this._customCommands = config.customCommands ?? [];
|
|
201
237
|
this._skillsSettings = config.skillsSettings;
|
|
202
238
|
this._modelRegistry = config.modelRegistry;
|
|
239
|
+
this._toolRegistry = config.toolRegistry ?? new Map();
|
|
240
|
+
this._rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
241
|
+
this._baseSystemPrompt = this.agent.state.systemPrompt;
|
|
203
242
|
this._ttsrManager = config.ttsrManager;
|
|
204
243
|
|
|
205
244
|
// Always subscribe to agent events for internal handling
|
|
@@ -240,22 +279,27 @@ export class AgentSession {
|
|
|
240
279
|
|
|
241
280
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
242
281
|
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
243
|
-
// When a user message starts, check if it's from
|
|
282
|
+
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
244
283
|
// This ensures the UI sees the updated queue state
|
|
245
|
-
if (event.type === "message_start" && event.message.role === "user"
|
|
246
|
-
// Extract text content from the message
|
|
284
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
247
285
|
const messageText = this._getUserMessageText(event.message);
|
|
248
|
-
if (messageText
|
|
249
|
-
//
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
this.
|
|
286
|
+
if (messageText) {
|
|
287
|
+
// Check steering queue first
|
|
288
|
+
const steeringIndex = this._steeringMessages.indexOf(messageText);
|
|
289
|
+
if (steeringIndex !== -1) {
|
|
290
|
+
this._steeringMessages.splice(steeringIndex, 1);
|
|
291
|
+
} else {
|
|
292
|
+
// Check follow-up queue
|
|
293
|
+
const followUpIndex = this._followUpMessages.indexOf(messageText);
|
|
294
|
+
if (followUpIndex !== -1) {
|
|
295
|
+
this._followUpMessages.splice(followUpIndex, 1);
|
|
296
|
+
}
|
|
253
297
|
}
|
|
254
298
|
}
|
|
255
299
|
}
|
|
256
300
|
|
|
257
|
-
// Emit to
|
|
258
|
-
await this.
|
|
301
|
+
// Emit to extensions first
|
|
302
|
+
await this._emitExtensionEvent(event);
|
|
259
303
|
|
|
260
304
|
// Notify all listeners
|
|
261
305
|
this._emit(event);
|
|
@@ -316,8 +360,8 @@ export class AgentSession {
|
|
|
316
360
|
|
|
317
361
|
// Handle session persistence
|
|
318
362
|
if (event.type === "message_end") {
|
|
319
|
-
// Check if this is a hook message
|
|
320
|
-
if (event.message.role === "hookMessage") {
|
|
363
|
+
// Check if this is a hook/custom message
|
|
364
|
+
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
321
365
|
// Persist as CustomMessageEntry
|
|
322
366
|
this.sessionManager.appendCustomMessageEntry(
|
|
323
367
|
event.message.customType,
|
|
@@ -412,22 +456,22 @@ export class AgentSession {
|
|
|
412
456
|
return undefined;
|
|
413
457
|
}
|
|
414
458
|
|
|
415
|
-
/** Emit
|
|
416
|
-
private async
|
|
417
|
-
if (!this.
|
|
459
|
+
/** Emit extension events based on agent events */
|
|
460
|
+
private async _emitExtensionEvent(event: AgentEvent): Promise<void> {
|
|
461
|
+
if (!this._extensionRunner) return;
|
|
418
462
|
|
|
419
463
|
if (event.type === "agent_start") {
|
|
420
464
|
this._turnIndex = 0;
|
|
421
|
-
await this.
|
|
465
|
+
await this._extensionRunner.emit({ type: "agent_start" });
|
|
422
466
|
} else if (event.type === "agent_end") {
|
|
423
|
-
await this.
|
|
467
|
+
await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
|
|
424
468
|
} else if (event.type === "turn_start") {
|
|
425
469
|
const hookEvent: TurnStartEvent = {
|
|
426
470
|
type: "turn_start",
|
|
427
471
|
turnIndex: this._turnIndex,
|
|
428
472
|
timestamp: Date.now(),
|
|
429
473
|
};
|
|
430
|
-
await this.
|
|
474
|
+
await this._extensionRunner.emit(hookEvent);
|
|
431
475
|
} else if (event.type === "turn_end") {
|
|
432
476
|
const hookEvent: TurnEndEvent = {
|
|
433
477
|
type: "turn_end",
|
|
@@ -435,7 +479,7 @@ export class AgentSession {
|
|
|
435
479
|
message: event.message,
|
|
436
480
|
toolResults: event.toolResults,
|
|
437
481
|
};
|
|
438
|
-
await this.
|
|
482
|
+
await this._extensionRunner.emit(hookEvent);
|
|
439
483
|
this._turnIndex++;
|
|
440
484
|
}
|
|
441
485
|
}
|
|
@@ -512,6 +556,53 @@ export class AgentSession {
|
|
|
512
556
|
return this.agent.state.isStreaming;
|
|
513
557
|
}
|
|
514
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Get the names of currently active tools.
|
|
561
|
+
* Returns the names of tools currently set on the agent.
|
|
562
|
+
*/
|
|
563
|
+
getActiveToolNames(): string[] {
|
|
564
|
+
return this.agent.state.tools.map((t) => t.name);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get a tool by name from the registry.
|
|
569
|
+
*/
|
|
570
|
+
getToolByName(name: string): AgentTool | undefined {
|
|
571
|
+
return this._toolRegistry.get(name);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Get all configured tool names (built-in via --tools or default, plus custom tools).
|
|
576
|
+
*/
|
|
577
|
+
getAllToolNames(): string[] {
|
|
578
|
+
return Array.from(this._toolRegistry.keys());
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Set active tools by name.
|
|
583
|
+
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
584
|
+
* Also rebuilds the system prompt to reflect the new tool set.
|
|
585
|
+
* Changes take effect on the next agent turn.
|
|
586
|
+
*/
|
|
587
|
+
setActiveToolsByName(toolNames: string[]): void {
|
|
588
|
+
const tools: AgentTool[] = [];
|
|
589
|
+
const validToolNames: string[] = [];
|
|
590
|
+
for (const name of toolNames) {
|
|
591
|
+
const tool = this._toolRegistry.get(name);
|
|
592
|
+
if (tool) {
|
|
593
|
+
tools.push(tool);
|
|
594
|
+
validToolNames.push(name);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
this.agent.setTools(tools);
|
|
598
|
+
|
|
599
|
+
// Rebuild base system prompt with new tool set
|
|
600
|
+
if (this._rebuildSystemPrompt) {
|
|
601
|
+
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
|
602
|
+
this.agent.setSystemPrompt(this._baseSystemPrompt);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
515
606
|
/** Whether auto-compaction is currently running */
|
|
516
607
|
get isCompacting(): boolean {
|
|
517
608
|
return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;
|
|
@@ -522,9 +613,14 @@ export class AgentSession {
|
|
|
522
613
|
return this.agent.state.messages;
|
|
523
614
|
}
|
|
524
615
|
|
|
525
|
-
/** Current
|
|
526
|
-
get
|
|
527
|
-
return this.agent.
|
|
616
|
+
/** Current steering mode */
|
|
617
|
+
get steeringMode(): "all" | "one-at-a-time" {
|
|
618
|
+
return this.agent.getSteeringMode();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Current follow-up mode */
|
|
622
|
+
get followUpMode(): "all" | "one-at-a-time" {
|
|
623
|
+
return this.agent.getFollowUpMode();
|
|
528
624
|
}
|
|
529
625
|
|
|
530
626
|
/** Current interrupt mode */
|
|
@@ -547,9 +643,9 @@ export class AgentSession {
|
|
|
547
643
|
return this._scopedModels;
|
|
548
644
|
}
|
|
549
645
|
|
|
550
|
-
/**
|
|
551
|
-
get
|
|
552
|
-
return this.
|
|
646
|
+
/** Prompt templates */
|
|
647
|
+
get promptTemplates(): ReadonlyArray<PromptTemplate> {
|
|
648
|
+
return this._promptTemplates;
|
|
553
649
|
}
|
|
554
650
|
|
|
555
651
|
/** Custom commands (TypeScript slash commands) */
|
|
@@ -563,22 +659,20 @@ export class AgentSession {
|
|
|
563
659
|
|
|
564
660
|
/**
|
|
565
661
|
* Send a prompt to the agent.
|
|
566
|
-
* -
|
|
567
|
-
* -
|
|
568
|
-
* -
|
|
569
|
-
*
|
|
662
|
+
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
663
|
+
* - Expands file-based prompt templates by default
|
|
664
|
+
* - During streaming, queues via steer() or followUp() based on streamingBehavior option
|
|
665
|
+
* - Validates model and API key before sending (when not streaming)
|
|
666
|
+
* @throws Error if streaming and no streamingBehavior specified
|
|
667
|
+
* @throws Error if no model selected or no API key available (when not streaming)
|
|
570
668
|
*/
|
|
571
669
|
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
572
|
-
|
|
573
|
-
this._flushPendingBashMessages();
|
|
670
|
+
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
574
671
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (expandCommands && text.startsWith("/")) {
|
|
579
|
-
const handled = await this._tryExecuteHookCommand(text);
|
|
672
|
+
// Handle extension commands first (execute immediately, even during streaming)
|
|
673
|
+
if (expandPromptTemplates && text.startsWith("/")) {
|
|
674
|
+
const handled = await this._tryExecuteExtensionCommand(text);
|
|
580
675
|
if (handled) {
|
|
581
|
-
// Hook command executed, no prompt to send
|
|
582
676
|
return;
|
|
583
677
|
}
|
|
584
678
|
|
|
@@ -586,14 +680,33 @@ export class AgentSession {
|
|
|
586
680
|
const customResult = await this._tryExecuteCustomCommand(text);
|
|
587
681
|
if (customResult !== null) {
|
|
588
682
|
if (customResult === "") {
|
|
589
|
-
// Command handled, nothing to send
|
|
590
683
|
return;
|
|
591
684
|
}
|
|
592
|
-
// Command returned a prompt - use it instead of the original text
|
|
593
685
|
text = customResult;
|
|
594
686
|
}
|
|
595
687
|
}
|
|
596
688
|
|
|
689
|
+
// Expand file-based prompt templates if requested
|
|
690
|
+
const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this._promptTemplates]) : text;
|
|
691
|
+
|
|
692
|
+
// If streaming, queue via steer() or followUp() based on option
|
|
693
|
+
if (this.isStreaming) {
|
|
694
|
+
if (!options?.streamingBehavior) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.",
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (options.streamingBehavior === "followUp") {
|
|
700
|
+
await this._queueFollowUp(expandedText);
|
|
701
|
+
} else {
|
|
702
|
+
await this._queueSteer(expandedText);
|
|
703
|
+
}
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Flush any pending bash messages before the new prompt
|
|
708
|
+
this._flushPendingBashMessages();
|
|
709
|
+
|
|
597
710
|
// Validate model
|
|
598
711
|
if (!this.model) {
|
|
599
712
|
throw new Error(
|
|
@@ -618,10 +731,7 @@ export class AgentSession {
|
|
|
618
731
|
await this._checkCompaction(lastAssistant, false);
|
|
619
732
|
}
|
|
620
733
|
|
|
621
|
-
//
|
|
622
|
-
const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text;
|
|
623
|
-
|
|
624
|
-
// Build messages array (hook message if any, then user message)
|
|
734
|
+
// Build messages array (custom messages if any, then user message)
|
|
625
735
|
const messages: AgentMessage[] = [];
|
|
626
736
|
|
|
627
737
|
// Add user message
|
|
@@ -635,6 +745,12 @@ export class AgentSession {
|
|
|
635
745
|
timestamp: Date.now(),
|
|
636
746
|
});
|
|
637
747
|
|
|
748
|
+
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
749
|
+
for (const msg of this._pendingNextTurnMessages) {
|
|
750
|
+
messages.push(msg);
|
|
751
|
+
}
|
|
752
|
+
this._pendingNextTurnMessages = [];
|
|
753
|
+
|
|
638
754
|
// Auto-read @filepath mentions
|
|
639
755
|
const fileMentions = extractFileMentions(expandedText);
|
|
640
756
|
if (fileMentions.length > 0) {
|
|
@@ -642,18 +758,26 @@ export class AgentSession {
|
|
|
642
758
|
messages.push(...fileMentionMessages);
|
|
643
759
|
}
|
|
644
760
|
|
|
645
|
-
// Emit before_agent_start
|
|
646
|
-
if (this.
|
|
647
|
-
const result = await this.
|
|
648
|
-
if (result?.
|
|
649
|
-
messages
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
761
|
+
// Emit before_agent_start extension event
|
|
762
|
+
if (this._extensionRunner) {
|
|
763
|
+
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, options?.images);
|
|
764
|
+
if (result?.messages) {
|
|
765
|
+
for (const msg of result.messages) {
|
|
766
|
+
messages.push({
|
|
767
|
+
role: "custom",
|
|
768
|
+
customType: msg.customType,
|
|
769
|
+
content: msg.content,
|
|
770
|
+
display: msg.display,
|
|
771
|
+
details: msg.details,
|
|
772
|
+
timestamp: Date.now(),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (result?.systemPromptAppend) {
|
|
778
|
+
this.agent.setSystemPrompt(`${this._baseSystemPrompt}\n\n${result.systemPromptAppend}`);
|
|
779
|
+
} else {
|
|
780
|
+
this.agent.setSystemPrompt(this._baseSystemPrompt);
|
|
657
781
|
}
|
|
658
782
|
}
|
|
659
783
|
|
|
@@ -662,29 +786,29 @@ export class AgentSession {
|
|
|
662
786
|
}
|
|
663
787
|
|
|
664
788
|
/**
|
|
665
|
-
* Try to execute
|
|
789
|
+
* Try to execute an extension command. Returns true if command was found and executed.
|
|
666
790
|
*/
|
|
667
|
-
private async
|
|
668
|
-
if (!this.
|
|
791
|
+
private async _tryExecuteExtensionCommand(text: string): Promise<boolean> {
|
|
792
|
+
if (!this._extensionRunner) return false;
|
|
669
793
|
|
|
670
794
|
// Parse command name and args
|
|
671
795
|
const spaceIndex = text.indexOf(" ");
|
|
672
796
|
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
673
797
|
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
|
|
674
798
|
|
|
675
|
-
const command = this.
|
|
799
|
+
const command = this._extensionRunner.getCommand(commandName);
|
|
676
800
|
if (!command) return false;
|
|
677
801
|
|
|
678
|
-
// Get command context from
|
|
679
|
-
const ctx = this.
|
|
802
|
+
// Get command context from extension runner (includes session control methods)
|
|
803
|
+
const ctx = this._extensionRunner.createCommandContext();
|
|
680
804
|
|
|
681
805
|
try {
|
|
682
806
|
await command.handler(args, ctx);
|
|
683
807
|
return true;
|
|
684
808
|
} catch (err) {
|
|
685
|
-
// Emit error via
|
|
686
|
-
this.
|
|
687
|
-
|
|
809
|
+
// Emit error via extension runner
|
|
810
|
+
this._extensionRunner.emitError({
|
|
811
|
+
extensionPath: `command:${commandName}`,
|
|
688
812
|
event: "command",
|
|
689
813
|
error: err instanceof Error ? err.message : String(err),
|
|
690
814
|
});
|
|
@@ -692,13 +816,52 @@ export class AgentSession {
|
|
|
692
816
|
}
|
|
693
817
|
}
|
|
694
818
|
|
|
819
|
+
private _createCommandContext(): ExtensionCommandContext {
|
|
820
|
+
if (this._extensionRunner) {
|
|
821
|
+
return this._extensionRunner.createCommandContext();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
ui: noOpUIContext,
|
|
826
|
+
hasUI: false,
|
|
827
|
+
cwd: this.sessionManager.getCwd(),
|
|
828
|
+
sessionManager: this.sessionManager,
|
|
829
|
+
modelRegistry: this._modelRegistry,
|
|
830
|
+
model: this.model ?? undefined,
|
|
831
|
+
isIdle: () => !this.isStreaming,
|
|
832
|
+
abort: () => {
|
|
833
|
+
void this.abort();
|
|
834
|
+
},
|
|
835
|
+
hasPendingMessages: () => this.queuedMessageCount > 0,
|
|
836
|
+
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
837
|
+
waitForIdle: () => this.agent.waitForIdle(),
|
|
838
|
+
newSession: async (options) => {
|
|
839
|
+
const success = await this.newSession({ parentSession: options?.parentSession });
|
|
840
|
+
if (!success) {
|
|
841
|
+
return { cancelled: true };
|
|
842
|
+
}
|
|
843
|
+
if (options?.setup) {
|
|
844
|
+
await options.setup(this.sessionManager);
|
|
845
|
+
}
|
|
846
|
+
return { cancelled: false };
|
|
847
|
+
},
|
|
848
|
+
branch: async (entryId) => {
|
|
849
|
+
const result = await this.branch(entryId);
|
|
850
|
+
return { cancelled: result.cancelled };
|
|
851
|
+
},
|
|
852
|
+
navigateTree: async (targetId, options) => {
|
|
853
|
+
const result = await this.navigateTree(targetId, { summarize: options?.summarize });
|
|
854
|
+
return { cancelled: result.cancelled };
|
|
855
|
+
},
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
695
859
|
/**
|
|
696
860
|
* Try to execute a custom command. Returns the prompt string if found, null otherwise.
|
|
697
861
|
* If the command returns void, returns empty string to indicate it was handled.
|
|
698
862
|
*/
|
|
699
863
|
private async _tryExecuteCustomCommand(text: string): Promise<string | null> {
|
|
700
864
|
if (this._customCommands.length === 0) return null;
|
|
701
|
-
if (!this._hookRunner) return null; // Need hook runner for command context
|
|
702
865
|
|
|
703
866
|
// Parse command name and args
|
|
704
867
|
const spaceIndex = text.indexOf(" ");
|
|
@@ -709,8 +872,12 @@ export class AgentSession {
|
|
|
709
872
|
const loaded = this._customCommands.find((c) => c.command.name === commandName);
|
|
710
873
|
if (!loaded) return null;
|
|
711
874
|
|
|
712
|
-
// Get command context from
|
|
713
|
-
const
|
|
875
|
+
// Get command context from extension runner (includes session control methods)
|
|
876
|
+
const baseCtx = this._createCommandContext();
|
|
877
|
+
const ctx = {
|
|
878
|
+
...baseCtx,
|
|
879
|
+
hasQueuedMessages: baseCtx.hasPendingMessages,
|
|
880
|
+
} as HookCommandContext;
|
|
714
881
|
|
|
715
882
|
try {
|
|
716
883
|
const args = parseCommandArgs(argsString);
|
|
@@ -719,23 +886,51 @@ export class AgentSession {
|
|
|
719
886
|
// If void/undefined, command handled everything
|
|
720
887
|
return result ?? "";
|
|
721
888
|
} catch (err) {
|
|
722
|
-
// Emit error via
|
|
723
|
-
this.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
889
|
+
// Emit error via extension runner
|
|
890
|
+
if (this._extensionRunner) {
|
|
891
|
+
this._extensionRunner.emitError({
|
|
892
|
+
extensionPath: `custom-command:${commandName}`,
|
|
893
|
+
event: "command",
|
|
894
|
+
error: err instanceof Error ? err.message : String(err),
|
|
895
|
+
});
|
|
896
|
+
} else {
|
|
897
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
898
|
+
console.error(`Custom command "${commandName}" failed: ${message}`);
|
|
899
|
+
}
|
|
728
900
|
return ""; // Command was handled (with error)
|
|
729
901
|
}
|
|
730
902
|
}
|
|
731
903
|
|
|
732
904
|
/**
|
|
733
|
-
* Queue a message to
|
|
734
|
-
|
|
905
|
+
* Queue a steering message to interrupt the agent mid-run.
|
|
906
|
+
*/
|
|
907
|
+
async steer(text: string): Promise<void> {
|
|
908
|
+
if (text.startsWith("/")) {
|
|
909
|
+
this._throwIfExtensionCommand(text);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
|
913
|
+
await this._queueSteer(expandedText);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Queue a follow-up message to process after the agent would otherwise stop.
|
|
735
918
|
*/
|
|
736
|
-
async
|
|
737
|
-
|
|
738
|
-
|
|
919
|
+
async followUp(text: string): Promise<void> {
|
|
920
|
+
if (text.startsWith("/")) {
|
|
921
|
+
this._throwIfExtensionCommand(text);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
|
925
|
+
await this._queueFollowUp(expandedText);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
930
|
+
*/
|
|
931
|
+
private async _queueSteer(text: string): Promise<void> {
|
|
932
|
+
this._steeringMessages.push(text);
|
|
933
|
+
this.agent.steer({
|
|
739
934
|
role: "user",
|
|
740
935
|
content: [{ type: "text", text }],
|
|
741
936
|
timestamp: Date.now(),
|
|
@@ -743,65 +938,103 @@ export class AgentSession {
|
|
|
743
938
|
}
|
|
744
939
|
|
|
745
940
|
/**
|
|
746
|
-
*
|
|
941
|
+
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
942
|
+
*/
|
|
943
|
+
private async _queueFollowUp(text: string): Promise<void> {
|
|
944
|
+
this._followUpMessages.push(text);
|
|
945
|
+
this.agent.followUp({
|
|
946
|
+
role: "user",
|
|
947
|
+
content: [{ type: "text", text }],
|
|
948
|
+
timestamp: Date.now(),
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Throw an error if the text is an extension command.
|
|
954
|
+
*/
|
|
955
|
+
private _throwIfExtensionCommand(text: string): void {
|
|
956
|
+
if (!this._extensionRunner) return;
|
|
957
|
+
|
|
958
|
+
const spaceIndex = text.indexOf(" ");
|
|
959
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
960
|
+
const command = this._extensionRunner.getCommand(commandName);
|
|
961
|
+
|
|
962
|
+
if (command) {
|
|
963
|
+
throw new Error(
|
|
964
|
+
`Extension command "/${commandName}" cannot be queued. Use prompt() or execute the command when not streaming.`,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Send a custom message to the session. Creates a CustomMessageEntry.
|
|
747
971
|
*
|
|
748
972
|
* Handles three cases:
|
|
749
|
-
* - Streaming:
|
|
973
|
+
* - Streaming: queue as steer/follow-up or store for next turn
|
|
750
974
|
* - Not streaming + triggerTurn: appends to state/session, starts new turn
|
|
751
975
|
* - Not streaming + no trigger: appends to state/session, no turn
|
|
752
|
-
*
|
|
753
|
-
* @param message Hook message with customType, content, display, details
|
|
754
|
-
* @param triggerTurn If true and not streaming, triggers a new LLM turn
|
|
755
976
|
*/
|
|
756
|
-
async
|
|
757
|
-
message: Pick<
|
|
758
|
-
triggerTurn?: boolean,
|
|
977
|
+
async sendCustomMessage<T = unknown>(
|
|
978
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
|
979
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
759
980
|
): Promise<void> {
|
|
760
|
-
const appMessage = {
|
|
761
|
-
role: "
|
|
981
|
+
const appMessage: CustomMessage<T> = {
|
|
982
|
+
role: "custom",
|
|
762
983
|
customType: message.customType,
|
|
763
984
|
content: message.content,
|
|
764
985
|
display: message.display,
|
|
765
986
|
details: message.details,
|
|
766
987
|
timestamp: Date.now(),
|
|
767
|
-
}
|
|
988
|
+
};
|
|
768
989
|
if (this.isStreaming) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
990
|
+
if (options?.deliverAs === "nextTurn") {
|
|
991
|
+
this._pendingNextTurnMessages.push(appMessage);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (options?.deliverAs === "followUp") {
|
|
996
|
+
this.agent.followUp(appMessage);
|
|
997
|
+
} else {
|
|
998
|
+
this.agent.steer(appMessage);
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (options?.triggerTurn) {
|
|
773
1004
|
await this.agent.prompt(appMessage);
|
|
774
|
-
|
|
775
|
-
// Just append to agent state and session, no turn
|
|
776
|
-
this.agent.appendMessage(appMessage);
|
|
777
|
-
this.sessionManager.appendCustomMessageEntry(
|
|
778
|
-
message.customType,
|
|
779
|
-
message.content,
|
|
780
|
-
message.display,
|
|
781
|
-
message.details,
|
|
782
|
-
);
|
|
1005
|
+
return;
|
|
783
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
this.agent.appendMessage(appMessage);
|
|
1009
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
1010
|
+
message.customType,
|
|
1011
|
+
message.content,
|
|
1012
|
+
message.display,
|
|
1013
|
+
message.details,
|
|
1014
|
+
);
|
|
784
1015
|
}
|
|
785
1016
|
|
|
786
1017
|
/**
|
|
787
1018
|
* Clear queued messages and return them.
|
|
788
1019
|
* Useful for restoring to editor when user aborts.
|
|
789
1020
|
*/
|
|
790
|
-
clearQueue(): string[] {
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
this.
|
|
794
|
-
|
|
1021
|
+
clearQueue(): { steering: string[]; followUp: string[] } {
|
|
1022
|
+
const steering = [...this._steeringMessages];
|
|
1023
|
+
const followUp = [...this._followUpMessages];
|
|
1024
|
+
this._steeringMessages = [];
|
|
1025
|
+
this._followUpMessages = [];
|
|
1026
|
+
this.agent.clearAllQueues();
|
|
1027
|
+
return { steering, followUp };
|
|
795
1028
|
}
|
|
796
1029
|
|
|
797
|
-
/** Number of messages
|
|
1030
|
+
/** Number of pending messages (includes both steering and follow-up) */
|
|
798
1031
|
get queuedMessageCount(): number {
|
|
799
|
-
return this.
|
|
1032
|
+
return this._steeringMessages.length + this._followUpMessages.length;
|
|
800
1033
|
}
|
|
801
1034
|
|
|
802
|
-
/** Get
|
|
803
|
-
getQueuedMessages(): readonly string[] {
|
|
804
|
-
return this.
|
|
1035
|
+
/** Get pending messages (read-only) */
|
|
1036
|
+
getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
|
|
1037
|
+
return { steering: this._steeringMessages, followUp: this._followUpMessages };
|
|
805
1038
|
}
|
|
806
1039
|
|
|
807
1040
|
get skillsSettings(): Required<SkillsSettings> | undefined {
|
|
@@ -828,8 +1061,8 @@ export class AgentSession {
|
|
|
828
1061
|
const previousSessionFile = this.sessionFile;
|
|
829
1062
|
|
|
830
1063
|
// Emit session_before_switch event with reason "new" (can be cancelled)
|
|
831
|
-
if (this.
|
|
832
|
-
const result = (await this.
|
|
1064
|
+
if (this._extensionRunner?.hasHandlers("session_before_switch")) {
|
|
1065
|
+
const result = (await this._extensionRunner.emit({
|
|
833
1066
|
type: "session_before_switch",
|
|
834
1067
|
reason: "new",
|
|
835
1068
|
})) as SessionBeforeSwitchResult | undefined;
|
|
@@ -844,20 +1077,20 @@ export class AgentSession {
|
|
|
844
1077
|
this.agent.reset();
|
|
845
1078
|
await this.sessionManager.flush();
|
|
846
1079
|
this.sessionManager.newSession(options);
|
|
847
|
-
this.
|
|
1080
|
+
this._steeringMessages = [];
|
|
1081
|
+
this._followUpMessages = [];
|
|
1082
|
+
this._pendingNextTurnMessages = [];
|
|
848
1083
|
this._reconnectToAgent();
|
|
849
1084
|
|
|
850
1085
|
// Emit session_switch event with reason "new" to hooks
|
|
851
|
-
if (this.
|
|
852
|
-
await this.
|
|
1086
|
+
if (this._extensionRunner) {
|
|
1087
|
+
await this._extensionRunner.emit({
|
|
853
1088
|
type: "session_switch",
|
|
854
1089
|
reason: "new",
|
|
855
1090
|
previousSessionFile,
|
|
856
1091
|
});
|
|
857
1092
|
}
|
|
858
1093
|
|
|
859
|
-
// Emit session event to custom tools
|
|
860
|
-
await this.emitCustomToolSessionEvent("switch", previousSessionFile);
|
|
861
1094
|
return true;
|
|
862
1095
|
}
|
|
863
1096
|
|
|
@@ -897,6 +1130,55 @@ export class AgentSession {
|
|
|
897
1130
|
return this._cycleAvailableModel(direction);
|
|
898
1131
|
}
|
|
899
1132
|
|
|
1133
|
+
/**
|
|
1134
|
+
* Cycle through configured role models in a fixed order.
|
|
1135
|
+
* Skips missing roles and deduplicates models.
|
|
1136
|
+
*/
|
|
1137
|
+
async cycleRoleModels(roleOrder: string[]): Promise<RoleModelCycleResult | undefined> {
|
|
1138
|
+
const availableModels = this._modelRegistry.getAvailable();
|
|
1139
|
+
if (availableModels.length === 0) return undefined;
|
|
1140
|
+
|
|
1141
|
+
const currentModel = this.model;
|
|
1142
|
+
if (!currentModel) return undefined;
|
|
1143
|
+
const roleModels: Array<{ role: string; model: Model<any> }> = [];
|
|
1144
|
+
const seen = new Set<string>();
|
|
1145
|
+
|
|
1146
|
+
for (const role of roleOrder) {
|
|
1147
|
+
const roleModelStr =
|
|
1148
|
+
role === "default"
|
|
1149
|
+
? (this.settingsManager.getModelRole("default") ?? `${currentModel.provider}/${currentModel.id}`)
|
|
1150
|
+
: this.settingsManager.getModelRole(role);
|
|
1151
|
+
if (!roleModelStr) continue;
|
|
1152
|
+
|
|
1153
|
+
const parsed = parseModelString(roleModelStr);
|
|
1154
|
+
let match: Model<any> | undefined;
|
|
1155
|
+
if (parsed) {
|
|
1156
|
+
match = availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
1157
|
+
}
|
|
1158
|
+
if (!match) {
|
|
1159
|
+
match = availableModels.find((m) => m.id.toLowerCase() === roleModelStr.toLowerCase());
|
|
1160
|
+
}
|
|
1161
|
+
if (!match) continue;
|
|
1162
|
+
|
|
1163
|
+
const key = `${match.provider}/${match.id}`;
|
|
1164
|
+
if (seen.has(key)) continue;
|
|
1165
|
+
seen.add(key);
|
|
1166
|
+
roleModels.push({ role, model: match });
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (roleModels.length <= 1) return undefined;
|
|
1170
|
+
|
|
1171
|
+
let currentIndex = roleModels.findIndex((entry) => modelsAreEqual(entry.model, currentModel));
|
|
1172
|
+
if (currentIndex === -1) currentIndex = 0;
|
|
1173
|
+
|
|
1174
|
+
const nextIndex = (currentIndex + 1) % roleModels.length;
|
|
1175
|
+
const next = roleModels[nextIndex];
|
|
1176
|
+
|
|
1177
|
+
await this.setModel(next.model, next.role);
|
|
1178
|
+
|
|
1179
|
+
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
900
1182
|
private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
901
1183
|
if (this._scopedModels.length <= 1) return undefined;
|
|
902
1184
|
|
|
@@ -926,7 +1208,7 @@ export class AgentSession {
|
|
|
926
1208
|
}
|
|
927
1209
|
|
|
928
1210
|
private async _cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
929
|
-
const availableModels =
|
|
1211
|
+
const availableModels = this._modelRegistry.getAvailable();
|
|
930
1212
|
if (availableModels.length <= 1) return undefined;
|
|
931
1213
|
|
|
932
1214
|
const currentModel = this.model;
|
|
@@ -955,7 +1237,7 @@ export class AgentSession {
|
|
|
955
1237
|
/**
|
|
956
1238
|
* Get all available models with valid API keys.
|
|
957
1239
|
*/
|
|
958
|
-
|
|
1240
|
+
getAvailableModels(): Model<any>[] {
|
|
959
1241
|
return this._modelRegistry.getAvailable();
|
|
960
1242
|
}
|
|
961
1243
|
|
|
@@ -1018,16 +1300,25 @@ export class AgentSession {
|
|
|
1018
1300
|
}
|
|
1019
1301
|
|
|
1020
1302
|
// =========================================================================
|
|
1021
|
-
// Queue Mode Management
|
|
1303
|
+
// Message Queue Mode Management
|
|
1022
1304
|
// =========================================================================
|
|
1023
1305
|
|
|
1024
1306
|
/**
|
|
1025
|
-
* Set
|
|
1307
|
+
* Set steering mode.
|
|
1308
|
+
* Saves to settings.
|
|
1309
|
+
*/
|
|
1310
|
+
setSteeringMode(mode: "all" | "one-at-a-time"): void {
|
|
1311
|
+
this.agent.setSteeringMode(mode);
|
|
1312
|
+
this.settingsManager.setSteeringMode(mode);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Set follow-up mode.
|
|
1026
1317
|
* Saves to settings.
|
|
1027
1318
|
*/
|
|
1028
|
-
|
|
1029
|
-
this.agent.
|
|
1030
|
-
this.settingsManager.
|
|
1319
|
+
setFollowUpMode(mode: "all" | "one-at-a-time"): void {
|
|
1320
|
+
this.agent.setFollowUpMode(mode);
|
|
1321
|
+
this.settingsManager.setFollowUpMode(mode);
|
|
1031
1322
|
}
|
|
1032
1323
|
|
|
1033
1324
|
/**
|
|
@@ -1077,10 +1368,10 @@ export class AgentSession {
|
|
|
1077
1368
|
}
|
|
1078
1369
|
|
|
1079
1370
|
let hookCompaction: CompactionResult | undefined;
|
|
1080
|
-
let
|
|
1371
|
+
let fromExtension = false;
|
|
1081
1372
|
|
|
1082
|
-
if (this.
|
|
1083
|
-
const result = (await this.
|
|
1373
|
+
if (this._extensionRunner?.hasHandlers("session_before_compact")) {
|
|
1374
|
+
const result = (await this._extensionRunner.emit({
|
|
1084
1375
|
type: "session_before_compact",
|
|
1085
1376
|
preparation,
|
|
1086
1377
|
branchEntries: pathEntries,
|
|
@@ -1094,7 +1385,7 @@ export class AgentSession {
|
|
|
1094
1385
|
|
|
1095
1386
|
if (result?.compaction) {
|
|
1096
1387
|
hookCompaction = result.compaction;
|
|
1097
|
-
|
|
1388
|
+
fromExtension = true;
|
|
1098
1389
|
}
|
|
1099
1390
|
}
|
|
1100
1391
|
|
|
@@ -1104,7 +1395,7 @@ export class AgentSession {
|
|
|
1104
1395
|
let details: unknown;
|
|
1105
1396
|
|
|
1106
1397
|
if (hookCompaction) {
|
|
1107
|
-
//
|
|
1398
|
+
// Extension provided compaction content
|
|
1108
1399
|
summary = hookCompaction.summary;
|
|
1109
1400
|
firstKeptEntryId = hookCompaction.firstKeptEntryId;
|
|
1110
1401
|
tokensBefore = hookCompaction.tokensBefore;
|
|
@@ -1128,7 +1419,7 @@ export class AgentSession {
|
|
|
1128
1419
|
throw new Error("Compaction cancelled");
|
|
1129
1420
|
}
|
|
1130
1421
|
|
|
1131
|
-
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details,
|
|
1422
|
+
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
1132
1423
|
const newEntries = this.sessionManager.getEntries();
|
|
1133
1424
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1134
1425
|
this.agent.replaceMessages(sessionContext.messages);
|
|
@@ -1138,11 +1429,11 @@ export class AgentSession {
|
|
|
1138
1429
|
| CompactionEntry
|
|
1139
1430
|
| undefined;
|
|
1140
1431
|
|
|
1141
|
-
if (this.
|
|
1142
|
-
await this.
|
|
1432
|
+
if (this._extensionRunner && savedCompactionEntry) {
|
|
1433
|
+
await this._extensionRunner.emit({
|
|
1143
1434
|
type: "session_compact",
|
|
1144
1435
|
compactionEntry: savedCompactionEntry,
|
|
1145
|
-
|
|
1436
|
+
fromExtension,
|
|
1146
1437
|
});
|
|
1147
1438
|
}
|
|
1148
1439
|
|
|
@@ -1249,10 +1540,10 @@ export class AgentSession {
|
|
|
1249
1540
|
}
|
|
1250
1541
|
|
|
1251
1542
|
let hookCompaction: CompactionResult | undefined;
|
|
1252
|
-
let
|
|
1543
|
+
let fromExtension = false;
|
|
1253
1544
|
|
|
1254
|
-
if (this.
|
|
1255
|
-
const hookResult = (await this.
|
|
1545
|
+
if (this._extensionRunner?.hasHandlers("session_before_compact")) {
|
|
1546
|
+
const hookResult = (await this._extensionRunner.emit({
|
|
1256
1547
|
type: "session_before_compact",
|
|
1257
1548
|
preparation,
|
|
1258
1549
|
branchEntries: pathEntries,
|
|
@@ -1267,7 +1558,7 @@ export class AgentSession {
|
|
|
1267
1558
|
|
|
1268
1559
|
if (hookResult?.compaction) {
|
|
1269
1560
|
hookCompaction = hookResult.compaction;
|
|
1270
|
-
|
|
1561
|
+
fromExtension = true;
|
|
1271
1562
|
}
|
|
1272
1563
|
}
|
|
1273
1564
|
|
|
@@ -1277,7 +1568,7 @@ export class AgentSession {
|
|
|
1277
1568
|
let details: unknown;
|
|
1278
1569
|
|
|
1279
1570
|
if (hookCompaction) {
|
|
1280
|
-
//
|
|
1571
|
+
// Extension provided compaction content
|
|
1281
1572
|
summary = hookCompaction.summary;
|
|
1282
1573
|
firstKeptEntryId = hookCompaction.firstKeptEntryId;
|
|
1283
1574
|
tokensBefore = hookCompaction.tokensBefore;
|
|
@@ -1302,7 +1593,7 @@ export class AgentSession {
|
|
|
1302
1593
|
return;
|
|
1303
1594
|
}
|
|
1304
1595
|
|
|
1305
|
-
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details,
|
|
1596
|
+
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
1306
1597
|
const newEntries = this.sessionManager.getEntries();
|
|
1307
1598
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1308
1599
|
this.agent.replaceMessages(sessionContext.messages);
|
|
@@ -1312,11 +1603,11 @@ export class AgentSession {
|
|
|
1312
1603
|
| CompactionEntry
|
|
1313
1604
|
| undefined;
|
|
1314
1605
|
|
|
1315
|
-
if (this.
|
|
1316
|
-
await this.
|
|
1606
|
+
if (this._extensionRunner && savedCompactionEntry) {
|
|
1607
|
+
await this._extensionRunner.emit({
|
|
1317
1608
|
type: "session_compact",
|
|
1318
1609
|
compactionEntry: savedCompactionEntry,
|
|
1319
|
-
|
|
1610
|
+
fromExtension,
|
|
1320
1611
|
});
|
|
1321
1612
|
}
|
|
1322
1613
|
|
|
@@ -1533,8 +1824,13 @@ export class AgentSession {
|
|
|
1533
1824
|
* Adds result to agent context and session.
|
|
1534
1825
|
* @param command The bash command to execute
|
|
1535
1826
|
* @param onChunk Optional streaming callback for output
|
|
1827
|
+
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
1536
1828
|
*/
|
|
1537
|
-
async executeBash(
|
|
1829
|
+
async executeBash(
|
|
1830
|
+
command: string,
|
|
1831
|
+
onChunk?: (chunk: string) => void,
|
|
1832
|
+
options?: { excludeFromContext?: boolean },
|
|
1833
|
+
): Promise<BashResult> {
|
|
1538
1834
|
this._bashAbortController = new AbortController();
|
|
1539
1835
|
|
|
1540
1836
|
try {
|
|
@@ -1553,6 +1849,7 @@ export class AgentSession {
|
|
|
1553
1849
|
truncated: result.truncated,
|
|
1554
1850
|
fullOutputPath: result.fullOutputPath,
|
|
1555
1851
|
timestamp: Date.now(),
|
|
1852
|
+
excludeFromContext: options?.excludeFromContext,
|
|
1556
1853
|
};
|
|
1557
1854
|
|
|
1558
1855
|
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
@@ -1622,8 +1919,8 @@ export class AgentSession {
|
|
|
1622
1919
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
1623
1920
|
|
|
1624
1921
|
// Emit session_before_switch event (can be cancelled)
|
|
1625
|
-
if (this.
|
|
1626
|
-
const result = (await this.
|
|
1922
|
+
if (this._extensionRunner?.hasHandlers("session_before_switch")) {
|
|
1923
|
+
const result = (await this._extensionRunner.emit({
|
|
1627
1924
|
type: "session_before_switch",
|
|
1628
1925
|
reason: "resume",
|
|
1629
1926
|
targetSessionFile: sessionPath,
|
|
@@ -1636,7 +1933,9 @@ export class AgentSession {
|
|
|
1636
1933
|
|
|
1637
1934
|
this._disconnectFromAgent();
|
|
1638
1935
|
await this.abort();
|
|
1639
|
-
this.
|
|
1936
|
+
this._steeringMessages = [];
|
|
1937
|
+
this._followUpMessages = [];
|
|
1938
|
+
this._pendingNextTurnMessages = [];
|
|
1640
1939
|
|
|
1641
1940
|
// Flush pending writes before switching
|
|
1642
1941
|
await this.sessionManager.flush();
|
|
@@ -1648,17 +1947,14 @@ export class AgentSession {
|
|
|
1648
1947
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1649
1948
|
|
|
1650
1949
|
// Emit session_switch event to hooks
|
|
1651
|
-
if (this.
|
|
1652
|
-
await this.
|
|
1950
|
+
if (this._extensionRunner) {
|
|
1951
|
+
await this._extensionRunner.emit({
|
|
1653
1952
|
type: "session_switch",
|
|
1654
1953
|
reason: "resume",
|
|
1655
1954
|
previousSessionFile,
|
|
1656
1955
|
});
|
|
1657
1956
|
}
|
|
1658
1957
|
|
|
1659
|
-
// Emit session event to custom tools
|
|
1660
|
-
await this.emitCustomToolSessionEvent("switch", previousSessionFile);
|
|
1661
|
-
|
|
1662
1958
|
this.agent.replaceMessages(sessionContext.messages);
|
|
1663
1959
|
|
|
1664
1960
|
// Restore model if saved
|
|
@@ -1668,7 +1964,7 @@ export class AgentSession {
|
|
|
1668
1964
|
if (slashIdx > 0) {
|
|
1669
1965
|
const provider = defaultModelStr.slice(0, slashIdx);
|
|
1670
1966
|
const modelId = defaultModelStr.slice(slashIdx + 1);
|
|
1671
|
-
const availableModels =
|
|
1967
|
+
const availableModels = this._modelRegistry.getAvailable();
|
|
1672
1968
|
const match = availableModels.find((m) => m.provider === provider && m.id === modelId);
|
|
1673
1969
|
if (match) {
|
|
1674
1970
|
this.agent.setModel(match);
|
|
@@ -1707,8 +2003,8 @@ export class AgentSession {
|
|
|
1707
2003
|
let skipConversationRestore = false;
|
|
1708
2004
|
|
|
1709
2005
|
// Emit session_before_branch event (can be cancelled)
|
|
1710
|
-
if (this.
|
|
1711
|
-
const result = (await this.
|
|
2006
|
+
if (this._extensionRunner?.hasHandlers("session_before_branch")) {
|
|
2007
|
+
const result = (await this._extensionRunner.emit({
|
|
1712
2008
|
type: "session_before_branch",
|
|
1713
2009
|
entryId,
|
|
1714
2010
|
})) as SessionBeforeBranchResult | undefined;
|
|
@@ -1719,6 +2015,9 @@ export class AgentSession {
|
|
|
1719
2015
|
skipConversationRestore = result?.skipConversationRestore ?? false;
|
|
1720
2016
|
}
|
|
1721
2017
|
|
|
2018
|
+
// Clear pending messages (bound to old session state)
|
|
2019
|
+
this._pendingNextTurnMessages = [];
|
|
2020
|
+
|
|
1722
2021
|
// Flush pending writes before branching
|
|
1723
2022
|
await this.sessionManager.flush();
|
|
1724
2023
|
|
|
@@ -1732,16 +2031,13 @@ export class AgentSession {
|
|
|
1732
2031
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1733
2032
|
|
|
1734
2033
|
// Emit session_branch event to hooks (after branch completes)
|
|
1735
|
-
if (this.
|
|
1736
|
-
await this.
|
|
2034
|
+
if (this._extensionRunner) {
|
|
2035
|
+
await this._extensionRunner.emit({
|
|
1737
2036
|
type: "session_branch",
|
|
1738
2037
|
previousSessionFile,
|
|
1739
2038
|
});
|
|
1740
2039
|
}
|
|
1741
2040
|
|
|
1742
|
-
// Emit session event to custom tools (with reason "branch")
|
|
1743
|
-
await this.emitCustomToolSessionEvent("branch", previousSessionFile);
|
|
1744
|
-
|
|
1745
2041
|
if (!skipConversationRestore) {
|
|
1746
2042
|
this.agent.replaceMessages(sessionContext.messages);
|
|
1747
2043
|
}
|
|
@@ -1802,11 +2098,11 @@ export class AgentSession {
|
|
|
1802
2098
|
// Set up abort controller for summarization
|
|
1803
2099
|
this._branchSummaryAbortController = new AbortController();
|
|
1804
2100
|
let hookSummary: { summary: string; details?: unknown } | undefined;
|
|
1805
|
-
let
|
|
2101
|
+
let fromExtension = false;
|
|
1806
2102
|
|
|
1807
2103
|
// Emit session_before_tree event
|
|
1808
|
-
if (this.
|
|
1809
|
-
const result = (await this.
|
|
2104
|
+
if (this._extensionRunner?.hasHandlers("session_before_tree")) {
|
|
2105
|
+
const result = (await this._extensionRunner.emit({
|
|
1810
2106
|
type: "session_before_tree",
|
|
1811
2107
|
preparation,
|
|
1812
2108
|
signal: this._branchSummaryAbortController.signal,
|
|
@@ -1818,7 +2114,7 @@ export class AgentSession {
|
|
|
1818
2114
|
|
|
1819
2115
|
if (result?.summary && options.summarize) {
|
|
1820
2116
|
hookSummary = result.summary;
|
|
1821
|
-
|
|
2117
|
+
fromExtension = true;
|
|
1822
2118
|
}
|
|
1823
2119
|
}
|
|
1824
2120
|
|
|
@@ -1884,7 +2180,7 @@ export class AgentSession {
|
|
|
1884
2180
|
let summaryEntry: BranchSummaryEntry | undefined;
|
|
1885
2181
|
if (summaryText) {
|
|
1886
2182
|
// Create summary at target position (can be null for root)
|
|
1887
|
-
const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails,
|
|
2183
|
+
const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);
|
|
1888
2184
|
summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;
|
|
1889
2185
|
} else if (newLeafId === null) {
|
|
1890
2186
|
// No summary, navigating to root - reset leaf
|
|
@@ -1899,19 +2195,16 @@ export class AgentSession {
|
|
|
1899
2195
|
this.agent.replaceMessages(sessionContext.messages);
|
|
1900
2196
|
|
|
1901
2197
|
// Emit session_tree event
|
|
1902
|
-
if (this.
|
|
1903
|
-
await this.
|
|
2198
|
+
if (this._extensionRunner) {
|
|
2199
|
+
await this._extensionRunner.emit({
|
|
1904
2200
|
type: "session_tree",
|
|
1905
2201
|
newLeafId: this.sessionManager.getLeafId(),
|
|
1906
2202
|
oldLeafId,
|
|
1907
2203
|
summaryEntry,
|
|
1908
|
-
|
|
2204
|
+
fromExtension: summaryText ? fromExtension : undefined,
|
|
1909
2205
|
});
|
|
1910
2206
|
}
|
|
1911
2207
|
|
|
1912
|
-
// Emit to custom tools
|
|
1913
|
-
await this.emitCustomToolSessionEvent("tree", this.sessionFile);
|
|
1914
|
-
|
|
1915
2208
|
this._branchSummaryAbortController = undefined;
|
|
1916
2209
|
return { editorText, cancelled: false, summaryEntry };
|
|
1917
2210
|
}
|
|
@@ -2119,60 +2412,30 @@ export class AgentSession {
|
|
|
2119
2412
|
}
|
|
2120
2413
|
|
|
2121
2414
|
// =========================================================================
|
|
2122
|
-
//
|
|
2415
|
+
// Extension System
|
|
2123
2416
|
// =========================================================================
|
|
2124
2417
|
|
|
2125
2418
|
/**
|
|
2126
|
-
* Check if
|
|
2419
|
+
* Check if extensions have handlers for a specific event type.
|
|
2127
2420
|
*/
|
|
2128
|
-
|
|
2129
|
-
return this.
|
|
2421
|
+
hasExtensionHandlers(eventType: string): boolean {
|
|
2422
|
+
return this._extensionRunner?.hasHandlers(eventType) ?? false;
|
|
2130
2423
|
}
|
|
2131
2424
|
|
|
2132
2425
|
/**
|
|
2133
|
-
* Get the
|
|
2426
|
+
* Get the extension runner (for setting UI context and error handlers).
|
|
2134
2427
|
*/
|
|
2135
|
-
get
|
|
2136
|
-
return this.
|
|
2428
|
+
get extensionRunner(): ExtensionRunner | undefined {
|
|
2429
|
+
return this._extensionRunner;
|
|
2137
2430
|
}
|
|
2138
2431
|
|
|
2139
2432
|
/**
|
|
2140
|
-
*
|
|
2433
|
+
* Emit a custom tool session event (backwards compatibility for older callers).
|
|
2141
2434
|
*/
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
* Emit session event to all custom tools.
|
|
2148
|
-
* Called on session switch, branch, tree navigation, and shutdown.
|
|
2149
|
-
*/
|
|
2150
|
-
async emitCustomToolSessionEvent(
|
|
2151
|
-
reason: CustomToolSessionEvent["reason"],
|
|
2152
|
-
previousSessionFile?: string | undefined,
|
|
2153
|
-
): Promise<void> {
|
|
2154
|
-
if (!this._customTools) return;
|
|
2155
|
-
|
|
2156
|
-
const event: CustomToolSessionEvent = { reason, previousSessionFile };
|
|
2157
|
-
const ctx: CustomToolContext = {
|
|
2158
|
-
sessionManager: this.sessionManager,
|
|
2159
|
-
modelRegistry: this._modelRegistry,
|
|
2160
|
-
model: this.agent.state.model,
|
|
2161
|
-
isIdle: () => !this.isStreaming,
|
|
2162
|
-
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
2163
|
-
abort: () => {
|
|
2164
|
-
this.abort();
|
|
2165
|
-
},
|
|
2166
|
-
};
|
|
2167
|
-
|
|
2168
|
-
for (const { tool } of this._customTools) {
|
|
2169
|
-
if (tool.onSession) {
|
|
2170
|
-
try {
|
|
2171
|
-
await tool.onSession(event, ctx);
|
|
2172
|
-
} catch (err) {
|
|
2173
|
-
logger.warn("Tool onSession error", { error: String(err) });
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2435
|
+
async emitCustomToolSessionEvent(reason: "start" | "switch" | "branch" | "tree" | "shutdown"): Promise<void> {
|
|
2436
|
+
if (!this._extensionRunner) return;
|
|
2437
|
+
if (reason !== "shutdown") return;
|
|
2438
|
+
if (!this._extensionRunner.hasHandlers("session_shutdown")) return;
|
|
2439
|
+
await this._extensionRunner.emit({ type: "session_shutdown" });
|
|
2177
2440
|
}
|
|
2178
2441
|
}
|