@kolisachint/hoocode-agent 0.4.108 → 0.4.110

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +1 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/agent-session-compaction.d.ts +79 -0
  6. package/dist/core/agent-session-compaction.d.ts.map +1 -0
  7. package/dist/core/agent-session-compaction.js +346 -0
  8. package/dist/core/agent-session-compaction.js.map +1 -0
  9. package/dist/core/agent-session-retry.d.ts +76 -0
  10. package/dist/core/agent-session-retry.d.ts.map +1 -0
  11. package/dist/core/agent-session-retry.js +192 -0
  12. package/dist/core/agent-session-retry.js.map +1 -0
  13. package/dist/core/agent-session-skills.d.ts +34 -0
  14. package/dist/core/agent-session-skills.d.ts.map +1 -0
  15. package/dist/core/agent-session-skills.js +52 -0
  16. package/dist/core/agent-session-skills.js.map +1 -0
  17. package/dist/core/agent-session-stats.d.ts +74 -0
  18. package/dist/core/agent-session-stats.d.ts.map +1 -0
  19. package/dist/core/agent-session-stats.js +187 -0
  20. package/dist/core/agent-session-stats.js.map +1 -0
  21. package/dist/core/agent-session-tree-navigation.d.ts +69 -0
  22. package/dist/core/agent-session-tree-navigation.d.ts.map +1 -0
  23. package/dist/core/agent-session-tree-navigation.js +198 -0
  24. package/dist/core/agent-session-tree-navigation.js.map +1 -0
  25. package/dist/core/agent-session.d.ts +10 -66
  26. package/dist/core/agent-session.d.ts.map +1 -1
  27. package/dist/core/agent-session.js +96 -806
  28. package/dist/core/agent-session.js.map +1 -1
  29. package/dist/core/context-files.d.ts +25 -0
  30. package/dist/core/context-files.d.ts.map +1 -0
  31. package/dist/core/context-files.js +97 -0
  32. package/dist/core/context-files.js.map +1 -0
  33. package/dist/core/package-manager.d.ts.map +1 -1
  34. package/dist/core/package-manager.js +3 -519
  35. package/dist/core/package-manager.js.map +1 -1
  36. package/dist/core/package-resource-discovery.d.ts +62 -0
  37. package/dist/core/package-resource-discovery.d.ts.map +1 -0
  38. package/dist/core/package-resource-discovery.js +530 -0
  39. package/dist/core/package-resource-discovery.js.map +1 -0
  40. package/dist/core/resource-loader.d.ts +1 -10
  41. package/dist/core/resource-loader.d.ts.map +1 -1
  42. package/dist/core/resource-loader.js +4 -83
  43. package/dist/core/resource-loader.js.map +1 -1
  44. package/dist/core/settings-manager.d.ts +5 -140
  45. package/dist/core/settings-manager.d.ts.map +1 -1
  46. package/dist/core/settings-manager.js +4 -81
  47. package/dist/core/settings-manager.js.map +1 -1
  48. package/dist/core/settings-storage.d.ts +29 -0
  49. package/dist/core/settings-storage.d.ts.map +1 -0
  50. package/dist/core/settings-storage.js +90 -0
  51. package/dist/core/settings-storage.js.map +1 -0
  52. package/dist/core/settings-types.d.ts +128 -0
  53. package/dist/core/settings-types.d.ts.map +1 -0
  54. package/dist/core/settings-types.js +9 -0
  55. package/dist/core/settings-types.js.map +1 -0
  56. package/examples/extensions/bash-spawn-hook.ts +1 -1
  57. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  58. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  59. package/examples/extensions/sandbox/package.json +1 -1
  60. package/examples/extensions/with-deps/package.json +1 -1
  61. package/package.json +4 -4
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tree-navigation controller for AgentSession.
3
+ *
4
+ * Handles navigating to a different node in the session tree (staying in the
5
+ * same session file, unlike fork). When the user opts to summarize the
6
+ * abandoned branch it runs the `session_before_tree` hook, generates a branch
7
+ * summary (from an extension or the default summarizer), attaches it at the new
8
+ * leaf, refreshes agent context, and emits `session_tree`. Owns the branch
9
+ * summarization abort controller. Extracted from agent-session.ts behind a
10
+ * narrow TreeNavigationControllerDeps interface.
11
+ */
12
+ import type { AgentMessage } from "@kolisachint/hoocode-agent-core";
13
+ import type { Model } from "@kolisachint/hoocode-ai";
14
+ import type { ExtensionRunner } from "./extensions/index.js";
15
+ import type { BranchSummaryEntry, SessionManager } from "./session-manager.js";
16
+ import type { SettingsManager } from "./settings-manager.js";
17
+ /** Options for navigating the session tree. */
18
+ export interface NavigateTreeOptions {
19
+ /** Whether the user wants to summarize the abandoned branch */
20
+ summarize?: boolean;
21
+ /** Custom instructions for the summarizer */
22
+ customInstructions?: string;
23
+ /** If true, customInstructions replaces the default prompt */
24
+ replaceInstructions?: boolean;
25
+ /** Label to attach to the branch summary entry */
26
+ label?: string;
27
+ }
28
+ /** Result of navigating the session tree. */
29
+ export interface NavigateTreeResult {
30
+ editorText?: string;
31
+ cancelled: boolean;
32
+ aborted?: boolean;
33
+ summaryEntry?: BranchSummaryEntry;
34
+ }
35
+ /** Narrow dependencies the tree-navigation controller needs from AgentSession. */
36
+ export interface TreeNavigationControllerDeps {
37
+ sessionManager: SessionManager;
38
+ settingsManager: SettingsManager;
39
+ getModel(): Model<any> | undefined;
40
+ /** Read at call time; the extension runner is swapped on reload. */
41
+ getExtensionRunner(): ExtensionRunner;
42
+ getRequiredRequestAuth(model: Model<any>): Promise<{
43
+ apiKey: string;
44
+ headers?: Record<string, string>;
45
+ }>;
46
+ setAgentMessages(messages: AgentMessage[]): void;
47
+ }
48
+ export declare class TreeNavigationController {
49
+ private readonly deps;
50
+ private _branchSummaryAbortController;
51
+ constructor(deps: TreeNavigationControllerDeps);
52
+ /** Whether branch summarization is currently running */
53
+ get isSummarizing(): boolean;
54
+ /** Cancel in-progress branch summarization. */
55
+ abortBranchSummary(): void;
56
+ /**
57
+ * Navigate to a different node in the session tree.
58
+ * Unlike fork() which creates a new session file, this stays in the same file.
59
+ *
60
+ * @param targetId The entry ID to navigate to
61
+ * @param options.summarize Whether user wants to summarize abandoned branch
62
+ * @param options.customInstructions Custom instructions for summarizer
63
+ * @param options.replaceInstructions If true, customInstructions replaces the default prompt
64
+ * @param options.label Label to attach to the branch summary entry
65
+ * @returns Result with editorText (if user message) and cancelled status
66
+ */
67
+ navigateTree(targetId: string, options?: NavigateTreeOptions): Promise<NavigateTreeResult>;
68
+ }
69
+ //# sourceMappingURL=agent-session-tree-navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-tree-navigation.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-tree-navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,KAAK,EAAE,eAAe,EAA4C,MAAM,uBAAuB,CAAC;AACvG,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6CAA6C;IAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,6CAA6C;AAC7C,MAAM,WAAW,kBAAkB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED,kFAAkF;AAClF,MAAM,WAAW,4BAA4B;IAC5C,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;IACjC,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,oEAAoE;IACpE,kBAAkB,IAAI,eAAe,CAAC;IACtC,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IACzG,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;CACjD;AAED,qBAAa,wBAAwB;IAGxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,6BAA6B,CAA0C;IAE/E,YAA6B,IAAI,EAAE,4BAA4B,EAAI;IAEnE,wDAAwD;IACxD,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,+CAA+C;IAC/C,kBAAkB,IAAI,IAAI,CAEzB;IAED;;;;;;;;;;OAUG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAgLnG;CACD","sourcesContent":["/**\n * Tree-navigation controller for AgentSession.\n *\n * Handles navigating to a different node in the session tree (staying in the\n * same session file, unlike fork). When the user opts to summarize the\n * abandoned branch it runs the `session_before_tree` hook, generates a branch\n * summary (from an extension or the default summarizer), attaches it at the new\n * leaf, refreshes agent context, and emits `session_tree`. Owns the branch\n * summarization abort controller. Extracted from agent-session.ts behind a\n * narrow TreeNavigationControllerDeps interface.\n */\n\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { collectEntriesForBranchSummary, generateBranchSummary } from \"@kolisachint/hoocode-agent-core\";\nimport type { Model } from \"@kolisachint/hoocode-ai\";\nimport { extractUserMessageText } from \"./agent-session-stats.js\";\nimport type { ExtensionRunner, SessionBeforeTreeResult, TreePreparation } from \"./extensions/index.js\";\nimport type { BranchSummaryEntry, SessionManager } from \"./session-manager.js\";\nimport type { SettingsManager } from \"./settings-manager.js\";\n\n/** Options for navigating the session tree. */\nexport interface NavigateTreeOptions {\n\t/** Whether the user wants to summarize the abandoned branch */\n\tsummarize?: boolean;\n\t/** Custom instructions for the summarizer */\n\tcustomInstructions?: string;\n\t/** If true, customInstructions replaces the default prompt */\n\treplaceInstructions?: boolean;\n\t/** Label to attach to the branch summary entry */\n\tlabel?: string;\n}\n\n/** Result of navigating the session tree. */\nexport interface NavigateTreeResult {\n\teditorText?: string;\n\tcancelled: boolean;\n\taborted?: boolean;\n\tsummaryEntry?: BranchSummaryEntry;\n}\n\n/** Narrow dependencies the tree-navigation controller needs from AgentSession. */\nexport interface TreeNavigationControllerDeps {\n\tsessionManager: SessionManager;\n\tsettingsManager: SettingsManager;\n\tgetModel(): Model<any> | undefined;\n\t/** Read at call time; the extension runner is swapped on reload. */\n\tgetExtensionRunner(): ExtensionRunner;\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey: string; headers?: Record<string, string> }>;\n\tsetAgentMessages(messages: AgentMessage[]): void;\n}\n\nexport class TreeNavigationController {\n\tprivate _branchSummaryAbortController: AbortController | undefined = undefined;\n\n\tconstructor(private readonly deps: TreeNavigationControllerDeps) {}\n\n\t/** Whether branch summarization is currently running */\n\tget isSummarizing(): boolean {\n\t\treturn this._branchSummaryAbortController !== undefined;\n\t}\n\n\t/** Cancel in-progress branch summarization. */\n\tabortBranchSummary(): void {\n\t\tthis._branchSummaryAbortController?.abort();\n\t}\n\n\t/**\n\t * Navigate to a different node in the session tree.\n\t * Unlike fork() which creates a new session file, this stays in the same file.\n\t *\n\t * @param targetId The entry ID to navigate to\n\t * @param options.summarize Whether user wants to summarize abandoned branch\n\t * @param options.customInstructions Custom instructions for summarizer\n\t * @param options.replaceInstructions If true, customInstructions replaces the default prompt\n\t * @param options.label Label to attach to the branch summary entry\n\t * @returns Result with editorText (if user message) and cancelled status\n\t */\n\tasync navigateTree(targetId: string, options: NavigateTreeOptions = {}): Promise<NavigateTreeResult> {\n\t\tconst sessionManager = this.deps.sessionManager;\n\t\tconst extensionRunner = this.deps.getExtensionRunner();\n\t\tconst oldLeafId = sessionManager.getLeafId();\n\n\t\t// No-op if already at target\n\t\tif (targetId === oldLeafId) {\n\t\t\treturn { cancelled: false };\n\t\t}\n\n\t\t// Model required for summarization\n\t\tif (options.summarize && !this.deps.getModel()) {\n\t\t\tthrow new Error(\"No model available for summarization\");\n\t\t}\n\n\t\tconst targetEntry = sessionManager.getEntry(targetId);\n\t\tif (!targetEntry) {\n\t\t\tthrow new Error(`Entry ${targetId} not found`);\n\t\t}\n\n\t\t// Collect entries to summarize (from old leaf to common ancestor)\n\t\tconst { entries: entriesToSummarize, commonAncestorId } = await collectEntriesForBranchSummary(\n\t\t\tsessionManager,\n\t\t\toldLeafId,\n\t\t\ttargetId,\n\t\t);\n\n\t\t// Prepare event data - mutable so extensions can override\n\t\tlet customInstructions = options.customInstructions;\n\t\tlet replaceInstructions = options.replaceInstructions;\n\t\tlet label = options.label;\n\n\t\tconst preparation: TreePreparation = {\n\t\t\ttargetId,\n\t\t\toldLeafId,\n\t\t\tcommonAncestorId,\n\t\t\tentriesToSummarize,\n\t\t\tuserWantsSummary: options.summarize ?? false,\n\t\t\tcustomInstructions,\n\t\t\treplaceInstructions,\n\t\t\tlabel,\n\t\t};\n\n\t\t// Set up abort controller for summarization\n\t\tthis._branchSummaryAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tlet extensionSummary: { summary: string; details?: unknown } | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\t// Emit session_before_tree event\n\t\t\tif (extensionRunner.hasHandlers(\"session_before_tree\")) {\n\t\t\t\tconst result = (await extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_tree\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t})) as SessionBeforeTreeResult | undefined;\n\n\t\t\t\tif (result?.cancel) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\n\t\t\t\tif (result?.summary && options.summarize) {\n\t\t\t\t\textensionSummary = result.summary;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\n\t\t\t\t// Allow extensions to override instructions and label\n\t\t\t\tif (result?.customInstructions !== undefined) {\n\t\t\t\t\tcustomInstructions = result.customInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.replaceInstructions !== undefined) {\n\t\t\t\t\treplaceInstructions = result.replaceInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.label !== undefined) {\n\t\t\t\t\tlabel = result.label;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Run default summarizer if needed\n\t\t\tlet summaryText: string | undefined;\n\t\t\tlet summaryDetails: unknown;\n\t\t\tif (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {\n\t\t\t\tconst model = this.deps.getModel()!;\n\t\t\t\tconst { apiKey, headers } = await this.deps.getRequiredRequestAuth(model);\n\t\t\t\tconst branchSummarySettings = this.deps.settingsManager.getBranchSummarySettings();\n\t\t\t\tconst result = await generateBranchSummary(entriesToSummarize, {\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\treplaceInstructions,\n\t\t\t\t\treserveTokens: branchSummarySettings.reserveTokens,\n\t\t\t\t});\n\t\t\t\tif (result.aborted) {\n\t\t\t\t\treturn { cancelled: true, aborted: true };\n\t\t\t\t}\n\t\t\t\tif (result.error) {\n\t\t\t\t\tthrow new Error(result.error);\n\t\t\t\t}\n\t\t\t\tsummaryText = result.summary;\n\t\t\t\tsummaryDetails = {\n\t\t\t\t\treadFiles: result.readFiles || [],\n\t\t\t\t\tmodifiedFiles: result.modifiedFiles || [],\n\t\t\t\t};\n\t\t\t} else if (extensionSummary) {\n\t\t\t\tsummaryText = extensionSummary.summary;\n\t\t\t\tsummaryDetails = extensionSummary.details;\n\t\t\t}\n\n\t\t\t// Determine the new leaf position based on target type\n\t\t\tlet newLeafId: string | null;\n\t\t\tlet editorText: string | undefined;\n\n\t\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t\t// User message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText = extractUserMessageText(targetEntry.message.content);\n\t\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t\t// Custom message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText =\n\t\t\t\t\ttypeof targetEntry.content === \"string\"\n\t\t\t\t\t\t? targetEntry.content\n\t\t\t\t\t\t: targetEntry.content\n\t\t\t\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t\t.join(\"\");\n\t\t\t} else {\n\t\t\t\t// Non-user message: leaf = selected node\n\t\t\t\tnewLeafId = targetId;\n\t\t\t}\n\n\t\t\t// Switch leaf (with or without summary)\n\t\t\t// Summary is attached at the navigation target position (newLeafId), not the old branch\n\t\t\tlet summaryEntry: BranchSummaryEntry | undefined;\n\t\t\tif (summaryText) {\n\t\t\t\t// Create summary at target position (can be null for root)\n\t\t\t\tconst summaryId = sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);\n\t\t\t\tsummaryEntry = sessionManager.getEntry(summaryId) as BranchSummaryEntry;\n\n\t\t\t\t// Attach label to the summary entry\n\t\t\t\tif (label) {\n\t\t\t\t\tsessionManager.appendLabelChange(summaryId, label);\n\t\t\t\t}\n\t\t\t} else if (newLeafId === null) {\n\t\t\t\t// No summary, navigating to root - reset leaf\n\t\t\t\tsessionManager.resetLeaf();\n\t\t\t} else {\n\t\t\t\t// No summary, navigating to non-root\n\t\t\t\tsessionManager.branch(newLeafId);\n\t\t\t}\n\n\t\t\t// Attach label to target entry when not summarizing (no summary entry to label)\n\t\t\tif (label && !summaryText) {\n\t\t\t\tsessionManager.appendLabelChange(targetId, label);\n\t\t\t}\n\n\t\t\t// Update agent state\n\t\t\tconst sessionContext = sessionManager.buildSessionContext();\n\t\t\tthis.deps.setAgentMessages(sessionContext.messages);\n\n\t\t\t// Emit session_tree event\n\t\t\tawait extensionRunner.emit({\n\t\t\t\ttype: \"session_tree\",\n\t\t\t\tnewLeafId: sessionManager.getLeafId(),\n\t\t\t\toldLeafId,\n\t\t\t\tsummaryEntry,\n\t\t\t\tfromExtension: summaryText ? fromExtension : undefined,\n\t\t\t});\n\n\t\t\treturn { editorText, cancelled: false, summaryEntry };\n\t\t} finally {\n\t\t\tthis._branchSummaryAbortController = undefined;\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Tree-navigation controller for AgentSession.
3
+ *
4
+ * Handles navigating to a different node in the session tree (staying in the
5
+ * same session file, unlike fork). When the user opts to summarize the
6
+ * abandoned branch it runs the `session_before_tree` hook, generates a branch
7
+ * summary (from an extension or the default summarizer), attaches it at the new
8
+ * leaf, refreshes agent context, and emits `session_tree`. Owns the branch
9
+ * summarization abort controller. Extracted from agent-session.ts behind a
10
+ * narrow TreeNavigationControllerDeps interface.
11
+ */
12
+ import { collectEntriesForBranchSummary, generateBranchSummary } from "@kolisachint/hoocode-agent-core";
13
+ import { extractUserMessageText } from "./agent-session-stats.js";
14
+ export class TreeNavigationController {
15
+ deps;
16
+ _branchSummaryAbortController = undefined;
17
+ constructor(deps) {
18
+ this.deps = deps;
19
+ }
20
+ /** Whether branch summarization is currently running */
21
+ get isSummarizing() {
22
+ return this._branchSummaryAbortController !== undefined;
23
+ }
24
+ /** Cancel in-progress branch summarization. */
25
+ abortBranchSummary() {
26
+ this._branchSummaryAbortController?.abort();
27
+ }
28
+ /**
29
+ * Navigate to a different node in the session tree.
30
+ * Unlike fork() which creates a new session file, this stays in the same file.
31
+ *
32
+ * @param targetId The entry ID to navigate to
33
+ * @param options.summarize Whether user wants to summarize abandoned branch
34
+ * @param options.customInstructions Custom instructions for summarizer
35
+ * @param options.replaceInstructions If true, customInstructions replaces the default prompt
36
+ * @param options.label Label to attach to the branch summary entry
37
+ * @returns Result with editorText (if user message) and cancelled status
38
+ */
39
+ async navigateTree(targetId, options = {}) {
40
+ const sessionManager = this.deps.sessionManager;
41
+ const extensionRunner = this.deps.getExtensionRunner();
42
+ const oldLeafId = sessionManager.getLeafId();
43
+ // No-op if already at target
44
+ if (targetId === oldLeafId) {
45
+ return { cancelled: false };
46
+ }
47
+ // Model required for summarization
48
+ if (options.summarize && !this.deps.getModel()) {
49
+ throw new Error("No model available for summarization");
50
+ }
51
+ const targetEntry = sessionManager.getEntry(targetId);
52
+ if (!targetEntry) {
53
+ throw new Error(`Entry ${targetId} not found`);
54
+ }
55
+ // Collect entries to summarize (from old leaf to common ancestor)
56
+ const { entries: entriesToSummarize, commonAncestorId } = await collectEntriesForBranchSummary(sessionManager, oldLeafId, targetId);
57
+ // Prepare event data - mutable so extensions can override
58
+ let customInstructions = options.customInstructions;
59
+ let replaceInstructions = options.replaceInstructions;
60
+ let label = options.label;
61
+ const preparation = {
62
+ targetId,
63
+ oldLeafId,
64
+ commonAncestorId,
65
+ entriesToSummarize,
66
+ userWantsSummary: options.summarize ?? false,
67
+ customInstructions,
68
+ replaceInstructions,
69
+ label,
70
+ };
71
+ // Set up abort controller for summarization
72
+ this._branchSummaryAbortController = new AbortController();
73
+ try {
74
+ let extensionSummary;
75
+ let fromExtension = false;
76
+ // Emit session_before_tree event
77
+ if (extensionRunner.hasHandlers("session_before_tree")) {
78
+ const result = (await extensionRunner.emit({
79
+ type: "session_before_tree",
80
+ preparation,
81
+ signal: this._branchSummaryAbortController.signal,
82
+ }));
83
+ if (result?.cancel) {
84
+ return { cancelled: true };
85
+ }
86
+ if (result?.summary && options.summarize) {
87
+ extensionSummary = result.summary;
88
+ fromExtension = true;
89
+ }
90
+ // Allow extensions to override instructions and label
91
+ if (result?.customInstructions !== undefined) {
92
+ customInstructions = result.customInstructions;
93
+ }
94
+ if (result?.replaceInstructions !== undefined) {
95
+ replaceInstructions = result.replaceInstructions;
96
+ }
97
+ if (result?.label !== undefined) {
98
+ label = result.label;
99
+ }
100
+ }
101
+ // Run default summarizer if needed
102
+ let summaryText;
103
+ let summaryDetails;
104
+ if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
105
+ const model = this.deps.getModel();
106
+ const { apiKey, headers } = await this.deps.getRequiredRequestAuth(model);
107
+ const branchSummarySettings = this.deps.settingsManager.getBranchSummarySettings();
108
+ const result = await generateBranchSummary(entriesToSummarize, {
109
+ model,
110
+ apiKey,
111
+ headers,
112
+ signal: this._branchSummaryAbortController.signal,
113
+ customInstructions,
114
+ replaceInstructions,
115
+ reserveTokens: branchSummarySettings.reserveTokens,
116
+ });
117
+ if (result.aborted) {
118
+ return { cancelled: true, aborted: true };
119
+ }
120
+ if (result.error) {
121
+ throw new Error(result.error);
122
+ }
123
+ summaryText = result.summary;
124
+ summaryDetails = {
125
+ readFiles: result.readFiles || [],
126
+ modifiedFiles: result.modifiedFiles || [],
127
+ };
128
+ }
129
+ else if (extensionSummary) {
130
+ summaryText = extensionSummary.summary;
131
+ summaryDetails = extensionSummary.details;
132
+ }
133
+ // Determine the new leaf position based on target type
134
+ let newLeafId;
135
+ let editorText;
136
+ if (targetEntry.type === "message" && targetEntry.message.role === "user") {
137
+ // User message: leaf = parent (null if root), text goes to editor
138
+ newLeafId = targetEntry.parentId;
139
+ editorText = extractUserMessageText(targetEntry.message.content);
140
+ }
141
+ else if (targetEntry.type === "custom_message") {
142
+ // Custom message: leaf = parent (null if root), text goes to editor
143
+ newLeafId = targetEntry.parentId;
144
+ editorText =
145
+ typeof targetEntry.content === "string"
146
+ ? targetEntry.content
147
+ : targetEntry.content
148
+ .filter((c) => c.type === "text")
149
+ .map((c) => c.text)
150
+ .join("");
151
+ }
152
+ else {
153
+ // Non-user message: leaf = selected node
154
+ newLeafId = targetId;
155
+ }
156
+ // Switch leaf (with or without summary)
157
+ // Summary is attached at the navigation target position (newLeafId), not the old branch
158
+ let summaryEntry;
159
+ if (summaryText) {
160
+ // Create summary at target position (can be null for root)
161
+ const summaryId = sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);
162
+ summaryEntry = sessionManager.getEntry(summaryId);
163
+ // Attach label to the summary entry
164
+ if (label) {
165
+ sessionManager.appendLabelChange(summaryId, label);
166
+ }
167
+ }
168
+ else if (newLeafId === null) {
169
+ // No summary, navigating to root - reset leaf
170
+ sessionManager.resetLeaf();
171
+ }
172
+ else {
173
+ // No summary, navigating to non-root
174
+ sessionManager.branch(newLeafId);
175
+ }
176
+ // Attach label to target entry when not summarizing (no summary entry to label)
177
+ if (label && !summaryText) {
178
+ sessionManager.appendLabelChange(targetId, label);
179
+ }
180
+ // Update agent state
181
+ const sessionContext = sessionManager.buildSessionContext();
182
+ this.deps.setAgentMessages(sessionContext.messages);
183
+ // Emit session_tree event
184
+ await extensionRunner.emit({
185
+ type: "session_tree",
186
+ newLeafId: sessionManager.getLeafId(),
187
+ oldLeafId,
188
+ summaryEntry,
189
+ fromExtension: summaryText ? fromExtension : undefined,
190
+ });
191
+ return { editorText, cancelled: false, summaryEntry };
192
+ }
193
+ finally {
194
+ this._branchSummaryAbortController = undefined;
195
+ }
196
+ }
197
+ }
198
+ //# sourceMappingURL=agent-session-tree-navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-tree-navigation.js","sourceRoot":"","sources":["../../src/core/agent-session-tree-navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,8BAA8B,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAExG,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAoClE,MAAM,OAAO,wBAAwB;IAGP,IAAI;IAFzB,6BAA6B,GAAgC,SAAS,CAAC;IAE/E,YAA6B,IAAkC,EAAE;oBAApC,IAAI;IAAiC,CAAC;IAEnE,wDAAwD;IACxD,IAAI,aAAa,GAAY;QAC5B,OAAO,IAAI,CAAC,6BAA6B,KAAK,SAAS,CAAC;IAAA,CACxD;IAED,+CAA+C;IAC/C,kBAAkB,GAAS;QAC1B,IAAI,CAAC,6BAA6B,EAAE,KAAK,EAAE,CAAC;IAAA,CAC5C;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,YAAY,CAAC,QAAgB,EAAE,OAAO,GAAwB,EAAE,EAA+B;QACpG,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC;QAChD,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACvD,MAAM,SAAS,GAAG,cAAc,CAAC,SAAS,EAAE,CAAC;QAE7C,6BAA6B;QAC7B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QAC7B,CAAC;QAED,mCAAmC;QACnC,IAAI,OAAO,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YAChD,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QACzD,CAAC;QAED,MAAM,WAAW,GAAG,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACtD,IAAI,CAAC,WAAW,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,YAAY,CAAC,CAAC;QAChD,CAAC;QAED,kEAAkE;QAClE,MAAM,EAAE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,GAAG,MAAM,8BAA8B,CAC7F,cAAc,EACd,SAAS,EACT,QAAQ,CACR,CAAC;QAEF,0DAA0D;QAC1D,IAAI,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;QACpD,IAAI,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC;QACtD,IAAI,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAE1B,MAAM,WAAW,GAAoB;YACpC,QAAQ;YACR,SAAS;YACT,gBAAgB;YAChB,kBAAkB;YAClB,gBAAgB,EAAE,OAAO,CAAC,SAAS,IAAI,KAAK;YAC5C,kBAAkB;YAClB,mBAAmB;YACnB,KAAK;SACL,CAAC;QAEF,4CAA4C;QAC5C,IAAI,CAAC,6BAA6B,GAAG,IAAI,eAAe,EAAE,CAAC;QAE3D,IAAI,CAAC;YACJ,IAAI,gBAAoE,CAAC;YACzE,IAAI,aAAa,GAAG,KAAK,CAAC;YAE1B,iCAAiC;YACjC,IAAI,eAAe,CAAC,WAAW,CAAC,qBAAqB,CAAC,EAAE,CAAC;gBACxD,MAAM,MAAM,GAAG,CAAC,MAAM,eAAe,CAAC,IAAI,CAAC;oBAC1C,IAAI,EAAE,qBAAqB;oBAC3B,WAAW;oBACX,MAAM,EAAE,IAAI,CAAC,6BAA6B,CAAC,MAAM;iBACjD,CAAC,CAAwC,CAAC;gBAE3C,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC;oBACpB,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;gBAC5B,CAAC;gBAED,IAAI,MAAM,EAAE,OAAO,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;oBAC1C,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC;oBAClC,aAAa,GAAG,IAAI,CAAC;gBACtB,CAAC;gBAED,sDAAsD;gBACtD,IAAI,MAAM,EAAE,kBAAkB,KAAK,SAAS,EAAE,CAAC;oBAC9C,kBAAkB,GAAG,MAAM,CAAC,kBAAkB,CAAC;gBAChD,CAAC;gBACD,IAAI,MAAM,EAAE,mBAAmB,KAAK,SAAS,EAAE,CAAC;oBAC/C,mBAAmB,GAAG,MAAM,CAAC,mBAAmB,CAAC;gBAClD,CAAC;gBACD,IAAI,MAAM,EAAE,KAAK,KAAK,SAAS,EAAE,CAAC;oBACjC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;gBACtB,CAAC;YACF,CAAC;YAED,mCAAmC;YACnC,IAAI,WAA+B,CAAC;YACpC,IAAI,cAAuB,CAAC;YAC5B,IAAI,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAG,CAAC;gBACpC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC;gBAC1E,MAAM,qBAAqB,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,wBAAwB,EAAE,CAAC;gBACnF,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,kBAAkB,EAAE;oBAC9D,KAAK;oBACL,MAAM;oBACN,OAAO;oBACP,MAAM,EAAE,IAAI,CAAC,6BAA6B,CAAC,MAAM;oBACjD,kBAAkB;oBAClB,mBAAmB;oBACnB,aAAa,EAAE,qBAAqB,CAAC,aAAa;iBAClD,CAAC,CAAC;gBACH,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;oBACpB,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;gBAC3C,CAAC;gBACD,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC/B,CAAC;gBACD,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC;gBAC7B,cAAc,GAAG;oBAChB,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;oBACjC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;iBACzC,CAAC;YACH,CAAC;iBAAM,IAAI,gBAAgB,EAAE,CAAC;gBAC7B,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC;gBACvC,cAAc,GAAG,gBAAgB,CAAC,OAAO,CAAC;YAC3C,CAAC;YAED,uDAAuD;YACvD,IAAI,SAAwB,CAAC;YAC7B,IAAI,UAA8B,CAAC;YAEnC,IAAI,WAAW,CAAC,IAAI,KAAK,SAAS,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC3E,kEAAkE;gBAClE,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC;gBACjC,UAAU,GAAG,sBAAsB,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAClE,CAAC;iBAAM,IAAI,WAAW,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;gBAClD,oEAAoE;gBACpE,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC;gBACjC,UAAU;oBACT,OAAO,WAAW,CAAC,OAAO,KAAK,QAAQ;wBACtC,CAAC,CAAC,WAAW,CAAC,OAAO;wBACrB,CAAC,CAAC,WAAW,CAAC,OAAO;6BAClB,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;6BACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;6BAClB,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,CAAC;iBAAM,CAAC;gBACP,yCAAyC;gBACzC,SAAS,GAAG,QAAQ,CAAC;YACtB,CAAC;YAED,wCAAwC;YACxC,wFAAwF;YACxF,IAAI,YAA4C,CAAC;YACjD,IAAI,WAAW,EAAE,CAAC;gBACjB,2DAA2D;gBAC3D,MAAM,SAAS,GAAG,cAAc,CAAC,iBAAiB,CAAC,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC;gBAC1G,YAAY,GAAG,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAuB,CAAC;gBAExE,oCAAoC;gBACpC,IAAI,KAAK,EAAE,CAAC;oBACX,cAAc,CAAC,iBAAiB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;gBACpD,CAAC;YACF,CAAC;iBAAM,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;gBAC/B,8CAA8C;gBAC9C,cAAc,CAAC,SAAS,EAAE,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACP,qCAAqC;gBACrC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAClC,CAAC;YAED,gFAAgF;YAChF,IAAI,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;gBAC3B,cAAc,CAAC,iBAAiB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACnD,CAAC;YAED,qBAAqB;YACrB,MAAM,cAAc,GAAG,cAAc,CAAC,mBAAmB,EAAE,CAAC;YAC5D,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;YAEpD,0BAA0B;YAC1B,MAAM,eAAe,CAAC,IAAI,CAAC;gBAC1B,IAAI,EAAE,cAAc;gBACpB,SAAS,EAAE,cAAc,CAAC,SAAS,EAAE;gBACrC,SAAS;gBACT,YAAY;gBACZ,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;aACtD,CAAC,CAAC;YAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;QACvD,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,6BAA6B,GAAG,SAAS,CAAC;QAChD,CAAC;IAAA,CACD;CACD","sourcesContent":["/**\n * Tree-navigation controller for AgentSession.\n *\n * Handles navigating to a different node in the session tree (staying in the\n * same session file, unlike fork). When the user opts to summarize the\n * abandoned branch it runs the `session_before_tree` hook, generates a branch\n * summary (from an extension or the default summarizer), attaches it at the new\n * leaf, refreshes agent context, and emits `session_tree`. Owns the branch\n * summarization abort controller. Extracted from agent-session.ts behind a\n * narrow TreeNavigationControllerDeps interface.\n */\n\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { collectEntriesForBranchSummary, generateBranchSummary } from \"@kolisachint/hoocode-agent-core\";\nimport type { Model } from \"@kolisachint/hoocode-ai\";\nimport { extractUserMessageText } from \"./agent-session-stats.js\";\nimport type { ExtensionRunner, SessionBeforeTreeResult, TreePreparation } from \"./extensions/index.js\";\nimport type { BranchSummaryEntry, SessionManager } from \"./session-manager.js\";\nimport type { SettingsManager } from \"./settings-manager.js\";\n\n/** Options for navigating the session tree. */\nexport interface NavigateTreeOptions {\n\t/** Whether the user wants to summarize the abandoned branch */\n\tsummarize?: boolean;\n\t/** Custom instructions for the summarizer */\n\tcustomInstructions?: string;\n\t/** If true, customInstructions replaces the default prompt */\n\treplaceInstructions?: boolean;\n\t/** Label to attach to the branch summary entry */\n\tlabel?: string;\n}\n\n/** Result of navigating the session tree. */\nexport interface NavigateTreeResult {\n\teditorText?: string;\n\tcancelled: boolean;\n\taborted?: boolean;\n\tsummaryEntry?: BranchSummaryEntry;\n}\n\n/** Narrow dependencies the tree-navigation controller needs from AgentSession. */\nexport interface TreeNavigationControllerDeps {\n\tsessionManager: SessionManager;\n\tsettingsManager: SettingsManager;\n\tgetModel(): Model<any> | undefined;\n\t/** Read at call time; the extension runner is swapped on reload. */\n\tgetExtensionRunner(): ExtensionRunner;\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey: string; headers?: Record<string, string> }>;\n\tsetAgentMessages(messages: AgentMessage[]): void;\n}\n\nexport class TreeNavigationController {\n\tprivate _branchSummaryAbortController: AbortController | undefined = undefined;\n\n\tconstructor(private readonly deps: TreeNavigationControllerDeps) {}\n\n\t/** Whether branch summarization is currently running */\n\tget isSummarizing(): boolean {\n\t\treturn this._branchSummaryAbortController !== undefined;\n\t}\n\n\t/** Cancel in-progress branch summarization. */\n\tabortBranchSummary(): void {\n\t\tthis._branchSummaryAbortController?.abort();\n\t}\n\n\t/**\n\t * Navigate to a different node in the session tree.\n\t * Unlike fork() which creates a new session file, this stays in the same file.\n\t *\n\t * @param targetId The entry ID to navigate to\n\t * @param options.summarize Whether user wants to summarize abandoned branch\n\t * @param options.customInstructions Custom instructions for summarizer\n\t * @param options.replaceInstructions If true, customInstructions replaces the default prompt\n\t * @param options.label Label to attach to the branch summary entry\n\t * @returns Result with editorText (if user message) and cancelled status\n\t */\n\tasync navigateTree(targetId: string, options: NavigateTreeOptions = {}): Promise<NavigateTreeResult> {\n\t\tconst sessionManager = this.deps.sessionManager;\n\t\tconst extensionRunner = this.deps.getExtensionRunner();\n\t\tconst oldLeafId = sessionManager.getLeafId();\n\n\t\t// No-op if already at target\n\t\tif (targetId === oldLeafId) {\n\t\t\treturn { cancelled: false };\n\t\t}\n\n\t\t// Model required for summarization\n\t\tif (options.summarize && !this.deps.getModel()) {\n\t\t\tthrow new Error(\"No model available for summarization\");\n\t\t}\n\n\t\tconst targetEntry = sessionManager.getEntry(targetId);\n\t\tif (!targetEntry) {\n\t\t\tthrow new Error(`Entry ${targetId} not found`);\n\t\t}\n\n\t\t// Collect entries to summarize (from old leaf to common ancestor)\n\t\tconst { entries: entriesToSummarize, commonAncestorId } = await collectEntriesForBranchSummary(\n\t\t\tsessionManager,\n\t\t\toldLeafId,\n\t\t\ttargetId,\n\t\t);\n\n\t\t// Prepare event data - mutable so extensions can override\n\t\tlet customInstructions = options.customInstructions;\n\t\tlet replaceInstructions = options.replaceInstructions;\n\t\tlet label = options.label;\n\n\t\tconst preparation: TreePreparation = {\n\t\t\ttargetId,\n\t\t\toldLeafId,\n\t\t\tcommonAncestorId,\n\t\t\tentriesToSummarize,\n\t\t\tuserWantsSummary: options.summarize ?? false,\n\t\t\tcustomInstructions,\n\t\t\treplaceInstructions,\n\t\t\tlabel,\n\t\t};\n\n\t\t// Set up abort controller for summarization\n\t\tthis._branchSummaryAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tlet extensionSummary: { summary: string; details?: unknown } | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\t// Emit session_before_tree event\n\t\t\tif (extensionRunner.hasHandlers(\"session_before_tree\")) {\n\t\t\t\tconst result = (await extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_tree\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t})) as SessionBeforeTreeResult | undefined;\n\n\t\t\t\tif (result?.cancel) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\n\t\t\t\tif (result?.summary && options.summarize) {\n\t\t\t\t\textensionSummary = result.summary;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\n\t\t\t\t// Allow extensions to override instructions and label\n\t\t\t\tif (result?.customInstructions !== undefined) {\n\t\t\t\t\tcustomInstructions = result.customInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.replaceInstructions !== undefined) {\n\t\t\t\t\treplaceInstructions = result.replaceInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.label !== undefined) {\n\t\t\t\t\tlabel = result.label;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Run default summarizer if needed\n\t\t\tlet summaryText: string | undefined;\n\t\t\tlet summaryDetails: unknown;\n\t\t\tif (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {\n\t\t\t\tconst model = this.deps.getModel()!;\n\t\t\t\tconst { apiKey, headers } = await this.deps.getRequiredRequestAuth(model);\n\t\t\t\tconst branchSummarySettings = this.deps.settingsManager.getBranchSummarySettings();\n\t\t\t\tconst result = await generateBranchSummary(entriesToSummarize, {\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\treplaceInstructions,\n\t\t\t\t\treserveTokens: branchSummarySettings.reserveTokens,\n\t\t\t\t});\n\t\t\t\tif (result.aborted) {\n\t\t\t\t\treturn { cancelled: true, aborted: true };\n\t\t\t\t}\n\t\t\t\tif (result.error) {\n\t\t\t\t\tthrow new Error(result.error);\n\t\t\t\t}\n\t\t\t\tsummaryText = result.summary;\n\t\t\t\tsummaryDetails = {\n\t\t\t\t\treadFiles: result.readFiles || [],\n\t\t\t\t\tmodifiedFiles: result.modifiedFiles || [],\n\t\t\t\t};\n\t\t\t} else if (extensionSummary) {\n\t\t\t\tsummaryText = extensionSummary.summary;\n\t\t\t\tsummaryDetails = extensionSummary.details;\n\t\t\t}\n\n\t\t\t// Determine the new leaf position based on target type\n\t\t\tlet newLeafId: string | null;\n\t\t\tlet editorText: string | undefined;\n\n\t\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t\t// User message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText = extractUserMessageText(targetEntry.message.content);\n\t\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t\t// Custom message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText =\n\t\t\t\t\ttypeof targetEntry.content === \"string\"\n\t\t\t\t\t\t? targetEntry.content\n\t\t\t\t\t\t: targetEntry.content\n\t\t\t\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t\t.join(\"\");\n\t\t\t} else {\n\t\t\t\t// Non-user message: leaf = selected node\n\t\t\t\tnewLeafId = targetId;\n\t\t\t}\n\n\t\t\t// Switch leaf (with or without summary)\n\t\t\t// Summary is attached at the navigation target position (newLeafId), not the old branch\n\t\t\tlet summaryEntry: BranchSummaryEntry | undefined;\n\t\t\tif (summaryText) {\n\t\t\t\t// Create summary at target position (can be null for root)\n\t\t\t\tconst summaryId = sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);\n\t\t\t\tsummaryEntry = sessionManager.getEntry(summaryId) as BranchSummaryEntry;\n\n\t\t\t\t// Attach label to the summary entry\n\t\t\t\tif (label) {\n\t\t\t\t\tsessionManager.appendLabelChange(summaryId, label);\n\t\t\t\t}\n\t\t\t} else if (newLeafId === null) {\n\t\t\t\t// No summary, navigating to root - reset leaf\n\t\t\t\tsessionManager.resetLeaf();\n\t\t\t} else {\n\t\t\t\t// No summary, navigating to non-root\n\t\t\t\tsessionManager.branch(newLeafId);\n\t\t\t}\n\n\t\t\t// Attach label to target entry when not summarizing (no summary entry to label)\n\t\t\tif (label && !summaryText) {\n\t\t\t\tsessionManager.appendLabelChange(targetId, label);\n\t\t\t}\n\n\t\t\t// Update agent state\n\t\t\tconst sessionContext = sessionManager.buildSessionContext();\n\t\t\tthis.deps.setAgentMessages(sessionContext.messages);\n\n\t\t\t// Emit session_tree event\n\t\t\tawait extensionRunner.emit({\n\t\t\t\ttype: \"session_tree\",\n\t\t\t\tnewLeafId: sessionManager.getLeafId(),\n\t\t\t\toldLeafId,\n\t\t\t\tsummaryEntry,\n\t\t\t\tfromExtension: summaryText ? fromExtension : undefined,\n\t\t\t});\n\n\t\t\treturn { editorText, cancelled: false, summaryEntry };\n\t\t} finally {\n\t\t\tthis._branchSummaryAbortController = undefined;\n\t\t}\n\t}\n}\n"]}
@@ -14,26 +14,18 @@
14
14
  */
