@mariozechner/pi-coding-agent 0.30.1 → 0.31.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 +251 -2
- package/README.md +105 -84
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts +3 -3
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +7 -10
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +73 -34
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +464 -210
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +2 -7
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +4 -52
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -2
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +2 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +84 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/core/compaction/branch-summarization.js +233 -0
- package/dist/core/compaction/branch-summarization.js.map +1 -0
- package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
- package/dist/core/compaction/compaction.d.ts.map +1 -0
- package/dist/core/compaction/compaction.js +558 -0
- package/dist/core/compaction/compaction.js.map +1 -0
- package/dist/core/compaction/index.d.ts +7 -0
- package/dist/core/compaction/index.d.ts.map +1 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/index.js.map +1 -0
- package/dist/core/compaction/utils.d.ts +35 -0
- package/dist/core/compaction/utils.d.ts.map +1 -0
- package/dist/core/compaction/utils.js +138 -0
- package/dist/core/compaction/utils.js.map +1 -0
- package/dist/core/custom-tools/index.d.ts +2 -1
- package/dist/core/custom-tools/index.d.ts.map +1 -1
- package/dist/core/custom-tools/index.js +1 -0
- package/dist/core/custom-tools/index.js.map +1 -1
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +13 -80
- package/dist/core/custom-tools/loader.js.map +1 -1
- package/dist/core/custom-tools/types.d.ts +84 -59
- package/dist/core/custom-tools/types.d.ts.map +1 -1
- package/dist/core/custom-tools/types.js.map +1 -1
- package/dist/core/custom-tools/wrapper.d.ts +15 -0
- package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
- package/dist/core/custom-tools/wrapper.js +23 -0
- package/dist/core/custom-tools/wrapper.js.map +1 -0
- package/dist/core/exec.d.ts +29 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +71 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/export-html/index.d.ts +17 -0
- package/dist/core/export-html/index.d.ts.map +1 -0
- package/dist/core/export-html/index.js +171 -0
- package/dist/core/export-html/index.js.map +1 -0
- package/dist/core/export-html/template.css +781 -0
- package/dist/core/export-html/template.html +54 -0
- package/dist/core/export-html/template.js +1185 -0
- package/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/dist/core/export-html/vendor/marked.min.js +6 -0
- package/dist/core/hooks/index.d.ts +4 -4
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +3 -3
- package/dist/core/hooks/index.js.map +1 -1
- package/dist/core/hooks/loader.d.ts +40 -5
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +43 -10
- package/dist/core/hooks/loader.js.map +1 -1
- package/dist/core/hooks/runner.d.ts +94 -18
- package/dist/core/hooks/runner.d.ts.map +1 -1
- package/dist/core/hooks/runner.js +199 -120
- package/dist/core/hooks/runner.js.map +1 -1
- package/dist/core/hooks/tool-wrapper.d.ts +1 -1
- package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
- package/dist/core/hooks/tool-wrapper.js +36 -19
- package/dist/core/hooks/tool-wrapper.js.map +1 -1
- package/dist/core/hooks/types.d.ts +407 -96
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/messages.d.ts +44 -12
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +82 -34
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +5 -5
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +7 -7
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +7 -7
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +45 -14
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +7 -10
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +88 -32
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +202 -36
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +565 -133
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +9 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +13 -12
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +33 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -0
- package/dist/core/tools/edit-diff.js +171 -0
- package/dist/core/tools/edit-diff.js.map +1 -0
- package/dist/core/tools/edit.d.ts +7 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +20 -95
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +25 -25
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts +28 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +125 -0
- package/dist/migrations.js.map +1 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +3 -4
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +6 -2
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
- package/dist/modes/interactive/components/bordered-loader.js +30 -0
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.js +35 -0
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.js +5 -1
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +12 -6
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +57 -25
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
- package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-editor.js +95 -0
- package/dist/modes/interactive/components/hook-editor.js.map +1 -0
- package/dist/modes/interactive/components/hook-message.d.ts +18 -0
- package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-message.js +80 -0
- package/dist/modes/interactive/components/hook-message.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +3 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +70 -21
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/tree-selector.js +745 -0
- package/dist/modes/interactive/components/tree-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +1 -1
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +2 -5
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +29 -12
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +589 -208
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +13 -1
- package/dist/modes/interactive/theme/light.json +13 -1
- package/dist/modes/interactive/theme/theme-schema.json +34 -0
- package/dist/modes/interactive/theme/theme.d.ts +20 -2
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +135 -2
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +3 -3
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +26 -20
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +13 -10
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +11 -10
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +88 -35
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +30 -11
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/shell.d.ts +4 -2
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +36 -7
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/compaction.md +388 -0
- package/docs/custom-tools.md +146 -43
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +562 -596
- package/docs/rpc.md +33 -19
- package/docs/sdk.md +93 -21
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +172 -21
- package/docs/skills.md +2 -0
- package/docs/theme.md +31 -2
- package/docs/tree.md +197 -0
- package/docs/tui.md +343 -0
- package/examples/README.md +1 -9
- package/examples/custom-tools/hello/index.ts +4 -3
- package/examples/custom-tools/question/index.ts +4 -4
- package/examples/custom-tools/subagent/index.ts +7 -6
- package/examples/custom-tools/todo/index.ts +11 -5
- package/examples/hooks/README.md +29 -71
- package/examples/hooks/auto-commit-on-exit.ts +8 -9
- package/examples/hooks/confirm-destructive.ts +29 -30
- package/examples/hooks/custom-compaction.ts +20 -21
- package/examples/hooks/dirty-repo-guard.ts +41 -40
- package/examples/hooks/file-trigger.ts +10 -5
- package/examples/hooks/git-checkpoint.ts +16 -12
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +1 -1
- package/examples/hooks/protected-paths.ts +1 -1
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/03-custom-prompt.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +4 -4
- package/examples/sdk/06-hooks.ts +1 -1
- package/examples/sdk/07-context-files.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +6 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
- package/examples/sdk/10-settings.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/examples/sdk/12-full-control.ts +4 -7
- package/package.json +6 -6
- package/dist/core/compaction.d.ts.map +0 -1
- package/dist/core/compaction.js +0 -412
- package/dist/core/compaction.js.map +0 -1
- package/dist/core/export-html.d.ts +0 -23
- package/dist/core/export-html.d.ts.map +0 -1
- package/dist/core/export-html.js +0 -1185
- package/dist/core/export-html.js.map +0 -1
- package/dist/modes/interactive/components/compaction.d.ts +0 -15
- package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
- package/dist/modes/interactive/components/compaction.js +0 -41
- package/dist/modes/interactive/components/compaction.js.map +0 -1
- package/docs/hooks-v2.md +0 -385
- package/docs/session-tree.md +0 -452
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
|
16
|
-
import {
|
|
16
|
+
import { getAuthPath } from "../config.js";
|
|
17
17
|
import { executeBash as executeBashCommand } from "./bash-executor.js";
|
|
18
|
-
import { calculateContextTokens, compact, prepareCompaction, shouldCompact } from "./compaction.js";
|
|
19
|
-
import { exportSessionToHtml } from "./export-html.js";
|
|
18
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
19
|
+
import { exportSessionToHtml } from "./export-html/index.js";
|
|
20
20
|
import { expandSlashCommand } from "./slash-commands.js";
|
|
21
|
+
/** Internal marker for hook messages queued through the agent loop */
|
|
21
22
|
// ============================================================================
|
|
22
23
|
// Constants
|
|
23
24
|
// ============================================================================
|
|
@@ -40,18 +41,20 @@ export class AgentSession {
|
|
|
40
41
|
// Message queue state
|
|
41
42
|
_queuedMessages = [];
|
|
42
43
|
// Compaction state
|
|
43
|
-
_compactionAbortController =
|
|
44
|
-
_autoCompactionAbortController =
|
|
44
|
+
_compactionAbortController = undefined;
|
|
45
|
+
_autoCompactionAbortController = undefined;
|
|
46
|
+
// Branch summarization state
|
|
47
|
+
_branchSummaryAbortController = undefined;
|
|
45
48
|
// Retry state
|
|
46
|
-
_retryAbortController =
|
|
49
|
+
_retryAbortController = undefined;
|
|
47
50
|
_retryAttempt = 0;
|
|
48
|
-
_retryPromise =
|
|
49
|
-
_retryResolve =
|
|
51
|
+
_retryPromise = undefined;
|
|
52
|
+
_retryResolve = undefined;
|
|
50
53
|
// Bash execution state
|
|
51
|
-
_bashAbortController =
|
|
54
|
+
_bashAbortController = undefined;
|
|
52
55
|
_pendingBashMessages = [];
|
|
53
56
|
// Hook system
|
|
54
|
-
_hookRunner =
|
|
57
|
+
_hookRunner = undefined;
|
|
55
58
|
_turnIndex = 0;
|
|
56
59
|
// Custom tools for session lifecycle
|
|
57
60
|
_customTools = [];
|
|
@@ -64,10 +67,13 @@ export class AgentSession {
|
|
|
64
67
|
this.settingsManager = config.settingsManager;
|
|
65
68
|
this._scopedModels = config.scopedModels ?? [];
|
|
66
69
|
this._fileCommands = config.fileCommands ?? [];
|
|
67
|
-
this._hookRunner = config.hookRunner
|
|
70
|
+
this._hookRunner = config.hookRunner;
|
|
68
71
|
this._customTools = config.customTools ?? [];
|
|
69
72
|
this._skillsSettings = config.skillsSettings;
|
|
70
73
|
this._modelRegistry = config.modelRegistry;
|
|
74
|
+
// Always subscribe to agent events for internal handling
|
|
75
|
+
// (session persistence, hooks, auto-compaction, retry logic)
|
|
76
|
+
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
71
77
|
}
|
|
72
78
|
/** Model registry for API key resolution and model discovery */
|
|
73
79
|
get modelRegistry() {
|
|
@@ -83,7 +89,7 @@ export class AgentSession {
|
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
// Track last assistant message for auto-compaction check
|
|
86
|
-
_lastAssistantMessage =
|
|
92
|
+
_lastAssistantMessage = undefined;
|
|
87
93
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
88
94
|
_handleAgentEvent = async (event) => {
|
|
89
95
|
// When a user message starts, check if it's from the queue and remove it BEFORE emitting
|
|
@@ -105,7 +111,18 @@ export class AgentSession {
|
|
|
105
111
|
this._emit(event);
|
|
106
112
|
// Handle session persistence
|
|
107
113
|
if (event.type === "message_end") {
|
|
108
|
-
this
|
|
114
|
+
// Check if this is a hook message
|
|
115
|
+
if (event.message.role === "hookMessage") {
|
|
116
|
+
// Persist as CustomMessageEntry
|
|
117
|
+
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
118
|
+
}
|
|
119
|
+
else if (event.message.role === "user" ||
|
|
120
|
+
event.message.role === "assistant" ||
|
|
121
|
+
event.message.role === "toolResult") {
|
|
122
|
+
// Regular LLM message - persist as SessionMessageEntry
|
|
123
|
+
this.sessionManager.appendMessage(event.message);
|
|
124
|
+
}
|
|
125
|
+
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
109
126
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
110
127
|
if (event.message.role === "assistant") {
|
|
111
128
|
this._lastAssistantMessage = event.message;
|
|
@@ -114,7 +131,7 @@ export class AgentSession {
|
|
|
114
131
|
// Check auto-retry and auto-compaction after agent completes
|
|
115
132
|
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
|
116
133
|
const msg = this._lastAssistantMessage;
|
|
117
|
-
this._lastAssistantMessage =
|
|
134
|
+
this._lastAssistantMessage = undefined;
|
|
118
135
|
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
119
136
|
if (this._isRetryableError(msg)) {
|
|
120
137
|
const didRetry = await this._handleRetryableError(msg);
|
|
@@ -139,8 +156,8 @@ export class AgentSession {
|
|
|
139
156
|
_resolveRetry() {
|
|
140
157
|
if (this._retryResolve) {
|
|
141
158
|
this._retryResolve();
|
|
142
|
-
this._retryResolve =
|
|
143
|
-
this._retryPromise =
|
|
159
|
+
this._retryResolve = undefined;
|
|
160
|
+
this._retryPromise = undefined;
|
|
144
161
|
}
|
|
145
162
|
}
|
|
146
163
|
/** Extract text content from a message */
|
|
@@ -162,7 +179,7 @@ export class AgentSession {
|
|
|
162
179
|
return msg;
|
|
163
180
|
}
|
|
164
181
|
}
|
|
165
|
-
return
|
|
182
|
+
return undefined;
|
|
166
183
|
}
|
|
167
184
|
/** Emit hook events based on agent events */
|
|
168
185
|
async _emitHookEvent(event) {
|
|
@@ -201,10 +218,6 @@ export class AgentSession {
|
|
|
201
218
|
*/
|
|
202
219
|
subscribe(listener) {
|
|
203
220
|
this._eventListeners.push(listener);
|
|
204
|
-
// Set up agent subscription if not already done
|
|
205
|
-
if (!this._unsubscribeAgent) {
|
|
206
|
-
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
207
|
-
}
|
|
208
221
|
// Return unsubscribe function for this specific listener
|
|
209
222
|
return () => {
|
|
210
223
|
const index = this._eventListeners.indexOf(listener);
|
|
@@ -248,7 +261,7 @@ export class AgentSession {
|
|
|
248
261
|
get state() {
|
|
249
262
|
return this.agent.state;
|
|
250
263
|
}
|
|
251
|
-
/** Current model (may be
|
|
264
|
+
/** Current model (may be undefined if not yet selected) */
|
|
252
265
|
get model() {
|
|
253
266
|
return this.agent.state.model;
|
|
254
267
|
}
|
|
@@ -262,7 +275,7 @@ export class AgentSession {
|
|
|
262
275
|
}
|
|
263
276
|
/** Whether auto-compaction is currently running */
|
|
264
277
|
get isCompacting() {
|
|
265
|
-
return this._autoCompactionAbortController !==
|
|
278
|
+
return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;
|
|
266
279
|
}
|
|
267
280
|
/** All messages including custom types like BashExecutionMessage */
|
|
268
281
|
get messages() {
|
|
@@ -272,9 +285,9 @@ export class AgentSession {
|
|
|
272
285
|
get queueMode() {
|
|
273
286
|
return this.agent.getQueueMode();
|
|
274
287
|
}
|
|
275
|
-
/** Current session file path, or
|
|
288
|
+
/** Current session file path, or undefined if sessions are disabled */
|
|
276
289
|
get sessionFile() {
|
|
277
|
-
return this.sessionManager.
|
|
290
|
+
return this.sessionManager.getSessionFile();
|
|
278
291
|
}
|
|
279
292
|
/** Current session ID */
|
|
280
293
|
get sessionId() {
|
|
@@ -294,6 +307,7 @@ export class AgentSession {
|
|
|
294
307
|
/**
|
|
295
308
|
* Send a prompt to the agent.
|
|
296
309
|
* - Validates model and API key before sending
|
|
310
|
+
* - Handles hook commands (registered via pi.registerCommand)
|
|
297
311
|
* - Expands file-based slash commands by default
|
|
298
312
|
* @throws Error if no model selected or no API key available
|
|
299
313
|
*/
|
|
@@ -301,28 +315,91 @@ export class AgentSession {
|
|
|
301
315
|
// Flush any pending bash messages before the new prompt
|
|
302
316
|
this._flushPendingBashMessages();
|
|
303
317
|
const expandCommands = options?.expandSlashCommands ?? true;
|
|
318
|
+
// Handle hook commands first (if enabled and text is a slash command)
|
|
319
|
+
if (expandCommands && text.startsWith("/")) {
|
|
320
|
+
const handled = await this._tryExecuteHookCommand(text);
|
|
321
|
+
if (handled) {
|
|
322
|
+
// Hook command executed, no prompt to send
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
304
326
|
// Validate model
|
|
305
327
|
if (!this.model) {
|
|
306
328
|
throw new Error("No model selected.\n\n" +
|
|
307
|
-
`Use /login, set an API key environment variable, or create ${
|
|
329
|
+
`Use /login, set an API key environment variable, or create ${getAuthPath()}\n\n` +
|
|
308
330
|
"Then use /model to select a model.");
|
|
309
331
|
}
|
|
310
332
|
// Validate API key
|
|
311
333
|
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
312
334
|
if (!apiKey) {
|
|
313
335
|
throw new Error(`No API key found for ${this.model.provider}.\n\n` +
|
|
314
|
-
`Use /login, set an API key environment variable, or create ${
|
|
336
|
+
`Use /login, set an API key environment variable, or create ${getAuthPath()}`);
|
|
315
337
|
}
|
|
316
338
|
// Check if we need to compact before sending (catches aborted responses)
|
|
317
339
|
const lastAssistant = this._findLastAssistantMessage();
|
|
318
340
|
if (lastAssistant) {
|
|
319
341
|
await this._checkCompaction(lastAssistant, false);
|
|
320
342
|
}
|
|
321
|
-
// Expand slash commands if requested
|
|
343
|
+
// Expand file-based slash commands if requested
|
|
322
344
|
const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text;
|
|
323
|
-
|
|
345
|
+
// Build messages array (hook message if any, then user message)
|
|
346
|
+
const messages = [];
|
|
347
|
+
// Add user message
|
|
348
|
+
const userContent = [{ type: "text", text: expandedText }];
|
|
349
|
+
if (options?.images) {
|
|
350
|
+
userContent.push(...options.images);
|
|
351
|
+
}
|
|
352
|
+
messages.push({
|
|
353
|
+
role: "user",
|
|
354
|
+
content: userContent,
|
|
355
|
+
timestamp: Date.now(),
|
|
356
|
+
});
|
|
357
|
+
// Emit before_agent_start hook event
|
|
358
|
+
if (this._hookRunner) {
|
|
359
|
+
const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
|
|
360
|
+
if (result?.message) {
|
|
361
|
+
messages.push({
|
|
362
|
+
role: "hookMessage",
|
|
363
|
+
customType: result.message.customType,
|
|
364
|
+
content: result.message.content,
|
|
365
|
+
display: result.message.display,
|
|
366
|
+
details: result.message.details,
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
await this.agent.prompt(messages);
|
|
324
372
|
await this.waitForRetry();
|
|
325
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Try to execute a hook command. Returns true if command was found and executed.
|
|
376
|
+
*/
|
|
377
|
+
async _tryExecuteHookCommand(text) {
|
|
378
|
+
if (!this._hookRunner)
|
|
379
|
+
return false;
|
|
380
|
+
// Parse command name and args
|
|
381
|
+
const spaceIndex = text.indexOf(" ");
|
|
382
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
383
|
+
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
|
|
384
|
+
const command = this._hookRunner.getCommand(commandName);
|
|
385
|
+
if (!command)
|
|
386
|
+
return false;
|
|
387
|
+
// Get command context from hook runner (includes session control methods)
|
|
388
|
+
const ctx = this._hookRunner.createCommandContext();
|
|
389
|
+
try {
|
|
390
|
+
await command.handler(args, ctx);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
// Emit error via hook runner
|
|
395
|
+
this._hookRunner.emitError({
|
|
396
|
+
hookPath: `command:${commandName}`,
|
|
397
|
+
event: "command",
|
|
398
|
+
error: err instanceof Error ? err.message : String(err),
|
|
399
|
+
});
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
326
403
|
/**
|
|
327
404
|
* Queue a message to be sent after the current response completes.
|
|
328
405
|
* Use when agent is currently streaming.
|
|
@@ -335,6 +412,40 @@ export class AgentSession {
|
|
|
335
412
|
timestamp: Date.now(),
|
|
336
413
|
});
|
|
337
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Send a hook message to the session. Creates a CustomMessageEntry.
|
|
417
|
+
*
|
|
418
|
+
* Handles three cases:
|
|
419
|
+
* - Streaming: queues message, processed when loop pulls from queue
|
|
420
|
+
* - Not streaming + triggerTurn: appends to state/session, starts new turn
|
|
421
|
+
* - Not streaming + no trigger: appends to state/session, no turn
|
|
422
|
+
*
|
|
423
|
+
* @param message Hook message with customType, content, display, details
|
|
424
|
+
* @param triggerTurn If true and not streaming, triggers a new LLM turn
|
|
425
|
+
*/
|
|
426
|
+
async sendHookMessage(message, triggerTurn) {
|
|
427
|
+
const appMessage = {
|
|
428
|
+
role: "hookMessage",
|
|
429
|
+
customType: message.customType,
|
|
430
|
+
content: message.content,
|
|
431
|
+
display: message.display,
|
|
432
|
+
details: message.details,
|
|
433
|
+
timestamp: Date.now(),
|
|
434
|
+
};
|
|
435
|
+
if (this.isStreaming) {
|
|
436
|
+
// Queue for processing by agent loop
|
|
437
|
+
await this.agent.queueMessage(appMessage);
|
|
438
|
+
}
|
|
439
|
+
else if (triggerTurn) {
|
|
440
|
+
// Send as prompt - agent loop will emit message events
|
|
441
|
+
await this.agent.prompt(appMessage);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
// Just append to agent state and session, no turn
|
|
445
|
+
this.agent.appendMessage(appMessage);
|
|
446
|
+
this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
338
449
|
/**
|
|
339
450
|
* Clear queued messages and return them.
|
|
340
451
|
* Useful for restoring to editor when user aborts.
|
|
@@ -365,22 +476,19 @@ export class AgentSession {
|
|
|
365
476
|
await this.agent.waitForIdle();
|
|
366
477
|
}
|
|
367
478
|
/**
|
|
368
|
-
*
|
|
479
|
+
* Start a new session, optionally with initial messages and parent tracking.
|
|
369
480
|
* Clears all messages and starts a new session.
|
|
370
481
|
* Listeners are preserved and will continue receiving events.
|
|
371
|
-
* @
|
|
482
|
+
* @param options - Optional initial messages and parent session path
|
|
483
|
+
* @returns true if completed, false if cancelled by hook
|
|
372
484
|
*/
|
|
373
|
-
async
|
|
485
|
+
async newSession(options) {
|
|
374
486
|
const previousSessionFile = this.sessionFile;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (this._hookRunner?.hasHandlers("session")) {
|
|
487
|
+
// Emit session_before_switch event with reason "new" (can be cancelled)
|
|
488
|
+
if (this._hookRunner?.hasHandlers("session_before_switch")) {
|
|
378
489
|
const result = (await this._hookRunner.emit({
|
|
379
|
-
type: "
|
|
380
|
-
|
|
381
|
-
sessionFile: this.sessionFile,
|
|
382
|
-
previousSessionFile: null,
|
|
383
|
-
reason: "before_new",
|
|
490
|
+
type: "session_before_switch",
|
|
491
|
+
reason: "new",
|
|
384
492
|
}));
|
|
385
493
|
if (result?.cancel) {
|
|
386
494
|
return false;
|
|
@@ -389,22 +497,19 @@ export class AgentSession {
|
|
|
389
497
|
this._disconnectFromAgent();
|
|
390
498
|
await this.abort();
|
|
391
499
|
this.agent.reset();
|
|
392
|
-
this.sessionManager.
|
|
500
|
+
this.sessionManager.newSession(options);
|
|
393
501
|
this._queuedMessages = [];
|
|
394
502
|
this._reconnectToAgent();
|
|
395
|
-
// Emit
|
|
503
|
+
// Emit session_switch event with reason "new" to hooks
|
|
396
504
|
if (this._hookRunner) {
|
|
397
|
-
this._hookRunner.setSessionFile(this.sessionFile);
|
|
398
505
|
await this._hookRunner.emit({
|
|
399
|
-
type: "
|
|
400
|
-
entries: [],
|
|
401
|
-
sessionFile: this.sessionFile,
|
|
402
|
-
previousSessionFile,
|
|
506
|
+
type: "session_switch",
|
|
403
507
|
reason: "new",
|
|
508
|
+
previousSessionFile,
|
|
404
509
|
});
|
|
405
510
|
}
|
|
406
511
|
// Emit session event to custom tools
|
|
407
|
-
await this.
|
|
512
|
+
await this.emitCustomToolSessionEvent("switch", previousSessionFile);
|
|
408
513
|
return true;
|
|
409
514
|
}
|
|
410
515
|
// =========================================================================
|
|
@@ -421,7 +526,7 @@ export class AgentSession {
|
|
|
421
526
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
422
527
|
}
|
|
423
528
|
this.agent.setModel(model);
|
|
424
|
-
this.sessionManager.
|
|
529
|
+
this.sessionManager.appendModelChange(model.provider, model.id);
|
|
425
530
|
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
426
531
|
// Re-clamp thinking level for new model's capabilities
|
|
427
532
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -430,7 +535,7 @@ export class AgentSession {
|
|
|
430
535
|
* Cycle to next/previous model.
|
|
431
536
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
432
537
|
* @param direction - "forward" (default) or "backward"
|
|
433
|
-
* @returns The new model info, or
|
|
538
|
+
* @returns The new model info, or undefined if only one model available
|
|
434
539
|
*/
|
|
435
540
|
async cycleModel(direction = "forward") {
|
|
436
541
|
if (this._scopedModels.length > 0) {
|
|
@@ -440,7 +545,7 @@ export class AgentSession {
|
|
|
440
545
|
}
|
|
441
546
|
async _cycleScopedModel(direction) {
|
|
442
547
|
if (this._scopedModels.length <= 1)
|
|
443
|
-
return
|
|
548
|
+
return undefined;
|
|
444
549
|
const currentModel = this.model;
|
|
445
550
|
let currentIndex = this._scopedModels.findIndex((sm) => modelsAreEqual(sm.model, currentModel));
|
|
446
551
|
if (currentIndex === -1)
|
|
@@ -455,7 +560,7 @@ export class AgentSession {
|
|
|
455
560
|
}
|
|
456
561
|
// Apply model
|
|
457
562
|
this.agent.setModel(next.model);
|
|
458
|
-
this.sessionManager.
|
|
563
|
+
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
459
564
|
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
|
|
460
565
|
// Apply thinking level (setThinkingLevel clamps to model capabilities)
|
|
461
566
|
this.setThinkingLevel(next.thinkingLevel);
|
|
@@ -464,7 +569,7 @@ export class AgentSession {
|
|
|
464
569
|
async _cycleAvailableModel(direction) {
|
|
465
570
|
const availableModels = await this._modelRegistry.getAvailable();
|
|
466
571
|
if (availableModels.length <= 1)
|
|
467
|
-
return
|
|
572
|
+
return undefined;
|
|
468
573
|
const currentModel = this.model;
|
|
469
574
|
let currentIndex = availableModels.findIndex((m) => modelsAreEqual(m, currentModel));
|
|
470
575
|
if (currentIndex === -1)
|
|
@@ -477,7 +582,7 @@ export class AgentSession {
|
|
|
477
582
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
478
583
|
}
|
|
479
584
|
this.agent.setModel(nextModel);
|
|
480
|
-
this.sessionManager.
|
|
585
|
+
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
|
481
586
|
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
482
587
|
// Re-clamp thinking level for new model's capabilities
|
|
483
588
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -506,16 +611,16 @@ export class AgentSession {
|
|
|
506
611
|
effectiveLevel = "high";
|
|
507
612
|
}
|
|
508
613
|
this.agent.setThinkingLevel(effectiveLevel);
|
|
509
|
-
this.sessionManager.
|
|
614
|
+
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
510
615
|
this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
|
|
511
616
|
}
|
|
512
617
|
/**
|
|
513
618
|
* Cycle to next thinking level.
|
|
514
|
-
* @returns New level, or
|
|
619
|
+
* @returns New level, or undefined if model doesn't support thinking
|
|
515
620
|
*/
|
|
516
621
|
cycleThinkingLevel() {
|
|
517
622
|
if (!this.supportsThinking())
|
|
518
|
-
return
|
|
623
|
+
return undefined;
|
|
519
624
|
const levels = this.getAvailableThinkingLevels();
|
|
520
625
|
const currentIndex = levels.indexOf(this.thinkingLevel);
|
|
521
626
|
const nextIndex = (currentIndex + 1) % levels.length;
|
|
@@ -572,76 +677,79 @@ export class AgentSession {
|
|
|
572
677
|
if (!apiKey) {
|
|
573
678
|
throw new Error(`No API key for ${this.model.provider}`);
|
|
574
679
|
}
|
|
575
|
-
const
|
|
680
|
+
const pathEntries = this.sessionManager.getBranch();
|
|
576
681
|
const settings = this.settingsManager.getCompactionSettings();
|
|
577
|
-
const preparation = prepareCompaction(
|
|
682
|
+
const preparation = prepareCompaction(pathEntries, settings);
|
|
578
683
|
if (!preparation) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
584
|
-
if (entries[i].type === "compaction") {
|
|
585
|
-
previousSummary = entries[i].summary;
|
|
586
|
-
break;
|
|
684
|
+
// Check why we can't compact
|
|
685
|
+
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
686
|
+
if (lastEntry?.type === "compaction") {
|
|
687
|
+
throw new Error("Already compacted");
|
|
587
688
|
}
|
|
689
|
+
throw new Error("Nothing to compact (session too small)");
|
|
588
690
|
}
|
|
589
|
-
let
|
|
691
|
+
let hookCompaction;
|
|
590
692
|
let fromHook = false;
|
|
591
|
-
if (this._hookRunner?.hasHandlers("
|
|
693
|
+
if (this._hookRunner?.hasHandlers("session_before_compact")) {
|
|
592
694
|
const result = (await this._hookRunner.emit({
|
|
593
|
-
type: "
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
previousSessionFile: null,
|
|
597
|
-
reason: "before_compact",
|
|
598
|
-
cutPoint: preparation.cutPoint,
|
|
599
|
-
previousSummary,
|
|
600
|
-
messagesToSummarize: [...preparation.messagesToSummarize],
|
|
601
|
-
messagesToKeep: [...preparation.messagesToKeep],
|
|
602
|
-
tokensBefore: preparation.tokensBefore,
|
|
695
|
+
type: "session_before_compact",
|
|
696
|
+
preparation,
|
|
697
|
+
branchEntries: pathEntries,
|
|
603
698
|
customInstructions,
|
|
604
|
-
model: this.model,
|
|
605
|
-
resolveApiKey: async (m) => (await this._modelRegistry.getApiKey(m)) ?? undefined,
|
|
606
699
|
signal: this._compactionAbortController.signal,
|
|
607
700
|
}));
|
|
608
701
|
if (result?.cancel) {
|
|
609
702
|
throw new Error("Compaction cancelled");
|
|
610
703
|
}
|
|
611
|
-
if (result?.
|
|
612
|
-
|
|
704
|
+
if (result?.compaction) {
|
|
705
|
+
hookCompaction = result.compaction;
|
|
613
706
|
fromHook = true;
|
|
614
707
|
}
|
|
615
708
|
}
|
|
616
|
-
|
|
617
|
-
|
|
709
|
+
let summary;
|
|
710
|
+
let firstKeptEntryId;
|
|
711
|
+
let tokensBefore;
|
|
712
|
+
let details;
|
|
713
|
+
if (hookCompaction) {
|
|
714
|
+
// Hook provided compaction content
|
|
715
|
+
summary = hookCompaction.summary;
|
|
716
|
+
firstKeptEntryId = hookCompaction.firstKeptEntryId;
|
|
717
|
+
tokensBefore = hookCompaction.tokensBefore;
|
|
718
|
+
details = hookCompaction.details;
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
// Generate compaction result
|
|
722
|
+
const result = await compact(preparation, this.model, apiKey, customInstructions, this._compactionAbortController.signal);
|
|
723
|
+
summary = result.summary;
|
|
724
|
+
firstKeptEntryId = result.firstKeptEntryId;
|
|
725
|
+
tokensBefore = result.tokensBefore;
|
|
726
|
+
details = result.details;
|
|
618
727
|
}
|
|
619
728
|
if (this._compactionAbortController.signal.aborted) {
|
|
620
729
|
throw new Error("Compaction cancelled");
|
|
621
730
|
}
|
|
622
|
-
this.sessionManager.
|
|
731
|
+
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook);
|
|
623
732
|
const newEntries = this.sessionManager.getEntries();
|
|
624
733
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
625
734
|
this.agent.replaceMessages(sessionContext.messages);
|
|
626
|
-
|
|
735
|
+
// Get the saved compaction entry for the hook
|
|
736
|
+
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
737
|
+
if (this._hookRunner && savedCompactionEntry) {
|
|
627
738
|
await this._hookRunner.emit({
|
|
628
|
-
type: "
|
|
629
|
-
|
|
630
|
-
sessionFile: this.sessionFile,
|
|
631
|
-
previousSessionFile: null,
|
|
632
|
-
reason: "compact",
|
|
633
|
-
compactionEntry,
|
|
634
|
-
tokensBefore: compactionEntry.tokensBefore,
|
|
739
|
+
type: "session_compact",
|
|
740
|
+
compactionEntry: savedCompactionEntry,
|
|
635
741
|
fromHook,
|
|
636
742
|
});
|
|
637
743
|
}
|
|
638
744
|
return {
|
|
639
|
-
|
|
640
|
-
|
|
745
|
+
summary,
|
|
746
|
+
firstKeptEntryId,
|
|
747
|
+
tokensBefore,
|
|
748
|
+
details,
|
|
641
749
|
};
|
|
642
750
|
}
|
|
643
751
|
finally {
|
|
644
|
-
this._compactionAbortController =
|
|
752
|
+
this._compactionAbortController = undefined;
|
|
645
753
|
this._reconnectToAgent();
|
|
646
754
|
}
|
|
647
755
|
}
|
|
@@ -652,6 +760,12 @@ export class AgentSession {
|
|
|
652
760
|
this._compactionAbortController?.abort();
|
|
653
761
|
this._autoCompactionAbortController?.abort();
|
|
654
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Cancel in-progress branch summarization.
|
|
765
|
+
*/
|
|
766
|
+
abortBranchSummary() {
|
|
767
|
+
this._branchSummaryAbortController?.abort();
|
|
768
|
+
}
|
|
655
769
|
/**
|
|
656
770
|
* Check if compaction is needed and run it.
|
|
657
771
|
* Called after agent_end and before prompt submission.
|
|
@@ -700,82 +814,80 @@ export class AgentSession {
|
|
|
700
814
|
this._autoCompactionAbortController = new AbortController();
|
|
701
815
|
try {
|
|
702
816
|
if (!this.model) {
|
|
703
|
-
this._emit({ type: "auto_compaction_end", result:
|
|
817
|
+
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
|
704
818
|
return;
|
|
705
819
|
}
|
|
706
820
|
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
707
821
|
if (!apiKey) {
|
|
708
|
-
this._emit({ type: "auto_compaction_end", result:
|
|
822
|
+
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
|
709
823
|
return;
|
|
710
824
|
}
|
|
711
|
-
const
|
|
712
|
-
const preparation = prepareCompaction(
|
|
825
|
+
const pathEntries = this.sessionManager.getBranch();
|
|
826
|
+
const preparation = prepareCompaction(pathEntries, settings);
|
|
713
827
|
if (!preparation) {
|
|
714
|
-
this._emit({ type: "auto_compaction_end", result:
|
|
828
|
+
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
|
715
829
|
return;
|
|
716
830
|
}
|
|
717
|
-
|
|
718
|
-
let previousSummary;
|
|
719
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
720
|
-
if (entries[i].type === "compaction") {
|
|
721
|
-
previousSummary = entries[i].summary;
|
|
722
|
-
break;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
let compactionEntry;
|
|
831
|
+
let hookCompaction;
|
|
726
832
|
let fromHook = false;
|
|
727
|
-
if (this._hookRunner?.hasHandlers("
|
|
833
|
+
if (this._hookRunner?.hasHandlers("session_before_compact")) {
|
|
728
834
|
const hookResult = (await this._hookRunner.emit({
|
|
729
|
-
type: "
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
previousSessionFile: null,
|
|
733
|
-
reason: "before_compact",
|
|
734
|
-
cutPoint: preparation.cutPoint,
|
|
735
|
-
previousSummary,
|
|
736
|
-
messagesToSummarize: [...preparation.messagesToSummarize],
|
|
737
|
-
messagesToKeep: [...preparation.messagesToKeep],
|
|
738
|
-
tokensBefore: preparation.tokensBefore,
|
|
835
|
+
type: "session_before_compact",
|
|
836
|
+
preparation,
|
|
837
|
+
branchEntries: pathEntries,
|
|
739
838
|
customInstructions: undefined,
|
|
740
|
-
model: this.model,
|
|
741
|
-
resolveApiKey: async (m) => (await this._modelRegistry.getApiKey(m)) ?? undefined,
|
|
742
839
|
signal: this._autoCompactionAbortController.signal,
|
|
743
840
|
}));
|
|
744
841
|
if (hookResult?.cancel) {
|
|
745
|
-
this._emit({ type: "auto_compaction_end", result:
|
|
842
|
+
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
|
746
843
|
return;
|
|
747
844
|
}
|
|
748
|
-
if (hookResult?.
|
|
749
|
-
|
|
845
|
+
if (hookResult?.compaction) {
|
|
846
|
+
hookCompaction = hookResult.compaction;
|
|
750
847
|
fromHook = true;
|
|
751
848
|
}
|
|
752
849
|
}
|
|
753
|
-
|
|
754
|
-
|
|
850
|
+
let summary;
|
|
851
|
+
let firstKeptEntryId;
|
|
852
|
+
let tokensBefore;
|
|
853
|
+
let details;
|
|
854
|
+
if (hookCompaction) {
|
|
855
|
+
// Hook provided compaction content
|
|
856
|
+
summary = hookCompaction.summary;
|
|
857
|
+
firstKeptEntryId = hookCompaction.firstKeptEntryId;
|
|
858
|
+
tokensBefore = hookCompaction.tokensBefore;
|
|
859
|
+
details = hookCompaction.details;
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
// Generate compaction result
|
|
863
|
+
const compactResult = await compact(preparation, this.model, apiKey, undefined, this._autoCompactionAbortController.signal);
|
|
864
|
+
summary = compactResult.summary;
|
|
865
|
+
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
866
|
+
tokensBefore = compactResult.tokensBefore;
|
|
867
|
+
details = compactResult.details;
|
|
755
868
|
}
|
|
756
869
|
if (this._autoCompactionAbortController.signal.aborted) {
|
|
757
|
-
this._emit({ type: "auto_compaction_end", result:
|
|
870
|
+
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
|
758
871
|
return;
|
|
759
872
|
}
|
|
760
|
-
this.sessionManager.
|
|
873
|
+
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook);
|
|
761
874
|
const newEntries = this.sessionManager.getEntries();
|
|
762
875
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
763
876
|
this.agent.replaceMessages(sessionContext.messages);
|
|
764
|
-
|
|
877
|
+
// Get the saved compaction entry for the hook
|
|
878
|
+
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
879
|
+
if (this._hookRunner && savedCompactionEntry) {
|
|
765
880
|
await this._hookRunner.emit({
|
|
766
|
-
type: "
|
|
767
|
-
|
|
768
|
-
sessionFile: this.sessionFile,
|
|
769
|
-
previousSessionFile: null,
|
|
770
|
-
reason: "compact",
|
|
771
|
-
compactionEntry,
|
|
772
|
-
tokensBefore: compactionEntry.tokensBefore,
|
|
881
|
+
type: "session_compact",
|
|
882
|
+
compactionEntry: savedCompactionEntry,
|
|
773
883
|
fromHook,
|
|
774
884
|
});
|
|
775
885
|
}
|
|
776
886
|
const result = {
|
|
777
|
-
|
|
778
|
-
|
|
887
|
+
summary,
|
|
888
|
+
firstKeptEntryId,
|
|
889
|
+
tokensBefore,
|
|
890
|
+
details,
|
|
779
891
|
};
|
|
780
892
|
this._emit({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
|
781
893
|
if (willRetry) {
|
|
@@ -790,13 +902,13 @@ export class AgentSession {
|
|
|
790
902
|
}
|
|
791
903
|
}
|
|
792
904
|
catch (error) {
|
|
793
|
-
this._emit({ type: "auto_compaction_end", result:
|
|
905
|
+
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
|
794
906
|
if (reason === "overflow") {
|
|
795
907
|
throw new Error(`Context overflow: ${error instanceof Error ? error.message : "compaction failed"}. Your input may be too large for the context window.`);
|
|
796
908
|
}
|
|
797
909
|
}
|
|
798
910
|
finally {
|
|
799
|
-
this._autoCompactionAbortController =
|
|
911
|
+
this._autoCompactionAbortController = undefined;
|
|
800
912
|
}
|
|
801
913
|
}
|
|
802
914
|
/**
|
|
@@ -876,7 +988,7 @@ export class AgentSession {
|
|
|
876
988
|
// Aborted during sleep - emit end event so UI can clean up
|
|
877
989
|
const attempt = this._retryAttempt;
|
|
878
990
|
this._retryAttempt = 0;
|
|
879
|
-
this._retryAbortController =
|
|
991
|
+
this._retryAbortController = undefined;
|
|
880
992
|
this._emit({
|
|
881
993
|
type: "auto_retry_end",
|
|
882
994
|
success: false,
|
|
@@ -886,7 +998,7 @@ export class AgentSession {
|
|
|
886
998
|
this._resolveRetry();
|
|
887
999
|
return false;
|
|
888
1000
|
}
|
|
889
|
-
this._retryAbortController =
|
|
1001
|
+
this._retryAbortController = undefined;
|
|
890
1002
|
// Retry via continue() - use setTimeout to break out of event handler chain
|
|
891
1003
|
setTimeout(() => {
|
|
892
1004
|
this.agent.continue().catch(() => {
|
|
@@ -930,7 +1042,7 @@ export class AgentSession {
|
|
|
930
1042
|
}
|
|
931
1043
|
/** Whether auto-retry is currently in progress */
|
|
932
1044
|
get isRetrying() {
|
|
933
|
-
return this._retryPromise !==
|
|
1045
|
+
return this._retryPromise !== undefined;
|
|
934
1046
|
}
|
|
935
1047
|
/** Whether auto-retry is enabled */
|
|
936
1048
|
get autoRetryEnabled() {
|
|
@@ -978,12 +1090,12 @@ export class AgentSession {
|
|
|
978
1090
|
// Add to agent state immediately
|
|
979
1091
|
this.agent.appendMessage(bashMessage);
|
|
980
1092
|
// Save to session
|
|
981
|
-
this.sessionManager.
|
|
1093
|
+
this.sessionManager.appendMessage(bashMessage);
|
|
982
1094
|
}
|
|
983
1095
|
return result;
|
|
984
1096
|
}
|
|
985
1097
|
finally {
|
|
986
|
-
this._bashAbortController =
|
|
1098
|
+
this._bashAbortController = undefined;
|
|
987
1099
|
}
|
|
988
1100
|
}
|
|
989
1101
|
/**
|
|
@@ -994,7 +1106,7 @@ export class AgentSession {
|
|
|
994
1106
|
}
|
|
995
1107
|
/** Whether a bash command is currently running */
|
|
996
1108
|
get isBashRunning() {
|
|
997
|
-
return this._bashAbortController !==
|
|
1109
|
+
return this._bashAbortController !== undefined;
|
|
998
1110
|
}
|
|
999
1111
|
/** Whether there are pending bash messages waiting to be flushed */
|
|
1000
1112
|
get hasPendingBashMessages() {
|
|
@@ -1011,7 +1123,7 @@ export class AgentSession {
|
|
|
1011
1123
|
// Add to agent state
|
|
1012
1124
|
this.agent.appendMessage(bashMessage);
|
|
1013
1125
|
// Save to session
|
|
1014
|
-
this.sessionManager.
|
|
1126
|
+
this.sessionManager.appendMessage(bashMessage);
|
|
1015
1127
|
}
|
|
1016
1128
|
this._pendingBashMessages = [];
|
|
1017
1129
|
}
|
|
@@ -1025,16 +1137,13 @@ export class AgentSession {
|
|
|
1025
1137
|
* @returns true if switch completed, false if cancelled by hook
|
|
1026
1138
|
*/
|
|
1027
1139
|
async switchSession(sessionPath) {
|
|
1028
|
-
const previousSessionFile = this.
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
if (this._hookRunner?.hasHandlers("session")) {
|
|
1140
|
+
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
1141
|
+
// Emit session_before_switch event (can be cancelled)
|
|
1142
|
+
if (this._hookRunner?.hasHandlers("session_before_switch")) {
|
|
1032
1143
|
const result = (await this._hookRunner.emit({
|
|
1033
|
-
type: "
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
previousSessionFile: null,
|
|
1037
|
-
reason: "before_switch",
|
|
1144
|
+
type: "session_before_switch",
|
|
1145
|
+
reason: "resume",
|
|
1146
|
+
targetSessionFile: sessionPath,
|
|
1038
1147
|
}));
|
|
1039
1148
|
if (result?.cancel) {
|
|
1040
1149
|
return false;
|
|
@@ -1046,21 +1155,17 @@ export class AgentSession {
|
|
|
1046
1155
|
// Set new session
|
|
1047
1156
|
this.sessionManager.setSessionFile(sessionPath);
|
|
1048
1157
|
// Reload messages
|
|
1049
|
-
const entries = this.sessionManager.getEntries();
|
|
1050
1158
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1051
|
-
// Emit
|
|
1159
|
+
// Emit session_switch event to hooks
|
|
1052
1160
|
if (this._hookRunner) {
|
|
1053
|
-
this._hookRunner.setSessionFile(sessionPath);
|
|
1054
1161
|
await this._hookRunner.emit({
|
|
1055
|
-
type: "
|
|
1056
|
-
|
|
1057
|
-
sessionFile: sessionPath,
|
|
1162
|
+
type: "session_switch",
|
|
1163
|
+
reason: "resume",
|
|
1058
1164
|
previousSessionFile,
|
|
1059
|
-
reason: "switch",
|
|
1060
1165
|
});
|
|
1061
1166
|
}
|
|
1062
1167
|
// Emit session event to custom tools
|
|
1063
|
-
await this.
|
|
1168
|
+
await this.emitCustomToolSessionEvent("switch", previousSessionFile);
|
|
1064
1169
|
this.agent.replaceMessages(sessionContext.messages);
|
|
1065
1170
|
// Restore model if saved
|
|
1066
1171
|
if (sessionContext.model) {
|
|
@@ -1078,81 +1183,215 @@ export class AgentSession {
|
|
|
1078
1183
|
return true;
|
|
1079
1184
|
}
|
|
1080
1185
|
/**
|
|
1081
|
-
* Create a branch from a specific entry
|
|
1186
|
+
* Create a branch from a specific entry.
|
|
1082
1187
|
* Emits before_branch/branch session events to hooks.
|
|
1083
1188
|
*
|
|
1084
|
-
* @param
|
|
1189
|
+
* @param entryId ID of the entry to branch from
|
|
1085
1190
|
* @returns Object with:
|
|
1086
1191
|
* - selectedText: The text of the selected user message (for editor pre-fill)
|
|
1087
1192
|
* - cancelled: True if a hook cancelled the branch
|
|
1088
1193
|
*/
|
|
1089
|
-
async branch(
|
|
1194
|
+
async branch(entryId) {
|
|
1090
1195
|
const previousSessionFile = this.sessionFile;
|
|
1091
|
-
const
|
|
1092
|
-
const selectedEntry = entries[entryIndex];
|
|
1196
|
+
const selectedEntry = this.sessionManager.getEntry(entryId);
|
|
1093
1197
|
if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
|
|
1094
|
-
throw new Error("Invalid entry
|
|
1198
|
+
throw new Error("Invalid entry ID for branching");
|
|
1095
1199
|
}
|
|
1096
1200
|
const selectedText = this._extractUserMessageText(selectedEntry.message.content);
|
|
1097
1201
|
let skipConversationRestore = false;
|
|
1098
|
-
// Emit
|
|
1099
|
-
if (this._hookRunner?.hasHandlers("
|
|
1202
|
+
// Emit session_before_branch event (can be cancelled)
|
|
1203
|
+
if (this._hookRunner?.hasHandlers("session_before_branch")) {
|
|
1100
1204
|
const result = (await this._hookRunner.emit({
|
|
1101
|
-
type: "
|
|
1102
|
-
|
|
1103
|
-
sessionFile: this.sessionFile,
|
|
1104
|
-
previousSessionFile: null,
|
|
1105
|
-
reason: "before_branch",
|
|
1106
|
-
targetTurnIndex: entryIndex,
|
|
1205
|
+
type: "session_before_branch",
|
|
1206
|
+
entryId,
|
|
1107
1207
|
}));
|
|
1108
1208
|
if (result?.cancel) {
|
|
1109
1209
|
return { selectedText, cancelled: true };
|
|
1110
1210
|
}
|
|
1111
1211
|
skipConversationRestore = result?.skipConversationRestore ?? false;
|
|
1112
1212
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
this.sessionManager.
|
|
1213
|
+
if (!selectedEntry.parentId) {
|
|
1214
|
+
this.sessionManager.newSession();
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
1118
1218
|
}
|
|
1119
1219
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
1120
|
-
const newEntries = this.sessionManager.getEntries();
|
|
1121
1220
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1122
|
-
// Emit
|
|
1221
|
+
// Emit session_branch event to hooks (after branch completes)
|
|
1123
1222
|
if (this._hookRunner) {
|
|
1124
|
-
this._hookRunner.setSessionFile(newSessionFile);
|
|
1125
1223
|
await this._hookRunner.emit({
|
|
1126
|
-
type: "
|
|
1127
|
-
entries: newEntries,
|
|
1128
|
-
sessionFile: newSessionFile,
|
|
1224
|
+
type: "session_branch",
|
|
1129
1225
|
previousSessionFile,
|
|
1130
|
-
reason: "branch",
|
|
1131
|
-
targetTurnIndex: entryIndex,
|
|
1132
1226
|
});
|
|
1133
1227
|
}
|
|
1134
1228
|
// Emit session event to custom tools (with reason "branch")
|
|
1135
|
-
await this.
|
|
1229
|
+
await this.emitCustomToolSessionEvent("branch", previousSessionFile);
|
|
1136
1230
|
if (!skipConversationRestore) {
|
|
1137
1231
|
this.agent.replaceMessages(sessionContext.messages);
|
|
1138
1232
|
}
|
|
1139
1233
|
return { selectedText, cancelled: false };
|
|
1140
1234
|
}
|
|
1235
|
+
// =========================================================================
|
|
1236
|
+
// Tree Navigation
|
|
1237
|
+
// =========================================================================
|
|
1238
|
+
/**
|
|
1239
|
+
* Navigate to a different node in the session tree.
|
|
1240
|
+
* Unlike branch() which creates a new session file, this stays in the same file.
|
|
1241
|
+
*
|
|
1242
|
+
* @param targetId The entry ID to navigate to
|
|
1243
|
+
* @param options.summarize Whether user wants to summarize abandoned branch
|
|
1244
|
+
* @param options.customInstructions Custom instructions for summarizer
|
|
1245
|
+
* @returns Result with editorText (if user message) and cancelled status
|
|
1246
|
+
*/
|
|
1247
|
+
async navigateTree(targetId, options = {}) {
|
|
1248
|
+
const oldLeafId = this.sessionManager.getLeafId();
|
|
1249
|
+
// No-op if already at target
|
|
1250
|
+
if (targetId === oldLeafId) {
|
|
1251
|
+
return { cancelled: false };
|
|
1252
|
+
}
|
|
1253
|
+
// Model required for summarization
|
|
1254
|
+
if (options.summarize && !this.model) {
|
|
1255
|
+
throw new Error("No model available for summarization");
|
|
1256
|
+
}
|
|
1257
|
+
const targetEntry = this.sessionManager.getEntry(targetId);
|
|
1258
|
+
if (!targetEntry) {
|
|
1259
|
+
throw new Error(`Entry ${targetId} not found`);
|
|
1260
|
+
}
|
|
1261
|
+
// Collect entries to summarize (from old leaf to common ancestor)
|
|
1262
|
+
const { entries: entriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary(this.sessionManager, oldLeafId, targetId);
|
|
1263
|
+
// Prepare event data
|
|
1264
|
+
const preparation = {
|
|
1265
|
+
targetId,
|
|
1266
|
+
oldLeafId,
|
|
1267
|
+
commonAncestorId,
|
|
1268
|
+
entriesToSummarize,
|
|
1269
|
+
userWantsSummary: options.summarize ?? false,
|
|
1270
|
+
};
|
|
1271
|
+
// Set up abort controller for summarization
|
|
1272
|
+
this._branchSummaryAbortController = new AbortController();
|
|
1273
|
+
let hookSummary;
|
|
1274
|
+
let fromHook = false;
|
|
1275
|
+
// Emit session_before_tree event
|
|
1276
|
+
if (this._hookRunner?.hasHandlers("session_before_tree")) {
|
|
1277
|
+
const result = (await this._hookRunner.emit({
|
|
1278
|
+
type: "session_before_tree",
|
|
1279
|
+
preparation,
|
|
1280
|
+
signal: this._branchSummaryAbortController.signal,
|
|
1281
|
+
}));
|
|
1282
|
+
if (result?.cancel) {
|
|
1283
|
+
return { cancelled: true };
|
|
1284
|
+
}
|
|
1285
|
+
if (result?.summary && options.summarize) {
|
|
1286
|
+
hookSummary = result.summary;
|
|
1287
|
+
fromHook = true;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
// Run default summarizer if needed
|
|
1291
|
+
let summaryText;
|
|
1292
|
+
let summaryDetails;
|
|
1293
|
+
if (options.summarize && entriesToSummarize.length > 0 && !hookSummary) {
|
|
1294
|
+
const model = this.model;
|
|
1295
|
+
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
1296
|
+
if (!apiKey) {
|
|
1297
|
+
throw new Error(`No API key for ${model.provider}`);
|
|
1298
|
+
}
|
|
1299
|
+
const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
|
|
1300
|
+
const result = await generateBranchSummary(entriesToSummarize, {
|
|
1301
|
+
model,
|
|
1302
|
+
apiKey,
|
|
1303
|
+
signal: this._branchSummaryAbortController.signal,
|
|
1304
|
+
customInstructions: options.customInstructions,
|
|
1305
|
+
reserveTokens: branchSummarySettings.reserveTokens,
|
|
1306
|
+
});
|
|
1307
|
+
this._branchSummaryAbortController = undefined;
|
|
1308
|
+
if (result.aborted) {
|
|
1309
|
+
return { cancelled: true, aborted: true };
|
|
1310
|
+
}
|
|
1311
|
+
if (result.error) {
|
|
1312
|
+
throw new Error(result.error);
|
|
1313
|
+
}
|
|
1314
|
+
summaryText = result.summary;
|
|
1315
|
+
summaryDetails = {
|
|
1316
|
+
readFiles: result.readFiles || [],
|
|
1317
|
+
modifiedFiles: result.modifiedFiles || [],
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
else if (hookSummary) {
|
|
1321
|
+
summaryText = hookSummary.summary;
|
|
1322
|
+
summaryDetails = hookSummary.details;
|
|
1323
|
+
}
|
|
1324
|
+
// Determine the new leaf position based on target type
|
|
1325
|
+
let newLeafId;
|
|
1326
|
+
let editorText;
|
|
1327
|
+
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
1328
|
+
// User message: leaf = parent (null if root), text goes to editor
|
|
1329
|
+
newLeafId = targetEntry.parentId;
|
|
1330
|
+
editorText = this._extractUserMessageText(targetEntry.message.content);
|
|
1331
|
+
}
|
|
1332
|
+
else if (targetEntry.type === "custom_message") {
|
|
1333
|
+
// Custom message: leaf = parent (null if root), text goes to editor
|
|
1334
|
+
newLeafId = targetEntry.parentId;
|
|
1335
|
+
editorText =
|
|
1336
|
+
typeof targetEntry.content === "string"
|
|
1337
|
+
? targetEntry.content
|
|
1338
|
+
: targetEntry.content
|
|
1339
|
+
.filter((c) => c.type === "text")
|
|
1340
|
+
.map((c) => c.text)
|
|
1341
|
+
.join("");
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
// Non-user message: leaf = selected node
|
|
1345
|
+
newLeafId = targetId;
|
|
1346
|
+
}
|
|
1347
|
+
// Switch leaf (with or without summary)
|
|
1348
|
+
// Summary is attached at the navigation target position (newLeafId), not the old branch
|
|
1349
|
+
let summaryEntry;
|
|
1350
|
+
if (summaryText) {
|
|
1351
|
+
// Create summary at target position (can be null for root)
|
|
1352
|
+
const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromHook);
|
|
1353
|
+
summaryEntry = this.sessionManager.getEntry(summaryId);
|
|
1354
|
+
}
|
|
1355
|
+
else if (newLeafId === null) {
|
|
1356
|
+
// No summary, navigating to root - reset leaf
|
|
1357
|
+
this.sessionManager.resetLeaf();
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
// No summary, navigating to non-root
|
|
1361
|
+
this.sessionManager.branch(newLeafId);
|
|
1362
|
+
}
|
|
1363
|
+
// Update agent state
|
|
1364
|
+
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1365
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
1366
|
+
// Emit session_tree event
|
|
1367
|
+
if (this._hookRunner) {
|
|
1368
|
+
await this._hookRunner.emit({
|
|
1369
|
+
type: "session_tree",
|
|
1370
|
+
newLeafId: this.sessionManager.getLeafId(),
|
|
1371
|
+
oldLeafId,
|
|
1372
|
+
summaryEntry,
|
|
1373
|
+
fromHook: summaryText ? fromHook : undefined,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
// Emit to custom tools
|
|
1377
|
+
await this.emitCustomToolSessionEvent("tree", this.sessionFile);
|
|
1378
|
+
this._branchSummaryAbortController = undefined;
|
|
1379
|
+
return { editorText, cancelled: false, summaryEntry };
|
|
1380
|
+
}
|
|
1141
1381
|
/**
|
|
1142
1382
|
* Get all user messages from session for branch selector.
|
|
1143
1383
|
*/
|
|
1144
1384
|
getUserMessagesForBranching() {
|
|
1145
1385
|
const entries = this.sessionManager.getEntries();
|
|
1146
1386
|
const result = [];
|
|
1147
|
-
for (
|
|
1148
|
-
const entry = entries[i];
|
|
1387
|
+
for (const entry of entries) {
|
|
1149
1388
|
if (entry.type !== "message")
|
|
1150
1389
|
continue;
|
|
1151
1390
|
if (entry.message.role !== "user")
|
|
1152
1391
|
continue;
|
|
1153
1392
|
const text = this._extractUserMessageText(entry.message.content);
|
|
1154
1393
|
if (text) {
|
|
1155
|
-
result.push({
|
|
1394
|
+
result.push({ entryId: entry.id, text });
|
|
1156
1395
|
}
|
|
1157
1396
|
}
|
|
1158
1397
|
return result;
|
|
@@ -1226,22 +1465,30 @@ export class AgentSession {
|
|
|
1226
1465
|
/**
|
|
1227
1466
|
* Get text content of last assistant message.
|
|
1228
1467
|
* Useful for /copy command.
|
|
1229
|
-
* @returns Text content, or
|
|
1468
|
+
* @returns Text content, or undefined if no assistant message exists
|
|
1230
1469
|
*/
|
|
1231
1470
|
getLastAssistantText() {
|
|
1232
1471
|
const lastAssistant = this.messages
|
|
1233
1472
|
.slice()
|
|
1234
1473
|
.reverse()
|
|
1235
|
-
.find((m) =>
|
|
1474
|
+
.find((m) => {
|
|
1475
|
+
if (m.role !== "assistant")
|
|
1476
|
+
return false;
|
|
1477
|
+
const msg = m;
|
|
1478
|
+
// Skip aborted messages with no content
|
|
1479
|
+
if (msg.stopReason === "aborted" && msg.content.length === 0)
|
|
1480
|
+
return false;
|
|
1481
|
+
return true;
|
|
1482
|
+
});
|
|
1236
1483
|
if (!lastAssistant)
|
|
1237
|
-
return
|
|
1484
|
+
return undefined;
|
|
1238
1485
|
let text = "";
|
|
1239
1486
|
for (const content of lastAssistant.content) {
|
|
1240
1487
|
if (content.type === "text") {
|
|
1241
1488
|
text += content.text;
|
|
1242
1489
|
}
|
|
1243
1490
|
}
|
|
1244
|
-
return text.trim() ||
|
|
1491
|
+
return text.trim() || undefined;
|
|
1245
1492
|
}
|
|
1246
1493
|
// =========================================================================
|
|
1247
1494
|
// Hook System
|
|
@@ -1266,19 +1513,26 @@ export class AgentSession {
|
|
|
1266
1513
|
}
|
|
1267
1514
|
/**
|
|
1268
1515
|
* Emit session event to all custom tools.
|
|
1269
|
-
* Called on session switch, branch, and
|
|
1516
|
+
* Called on session switch, branch, tree navigation, and shutdown.
|
|
1270
1517
|
*/
|
|
1271
|
-
async
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1518
|
+
async emitCustomToolSessionEvent(reason, previousSessionFile) {
|
|
1519
|
+
if (!this._customTools)
|
|
1520
|
+
return;
|
|
1521
|
+
const event = { reason, previousSessionFile };
|
|
1522
|
+
const ctx = {
|
|
1523
|
+
sessionManager: this.sessionManager,
|
|
1524
|
+
modelRegistry: this._modelRegistry,
|
|
1525
|
+
model: this.agent.state.model,
|
|
1526
|
+
isIdle: () => !this.isStreaming,
|
|
1527
|
+
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
1528
|
+
abort: () => {
|
|
1529
|
+
this.abort();
|
|
1530
|
+
},
|
|
1277
1531
|
};
|
|
1278
1532
|
for (const { tool } of this._customTools) {
|
|
1279
1533
|
if (tool.onSession) {
|
|
1280
1534
|
try {
|
|
1281
|
-
await tool.onSession(event);
|
|
1535
|
+
await tool.onSession(event, ctx);
|
|
1282
1536
|
}
|
|
1283
1537
|
catch (_err) {
|
|
1284
1538
|
// Silently ignore tool errors during session events
|