15
15
  import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, CompactionResult, CustomMessage, ThinkingLevel } from "@kolisachint/hoocode-agent-core";
16
16
  import type { ImageContent, Model, TextContent } from "@kolisachint/hoocode-ai";
17
+ import { type SessionStats } from "./agent-session-stats.js";
18
+ import { type NavigateTreeOptions, type NavigateTreeResult } from "./agent-session-tree-navigation.js";
17
19
  import { type BashResult } from "./bash-executor.js";
18
20
  import { type ContextUsage, type ExtensionCommandContextActions, type ExtensionErrorListener, ExtensionRunner, type ExtensionUIContext, type InputSource, type ReplacedSessionContext, type SessionStartEvent, type ShutdownHandler, type ToolDefinition, type ToolInfo } from "./extensions/index.js";
19
21
  import type { ModelRegistry } from "./model-registry.js";
20
22
  import { type PromptTemplate } from "./prompt-templates.js";
21
23
  import type { ResourceLoader } from "./resource-loader.js";
22
- import type { BranchSummaryEntry, SessionManager } from "./session-manager.js";
24
+ import type { SessionManager } from "./session-manager.js";
23
25
  import type { SettingsManager } from "./settings-manager.js";
24
26
  import { type BashOperations } from "./tools/bash.js";
25
- /** Parsed skill block from a user message */
26
- export interface ParsedSkillBlock {
27
- name: string;
28
- location: string;
29
- content: string;
30
- userMessage: string | undefined;
31
- }
32
- /**
33
- * Parse a skill block from message text.
34
- * Returns null if the text doesn't contain a skill block.
35
- */
36
- export declare function parseSkillBlock(text: string): ParsedSkillBlock | null;
27
+ export type { ParsedSkillBlock } from "./agent-session-skills.js";
28
+ export { parseSkillBlock } from "./agent-session-skills.js";
37
29
  /** Session-specific events that extend the core AgentEvent */
38
30
  export type AgentSessionEvent = AgentEvent | {
39
31
  type: "queue_update";
@@ -131,25 +123,7 @@ export interface ModelCycleResult {
131
123
  /** Whether cycling through scoped models (--models flag) or all available */
132
124
  isScoped: boolean;
133
125
  }
134
- /** Session statistics for /session command */
135
- export interface SessionStats {
136
- sessionFile: string | undefined;
137
- sessionId: string;
138
- userMessages: number;
139
- assistantMessages: number;
140
- toolCalls: number;
141
- toolResults: number;
142
- totalMessages: number;
143
- tokens: {
144
- input: number;
145
- output: number;
146
- cacheRead: number;
147
- cacheWrite: number;
148
- total: number;
149
- };
150
- cost: number;
151
- contextUsage?: ContextUsage;
152
- }
126
+ export type { SessionStats } from "./agent-session-stats.js";
153
127
  export declare class AgentSession {
154
128
  readonly agent: Agent;
155
129
  readonly sessionManager: SessionManager;
@@ -164,14 +138,9 @@ export declare class AgentSession {
164
138
  private _followUpMessages;
165
139
  /** Messages queued to be included with the next user prompt as context ("asides"). */
166
140
  private _pendingNextTurnMessages;
167
- private _compactionAbortController;
168
- private _autoCompactionAbortController;
169
- private _overflowRecoveryAttempted;
170
- private _branchSummaryAbortController;
171
- private _retryAbortController;
172
- private _retryAttempt;
173
- private _retryPromise;
174
- private _retryResolve;
141
+ private _compaction;
142
+ private _tree;
143
+ private _retry;
175
144
  private _bashAbortController;
176
145
  private _pendingBashMessages;
177
146
  private _extensionRunner;
@@ -217,11 +186,7 @@ export declare class AgentSession {
217
186
  private _lastAssistantMessage;
218
187
  /** Internal handler for agent events - shared by subscribe and reconnect */
219
188
  private _handleAgentEvent;
220
- private _createRetryPromiseForAgentEnd;
221
- private _findLastAssistantInMessages;
222
189
  private _processAgentEvent;
223
- /** Resolve the pending retry promise */
224
- private _resolveRetry;
225
190
  /** Extract text content from a message */
226
191
  private _getUserMessageText;
227
192
  /** Find the last assistant message in agent state (including aborted ones) */
@@ -442,7 +407,6 @@ export declare class AgentSession {
442
407
  * Saves to settings.
443
408
  */
444
409
  setFollowUpMode(mode: "all" | "one-at-a-time"): void;
445
- private _applyCompaction;
446
410
  /**
447
411
  * Manually compact the session context.
448
412
  * Aborts current agent operation first.
@@ -457,8 +421,6 @@ export declare class AgentSession {
457
421
  * Cancel in-progress branch summarization.
458
422
  */
459
423
  abortBranchSummary(): void;
460
- private _checkCompaction;
461
- private _runAutoCompaction;
462
424
  /**
463
425
  * Toggle auto-compaction setting.
464
426
  */
@@ -475,17 +437,10 @@ export declare class AgentSession {
475
437
  private _refreshToolRegistry;
476
438
  private _buildRuntime;
477
439
  reload(): Promise<void>;
478
- /**
479
- * Check if an error is retryable (overloaded, rate limit, server errors).
480
- * Context overflow errors are NOT retryable (handled by compaction instead).
481
- */
482
- private _isRetryableError;
483
- private _handleRetryableError;
484
440
  /**
485
441
  * Cancel in-progress retry.
486
442
  */
487
443
  abortRetry(): void;
488
- private waitForRetry;
489
444
  /** Whether auto-retry is currently in progress */
490
445
  get isRetrying(): boolean;
491
446
  /** Whether auto-retry is enabled */
@@ -541,17 +496,7 @@ export declare class AgentSession {
541
496
  * @param options.label Label to attach to the branch summary entry
542
497
  * @returns Result with editorText (if user message) and cancelled status
543
498
  */
544
- navigateTree(targetId: string, options?: {
545
- summarize?: boolean;
546
- customInstructions?: string;
547
- replaceInstructions?: boolean;
548
- label?: string;
549
- }): Promise<{
550
- editorText?: string;
551
- cancelled: boolean;
552
- aborted?: boolean;
553
- summaryEntry?: BranchSummaryEntry;
554
- }>;
499
+ navigateTree(targetId: string, options?: NavigateTreeOptions): Promise<NavigateTreeResult>;
555
500
  /**
556
501
  * Get all user messages from session for fork selector.
557
502
  */
@@ -559,7 +504,6 @@ export declare class AgentSession {
559
504
  entryId: string;
560
505
  text: string;
561
506
  }>;
562
- private _extractUserMessageText;
563
507
  /**
564
508
  * Get session statistics.
565
509
  */