@kolisachint/hoocode-agent 0.4.107 → 0.4.109

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 (77) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/agent-session-compaction.d.ts +79 -0
  3. package/dist/core/agent-session-compaction.d.ts.map +1 -0
  4. package/dist/core/agent-session-compaction.js +346 -0
  5. package/dist/core/agent-session-compaction.js.map +1 -0
  6. package/dist/core/agent-session-retry.d.ts +76 -0
  7. package/dist/core/agent-session-retry.d.ts.map +1 -0
  8. package/dist/core/agent-session-retry.js +192 -0
  9. package/dist/core/agent-session-retry.js.map +1 -0
  10. package/dist/core/agent-session-skills.d.ts +34 -0
  11. package/dist/core/agent-session-skills.d.ts.map +1 -0
  12. package/dist/core/agent-session-skills.js +52 -0
  13. package/dist/core/agent-session-skills.js.map +1 -0
  14. package/dist/core/agent-session-stats.d.ts +74 -0
  15. package/dist/core/agent-session-stats.d.ts.map +1 -0
  16. package/dist/core/agent-session-stats.js +187 -0
  17. package/dist/core/agent-session-stats.js.map +1 -0
  18. package/dist/core/agent-session-tree-navigation.d.ts +69 -0
  19. package/dist/core/agent-session-tree-navigation.d.ts.map +1 -0
  20. package/dist/core/agent-session-tree-navigation.js +198 -0
  21. package/dist/core/agent-session-tree-navigation.js.map +1 -0
  22. package/dist/core/agent-session.d.ts +10 -66
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +96 -806
  25. package/dist/core/agent-session.js.map +1 -1
  26. package/dist/core/context-files.d.ts +25 -0
  27. package/dist/core/context-files.d.ts.map +1 -0
  28. package/dist/core/context-files.js +97 -0
  29. package/dist/core/context-files.js.map +1 -0
  30. package/dist/core/package-manager.d.ts.map +1 -1
  31. package/dist/core/package-manager.js +3 -519
  32. package/dist/core/package-manager.js.map +1 -1
  33. package/dist/core/package-resource-discovery.d.ts +62 -0
  34. package/dist/core/package-resource-discovery.d.ts.map +1 -0
  35. package/dist/core/package-resource-discovery.js +530 -0
  36. package/dist/core/package-resource-discovery.js.map +1 -0
  37. package/dist/core/resource-loader.d.ts +1 -10
  38. package/dist/core/resource-loader.d.ts.map +1 -1
  39. package/dist/core/resource-loader.js +4 -83
  40. package/dist/core/resource-loader.js.map +1 -1
  41. package/dist/core/settings-manager.d.ts +5 -140
  42. package/dist/core/settings-manager.d.ts.map +1 -1
  43. package/dist/core/settings-manager.js +4 -81
  44. package/dist/core/settings-manager.js.map +1 -1
  45. package/dist/core/settings-storage.d.ts +29 -0
  46. package/dist/core/settings-storage.d.ts.map +1 -0
  47. package/dist/core/settings-storage.js +90 -0
  48. package/dist/core/settings-storage.js.map +1 -0
  49. package/dist/core/settings-types.d.ts +128 -0
  50. package/dist/core/settings-types.d.ts.map +1 -0
  51. package/dist/core/settings-types.js +9 -0
  52. package/dist/core/settings-types.js.map +1 -0
  53. package/dist/modes/interactive/bash-execution-controller.d.ts +34 -0
  54. package/dist/modes/interactive/bash-execution-controller.d.ts.map +1 -0
  55. package/dist/modes/interactive/bash-execution-controller.js +98 -0
  56. package/dist/modes/interactive/bash-execution-controller.js.map +1 -0
  57. package/dist/modes/interactive/interactive-mode.d.ts +5 -45
  58. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  59. package/dist/modes/interactive/interactive-mode.js +91 -779
  60. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  61. package/dist/modes/interactive/login-controller.d.ts +50 -0
  62. package/dist/modes/interactive/login-controller.d.ts.map +1 -0
  63. package/dist/modes/interactive/login-controller.js +352 -0
  64. package/dist/modes/interactive/login-controller.js.map +1 -0
  65. package/dist/modes/interactive/message-queue-controller.d.ts +49 -0
  66. package/dist/modes/interactive/message-queue-controller.d.ts.map +1 -0
  67. package/dist/modes/interactive/message-queue-controller.js +184 -0
  68. package/dist/modes/interactive/message-queue-controller.js.map +1 -0
  69. package/dist/modes/interactive/model-controller.d.ts +43 -0
  70. package/dist/modes/interactive/model-controller.d.ts.map +1 -0
  71. package/dist/modes/interactive/model-controller.js +184 -0
  72. package/dist/modes/interactive/model-controller.js.map +1 -0
  73. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  74. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  75. package/examples/extensions/sandbox/package.json +1 -1
  76. package/examples/extensions/with-deps/package.json +1 -1
  77. package/package.json +4 -4
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Auto-retry controller for AgentSession.
3
+ *
4
+ * Owns the retry lifecycle for transient assistant errors (overloaded, rate
5
+ * limit, server/network/transport failures): deciding whether an error is
6
+ * retryable, arming the retry promise synchronously on agent_end, backing off
7
+ * exponentially, and re-driving the agent via continue(). Context-overflow
8
+ * errors are intentionally excluded here — those are handled by compaction.
9
+ */
10
+ import { isContextOverflow } from "@kolisachint/hoocode-ai";
11
+ import { sleep } from "../utils/sleep.js";
12
+ /**
13
+ * Retryable error signatures (overloaded, rate limit, server/network errors,
14
+ * transport closes). Compiled once at module load instead of on every assistant
15
+ * response. Context-overflow errors are handled separately by compaction.
16
+ */
17
+ const RETRYABLE_ERROR_PATTERN = /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
18
+ export class AutoRetryController {
19
+ deps;
20
+ _abortController = undefined;
21
+ _attempt = 0;
22
+ _promise = undefined;
23
+ _resolve = undefined;
24
+ constructor(deps) {
25
+ this.deps = deps;
26
+ }
27
+ /** Current retry attempt (0 if not retrying) */
28
+ get attempt() {
29
+ return this._attempt;
30
+ }
31
+ /** Whether a retry is currently in progress */
32
+ get isRetrying() {
33
+ return this._promise !== undefined;
34
+ }
35
+ /**
36
+ * Check if an error is retryable (overloaded, rate limit, server errors).
37
+ * Context overflow errors are NOT retryable (handled by compaction instead).
38
+ */
39
+ isRetryableError(message) {
40
+ if (message.stopReason !== "error" || !message.errorMessage)
41
+ return false;
42
+ // Context overflow is handled by compaction, not retry
43
+ const contextWindow = this.deps.getModel()?.contextWindow ?? 0;
44
+ if (isContextOverflow(message, contextWindow))
45
+ return false;
46
+ return RETRYABLE_ERROR_PATTERN.test(message.errorMessage);
47
+ }
48
+ /**
49
+ * Create the retry promise synchronously when an agent_end carries a
50
+ * retryable error. Agent.emit() runs handlers synchronously and prompt()
51
+ * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise
52
+ * here (rather than inside async event processing) ensures waitForRetry()
53
+ * never misses an in-flight retry.
54
+ */
55
+ createPromiseForAgentEnd(event) {
56
+ if (event.type !== "agent_end" || this._promise) {
57
+ return;
58
+ }
59
+ const settings = this.deps.getRetrySettings();
60
+ if (!settings.enabled) {
61
+ return;
62
+ }
63
+ const lastAssistant = this._findLastAssistantInMessages(event.messages);
64
+ if (!lastAssistant || !this.isRetryableError(lastAssistant)) {
65
+ return;
66
+ }
67
+ this._promise = new Promise((resolve) => {
68
+ this._resolve = resolve;
69
+ });
70
+ }
71
+ _findLastAssistantInMessages(messages) {
72
+ for (let i = messages.length - 1; i >= 0; i--) {
73
+ const message = messages[i];
74
+ if (message.role === "assistant") {
75
+ return message;
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * Reset the attempt counter after a successful assistant response.
82
+ * Callers invoke this only for non-error responses; it emits a success event
83
+ * when a retry was in progress.
84
+ */
85
+ onSuccessfulAssistantResponse() {
86
+ if (this._attempt > 0) {
87
+ this.deps.emit({
88
+ type: "auto_retry_end",
89
+ success: true,
90
+ attempt: this._attempt,
91
+ });
92
+ this._attempt = 0;
93
+ }
94
+ }
95
+ /** Resolve the pending retry promise */
96
+ resolve() {
97
+ if (this._resolve) {
98
+ this._resolve();
99
+ this._resolve = undefined;
100
+ this._promise = undefined;
101
+ }
102
+ }
103
+ /**
104
+ * Handle retryable errors with exponential backoff.
105
+ * @returns true if retry was initiated, false if max retries exceeded or disabled
106
+ */
107
+ async handleRetryableError(message) {
108
+ const settings = this.deps.getRetrySettings();
109
+ if (!settings.enabled) {
110
+ this.resolve();
111
+ return false;
112
+ }
113
+ // Retry promise is created synchronously in createPromiseForAgentEnd for agent_end.
114
+ // Keep a defensive fallback here in case a future refactor bypasses that path.
115
+ if (!this._promise) {
116
+ this._promise = new Promise((resolve) => {
117
+ this._resolve = resolve;
118
+ });
119
+ }
120
+ this._attempt++;
121
+ if (this._attempt > settings.maxRetries) {
122
+ // Max retries exceeded, emit final failure and reset
123
+ this.deps.emit({
124
+ type: "auto_retry_end",
125
+ success: false,
126
+ attempt: this._attempt - 1,
127
+ finalError: message.errorMessage,
128
+ });
129
+ this._attempt = 0;
130
+ this.resolve(); // Resolve so waitForRetry() completes
131
+ return false;
132
+ }
133
+ const delayMs = settings.baseDelayMs * 2 ** (this._attempt - 1);
134
+ this.deps.emit({
135
+ type: "auto_retry_start",
136
+ attempt: this._attempt,
137
+ maxAttempts: settings.maxRetries,
138
+ delayMs,
139
+ errorMessage: message.errorMessage || "Unknown error",
140
+ });
141
+ // Remove error message from agent state (keep in session for history)
142
+ const messages = this.deps.getAgentMessages();
143
+ if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
144
+ this.deps.setAgentMessages(messages.slice(0, -1));
145
+ }
146
+ // Wait with exponential backoff (abortable)
147
+ this._abortController = new AbortController();
148
+ try {
149
+ await sleep(delayMs, this._abortController.signal);
150
+ }
151
+ catch {
152
+ // Aborted during sleep - emit end event so UI can clean up
153
+ const attempt = this._attempt;
154
+ this._attempt = 0;
155
+ this._abortController = undefined;
156
+ this.deps.emit({
157
+ type: "auto_retry_end",
158
+ success: false,
159
+ attempt,
160
+ finalError: "Retry cancelled",
161
+ });
162
+ this.resolve();
163
+ return false;
164
+ }
165
+ this._abortController = undefined;
166
+ // Retry via continue() - use setTimeout to break out of event handler chain
167
+ setTimeout(() => {
168
+ this.deps.continueAgent();
169
+ }, 0);
170
+ return true;
171
+ }
172
+ /**
173
+ * Cancel in-progress retry.
174
+ */
175
+ abort() {
176
+ this._abortController?.abort();
177
+ // Note: _attempt is reset in the catch block of handleRetryableError
178
+ this.resolve();
179
+ }
180
+ /**
181
+ * Wait for any in-progress retry to complete.
182
+ * Returns immediately if no retry is in progress.
183
+ */
184
+ async waitForRetry() {
185
+ if (!this._promise) {
186
+ return;
187
+ }
188
+ await this._promise;
189
+ await this.deps.waitForAgentIdle();
190
+ }
191
+ }
192
+ //# sourceMappingURL=agent-session-retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-retry.js","sourceRoot":"","sources":["../../src/core/agent-session-retry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Auto-retry controller for AgentSession.\n *\n * Owns the retry lifecycle for transient assistant errors (overloaded, rate\n * limit, server/network/transport failures): deciding whether an error is\n * retryable, arming the retry promise synchronously on agent_end, backing off\n * exponentially, and re-driving the agent via continue(). Context-overflow\n * errors are intentionally excluded here — those are handled by compaction.\n */\n\nimport type { AgentEvent, AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow } from \"@kolisachint/hoocode-ai\";\nimport { sleep } from \"../utils/sleep.js\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\n\n/**\n * Retryable error signatures (overloaded, rate limit, server/network errors,\n * transport closes). Compiled once at module load instead of on every assistant\n * response. Context-overflow errors are handled separately by compaction.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;\n\n/** Narrow dependencies the retry controller needs from AgentSession. */\nexport interface AutoRetryDeps {\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number };\n\tgetModel(): Model<any> | undefined;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\t/** Fire-and-forget continue() on the agent (errors surface on the next agent_end). */\n\tcontinueAgent(): void;\n\twaitForAgentIdle(): Promise<void>;\n\temit(event: AgentSessionEvent): void;\n}\n\nexport class AutoRetryController {\n\tprivate _abortController: AbortController | undefined = undefined;\n\tprivate _attempt = 0;\n\tprivate _promise: Promise<void> | undefined = undefined;\n\tprivate _resolve: (() => void) | undefined = undefined;\n\n\tconstructor(private readonly deps: AutoRetryDeps) {}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** Whether a retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._promise !== undefined;\n\t}\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tisRetryableError(message: AssistantMessage): boolean {\n\t\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\n\t\t// Context overflow is handled by compaction, not retry\n\t\tconst contextWindow = this.deps.getModel()?.contextWindow ?? 0;\n\t\tif (isContextOverflow(message, contextWindow)) return false;\n\n\t\treturn RETRYABLE_ERROR_PATTERN.test(message.errorMessage);\n\t}\n\n\t/**\n\t * Create the retry promise synchronously when an agent_end carries a\n\t * retryable error. Agent.emit() runs handlers synchronously and prompt()\n\t * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise\n\t * here (rather than inside async event processing) ensures waitForRetry()\n\t * never misses an in-flight retry.\n\t */\n\tcreatePromiseForAgentEnd(event: AgentEvent): void {\n\t\tif (event.type !== \"agent_end\" || this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\t\tif (!lastAssistant || !this.isRetryableError(lastAssistant)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._promise = new Promise((resolve) => {\n\t\t\tthis._resolve = resolve;\n\t\t});\n\t}\n\n\tprivate _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\treturn message as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Reset the attempt counter after a successful assistant response.\n\t * Callers invoke this only for non-error responses; it emits a success event\n\t * when a retry was in progress.\n\t */\n\tonSuccessfulAssistantResponse(): void {\n\t\tif (this._attempt > 0) {\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: true,\n\t\t\t\tattempt: this._attempt,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t}\n\t}\n\n\t/** Resolve the pending retry promise */\n\tresolve(): void {\n\t\tif (this._resolve) {\n\t\t\tthis._resolve();\n\t\t\tthis._resolve = undefined;\n\t\t\tthis._promise = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Handle retryable errors with exponential backoff.\n\t * @returns true if retry was initiated, false if max retries exceeded or disabled\n\t */\n\tasync handleRetryableError(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\n\t\t// Retry promise is created synchronously in createPromiseForAgentEnd for agent_end.\n\t\t// Keep a defensive fallback here in case a future refactor bypasses that path.\n\t\tif (!this._promise) {\n\t\t\tthis._promise = new Promise((resolve) => {\n\t\t\t\tthis._resolve = resolve;\n\t\t\t});\n\t\t}\n\n\t\tthis._attempt++;\n\n\t\tif (this._attempt > settings.maxRetries) {\n\t\t\t// Max retries exceeded, emit final failure and reset\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._attempt - 1,\n\t\t\t\tfinalError: message.errorMessage,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t\tthis.resolve(); // Resolve so waitForRetry() completes\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = settings.baseDelayMs * 2 ** (this._attempt - 1);\n\n\t\tthis.deps.emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: settings.maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.deps.getAgentMessages();\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._abortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis._abortController = undefined;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\t\tthis._abortController = undefined;\n\n\t\t// Retry via continue() - use setTimeout to break out of event handler chain\n\t\tsetTimeout(() => {\n\t\t\tthis.deps.continueAgent();\n\t\t}, 0);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t\t// Note: _attempt is reset in the catch block of handleRetryableError\n\t\tthis.resolve();\n\t}\n\n\t/**\n\t * Wait for any in-progress retry to complete.\n\t * Returns immediately if no retry is in progress.\n\t */\n\tasync waitForRetry(): Promise<void> {\n\t\tif (!this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._promise;\n\t\tawait this.deps.waitForAgentIdle();\n\t}\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Skill-block parsing and inline skill-command expansion for AgentSession.
3
+ *
4
+ * `parseSkillBlock` recognizes the `<skill …>` envelope embedded in user
5
+ * messages; `expandSkillCommand` turns a `/skill:name args` invocation into that
6
+ * envelope by reading the skill file. Both are pure aside from the file read,
7
+ * which reports failures through the supplied error callback.
8
+ */
9
+ import type { Skill } from "./skills.js";
10
+ /** Parsed skill block from a user message */
11
+ export interface ParsedSkillBlock {
12
+ name: string;
13
+ location: string;
14
+ content: string;
15
+ userMessage: string | undefined;
16
+ }
17
+ /**
18
+ * Parse a skill block from message text.
19
+ * Returns null if the text doesn't contain a skill block.
20
+ */
21
+ export declare function parseSkillBlock(text: string): ParsedSkillBlock | null;
22
+ /** Error reported while reading a skill file during expansion. */
23
+ export interface SkillExpansionError {
24
+ filePath: string;
25
+ error: string;
26
+ }
27
+ /**
28
+ * Expand a skill command (`/skill:name args`) to its full skill-block content.
29
+ * Returns the expanded text, or the original text when it is not a skill command
30
+ * or the named skill is unknown. File-read failures are surfaced via `onError`
31
+ * and leave the original text unchanged.
32
+ */
33
+ export declare function expandSkillCommand(text: string, skills: Skill[], onError: (err: SkillExpansionError) => void): string;
34
+ //# sourceMappingURL=agent-session-skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-skills.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-skills.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CASrE;AAED,kEAAkE;AAClE,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,GAAG,MAAM,CAmBrH","sourcesContent":["/**\n * Skill-block parsing and inline skill-command expansion for AgentSession.\n *\n * `parseSkillBlock` recognizes the `<skill …>` envelope embedded in user\n * messages; `expandSkillCommand` turns a `/skill:name args` invocation into that\n * envelope by reading the skill file. Both are pure aside from the file read,\n * which reports failures through the supplied error callback.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { stripFrontmatter } from \"../utils/frontmatter.js\";\nimport type { Skill } from \"./skills.js\";\n\n/** Parsed skill block from a user message */\nexport interface ParsedSkillBlock {\n\tname: string;\n\tlocation: string;\n\tcontent: string;\n\tuserMessage: string | undefined;\n}\n\n/**\n * Parse a skill block from message text.\n * Returns null if the text doesn't contain a skill block.\n */\nexport function parseSkillBlock(text: string): ParsedSkillBlock | null {\n\tconst match = text.match(/^<skill name=\"([^\"]+)\" location=\"([^\"]+)\">\\n([\\s\\S]*?)\\n<\\/skill>(?:\\n\\n([\\s\\S]+))?$/);\n\tif (!match) return null;\n\treturn {\n\t\tname: match[1],\n\t\tlocation: match[2],\n\t\tcontent: match[3],\n\t\tuserMessage: match[4]?.trim() || undefined,\n\t};\n}\n\n/** Error reported while reading a skill file during expansion. */\nexport interface SkillExpansionError {\n\tfilePath: string;\n\terror: string;\n}\n\n/**\n * Expand a skill command (`/skill:name args`) to its full skill-block content.\n * Returns the expanded text, or the original text when it is not a skill command\n * or the named skill is unknown. File-read failures are surfaced via `onError`\n * and leave the original text unchanged.\n */\nexport function expandSkillCommand(text: string, skills: Skill[], onError: (err: SkillExpansionError) => void): string {\n\tif (!text.startsWith(\"/skill:\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);\n\tconst args = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1).trim();\n\n\tconst skill = skills.find((s) => s.name === skillName);\n\tif (!skill) return text; // Unknown skill, pass through\n\n\ttry {\n\t\tconst content = readFileSync(skill.filePath, \"utf-8\");\n\t\tconst body = stripFrontmatter(content).trim();\n\t\tconst skillBlock = `<skill name=\"${skill.name}\" location=\"${skill.filePath}\">\\nReferences are relative to ${skill.baseDir}.\\n\\n${body}\\n</skill>`;\n\t\treturn args ? `${skillBlock}\\n\\n${args}` : skillBlock;\n\t} catch (err) {\n\t\tonError({ filePath: skill.filePath, error: err instanceof Error ? err.message : String(err) });\n\t\treturn text; // Return original on error\n\t}\n}\n"]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Skill-block parsing and inline skill-command expansion for AgentSession.
3
+ *
4
+ * `parseSkillBlock` recognizes the `<skill …>` envelope embedded in user
5
+ * messages; `expandSkillCommand` turns a `/skill:name args` invocation into that
6
+ * envelope by reading the skill file. Both are pure aside from the file read,
7
+ * which reports failures through the supplied error callback.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { stripFrontmatter } from "../utils/frontmatter.js";
11
+ /**
12
+ * Parse a skill block from message text.
13
+ * Returns null if the text doesn't contain a skill block.
14
+ */
15
+ export function parseSkillBlock(text) {
16
+ const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);
17
+ if (!match)
18
+ return null;
19
+ return {
20
+ name: match[1],
21
+ location: match[2],
22
+ content: match[3],
23
+ userMessage: match[4]?.trim() || undefined,
24
+ };
25
+ }
26
+ /**
27
+ * Expand a skill command (`/skill:name args`) to its full skill-block content.
28
+ * Returns the expanded text, or the original text when it is not a skill command
29
+ * or the named skill is unknown. File-read failures are surfaced via `onError`
30
+ * and leave the original text unchanged.
31
+ */
32
+ export function expandSkillCommand(text, skills, onError) {
33
+ if (!text.startsWith("/skill:"))
34
+ return text;
35
+ const spaceIndex = text.indexOf(" ");
36
+ const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
37
+ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
38
+ const skill = skills.find((s) => s.name === skillName);
39
+ if (!skill)
40
+ return text; // Unknown skill, pass through
41
+ try {
42
+ const content = readFileSync(skill.filePath, "utf-8");
43
+ const body = stripFrontmatter(content).trim();
44
+ const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;
45
+ return args ? `${skillBlock}\n\n${args}` : skillBlock;
46
+ }
47
+ catch (err) {
48
+ onError({ filePath: skill.filePath, error: err instanceof Error ? err.message : String(err) });
49
+ return text; // Return original on error
50
+ }
51
+ }
52
+ //# sourceMappingURL=agent-session-skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-skills.js","sourceRoot":"","sources":["../../src/core/agent-session-skills.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAW3D;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAA2B;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,sFAAsF,CAAC,CAAC;IACjH,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO;QACN,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;QACd,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;QAClB,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QACjB,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS;KAC1C,CAAC;AAAA,CACF;AAQD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,MAAe,EAAE,OAA2C,EAAU;IACtH,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAE7C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,SAAS,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAChF,MAAM,IAAI,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAExE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IACvD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC,CAAC,8BAA8B;IAEvD,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,MAAM,UAAU,GAAG,gBAAgB,KAAK,CAAC,IAAI,eAAe,KAAK,CAAC,QAAQ,kCAAkC,KAAK,CAAC,OAAO,QAAQ,IAAI,YAAY,CAAC;QAClJ,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,UAAU,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;IACvD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/F,OAAO,IAAI,CAAC,CAAC,2BAA2B;IACzC,CAAC;AAAA,CACD","sourcesContent":["/**\n * Skill-block parsing and inline skill-command expansion for AgentSession.\n *\n * `parseSkillBlock` recognizes the `<skill …>` envelope embedded in user\n * messages; `expandSkillCommand` turns a `/skill:name args` invocation into that\n * envelope by reading the skill file. Both are pure aside from the file read,\n * which reports failures through the supplied error callback.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { stripFrontmatter } from \"../utils/frontmatter.js\";\nimport type { Skill } from \"./skills.js\";\n\n/** Parsed skill block from a user message */\nexport interface ParsedSkillBlock {\n\tname: string;\n\tlocation: string;\n\tcontent: string;\n\tuserMessage: string | undefined;\n}\n\n/**\n * Parse a skill block from message text.\n * Returns null if the text doesn't contain a skill block.\n */\nexport function parseSkillBlock(text: string): ParsedSkillBlock | null {\n\tconst match = text.match(/^<skill name=\"([^\"]+)\" location=\"([^\"]+)\">\\n([\\s\\S]*?)\\n<\\/skill>(?:\\n\\n([\\s\\S]+))?$/);\n\tif (!match) return null;\n\treturn {\n\t\tname: match[1],\n\t\tlocation: match[2],\n\t\tcontent: match[3],\n\t\tuserMessage: match[4]?.trim() || undefined,\n\t};\n}\n\n/** Error reported while reading a skill file during expansion. */\nexport interface SkillExpansionError {\n\tfilePath: string;\n\terror: string;\n}\n\n/**\n * Expand a skill command (`/skill:name args`) to its full skill-block content.\n * Returns the expanded text, or the original text when it is not a skill command\n * or the named skill is unknown. File-read failures are surfaced via `onError`\n * and leave the original text unchanged.\n */\nexport function expandSkillCommand(text: string, skills: Skill[], onError: (err: SkillExpansionError) => void): string {\n\tif (!text.startsWith(\"/skill:\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);\n\tconst args = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1).trim();\n\n\tconst skill = skills.find((s) => s.name === skillName);\n\tif (!skill) return text; // Unknown skill, pass through\n\n\ttry {\n\t\tconst content = readFileSync(skill.filePath, \"utf-8\");\n\t\tconst body = stripFrontmatter(content).trim();\n\t\tconst skillBlock = `<skill name=\"${skill.name}\" location=\"${skill.filePath}\">\\nReferences are relative to ${skill.baseDir}.\\n\\n${body}\\n</skill>`;\n\t\treturn args ? `${skillBlock}\\n\\n${args}` : skillBlock;\n\t} catch (err) {\n\t\tonError({ filePath: skill.filePath, error: err instanceof Error ? err.message : String(err) });\n\t\treturn text; // Return original on error\n\t}\n}\n"]}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pure helpers for AgentSession reporting and export.
3
+ *
4
+ * These functions derive statistics, context-usage, forkable user messages, and
5
+ * JSONL exports from session state without touching the live agent or extension
6
+ * runner. AgentSession delegates to them so the class stays focused on lifecycle
7
+ * and event wiring.
8
+ */
9
+ import type { AgentMessage } from "@kolisachint/hoocode-agent-core";
10
+ import type { Model } from "@kolisachint/hoocode-ai";
11
+ import type { ContextUsage } from "./extensions/index.js";
12
+ import type { SessionManager } from "./session-manager.js";
13
+ /** Session statistics for /session command */
14
+ export interface SessionStats {
15
+ sessionFile: string | undefined;
16
+ sessionId: string;
17
+ userMessages: number;
18
+ assistantMessages: number;
19
+ toolCalls: number;
20
+ toolResults: number;
21
+ totalMessages: number;
22
+ tokens: {
23
+ input: number;
24
+ output: number;
25
+ cacheRead: number;
26
+ cacheWrite: number;
27
+ total: number;
28
+ };
29
+ cost: number;
30
+ contextUsage?: ContextUsage;
31
+ }
32
+ /** Extract concatenated text from a user message content value. */
33
+ export declare function extractUserMessageText(content: string | Array<{
34
+ type: string;
35
+ text?: string;
36
+ }>): string;
37
+ /** Compute aggregate statistics (message counts, token usage, cost) for a session. */
38
+ export declare function computeSessionStats(params: {
39
+ messages: AgentMessage[];
40
+ sessionFile: string | undefined;
41
+ sessionId: string;
42
+ contextUsage: ContextUsage | undefined;
43
+ }): SessionStats;
44
+ /**
45
+ * Estimate current context-window usage.
46
+ *
47
+ * After compaction, the last assistant usage reflects pre-compaction context
48
+ * size, so usage is only trusted from an assistant that responded after the
49
+ * latest compaction boundary. When no such assistant exists yet, tokens are
50
+ * reported as null (unknown until the next LLM response).
51
+ */
52
+ export declare function computeContextUsage(params: {
53
+ model: Model<any> | undefined;
54
+ sessionManager: SessionManager;
55
+ messages: AgentMessage[];
56
+ }): ContextUsage | undefined;
57
+ /** Collect all user messages on the session (for the fork selector). */
58
+ export declare function collectUserMessagesForForking(sessionManager: SessionManager): Array<{
59
+ entryId: string;
60
+ text: string;
61
+ }>;
62
+ /**
63
+ * Get the text content of the last non-empty assistant message (for /copy).
64
+ * Returns undefined if no assistant message with text exists.
65
+ */
66
+ export declare function getLastAssistantText(messages: AgentMessage[]): string | undefined;
67
+ /**
68
+ * Export the current session branch to a JSONL file.
69
+ * Writes the session header followed by all entries on the current branch path,
70
+ * re-chaining parentIds into a linear sequence.
71
+ * @returns The resolved output file path.
72
+ */
73
+ export declare function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string;
74
+ //# sourceMappingURL=agent-session-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-stats.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-stats.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,OAAO,KAAK,EAAoB,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACd,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,YAAY,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CASvG;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC3C,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;CACvC,GAAG,YAAY,CA2Cf;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC3C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB,GAAG,YAAY,GAAG,SAAS,CAyC3B;AAED,wEAAwE;AACxE,wBAAgB,6BAA6B,CAC5C,cAAc,EAAE,cAAc,GAC5B,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAe1C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,GAAG,SAAS,CAsBjF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,cAAc,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CA4BtG","sourcesContent":["/**\n * Pure helpers for AgentSession reporting and export.\n *\n * These functions derive statistics, context-usage, forkable user messages, and\n * JSONL exports from session state without touching the live agent or extension\n * runner. AgentSession delegates to them so the class stays focused on lifecycle\n * and event wiring.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { calculateContextTokens, estimateContextTokens } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport type { ContextUsage } from \"./extensions/index.js\";\nimport type { SessionManager } from \"./session-manager.js\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.js\";\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\n/** Extract concatenated text from a user message content value. */\nexport function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\tif (typeof content === \"string\") return content;\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\t}\n\treturn \"\";\n}\n\n/** Compute aggregate statistics (message counts, token usage, cost) for a session. */\nexport function computeSessionStats(params: {\n\tmessages: AgentMessage[];\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tcontextUsage: ContextUsage | undefined;\n}): SessionStats {\n\tconst { messages, sessionFile, sessionId, contextUsage } = params;\n\tconst userMessages = messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\").length;\n\tconst toolResults = messages.filter((m) => m.role === \"toolResult\").length;\n\n\tlet toolCalls = 0;\n\tlet totalInput = 0;\n\tlet totalOutput = 0;\n\tlet totalCacheRead = 0;\n\tlet totalCacheWrite = 0;\n\tlet totalCost = 0;\n\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t}\n\t}\n\n\treturn {\n\t\tsessionFile,\n\t\tsessionId,\n\t\tuserMessages,\n\t\tassistantMessages,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\ttotalMessages: messages.length,\n\t\ttokens: {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t},\n\t\tcost: totalCost,\n\t\tcontextUsage,\n\t};\n}\n\n/**\n * Estimate current context-window usage.\n *\n * After compaction, the last assistant usage reflects pre-compaction context\n * size, so usage is only trusted from an assistant that responded after the\n * latest compaction boundary. When no such assistant exists yet, tokens are\n * reported as null (unknown until the next LLM response).\n */\nexport function computeContextUsage(params: {\n\tmodel: Model<any> | undefined;\n\tsessionManager: SessionManager;\n\tmessages: AgentMessage[];\n}): ContextUsage | undefined {\n\tconst { model, sessionManager, messages } = params;\n\tif (!model) return undefined;\n\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return undefined;\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\tif (latestCompaction) {\n\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\tlet hasPostCompactionUsage = false;\n\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\tconst entry = branchEntries[i];\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\tconst assistant = entry.message;\n\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!hasPostCompactionUsage) {\n\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t}\n\t}\n\n\tconst estimate = estimateContextTokens(messages);\n\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\treturn {\n\t\ttokens: estimate.tokens,\n\t\tcontextWindow,\n\t\tpercent,\n\t};\n}\n\n/** Collect all user messages on the session (for the fork selector). */\nexport function collectUserMessagesForForking(\n\tsessionManager: SessionManager,\n): Array<{ entryId: string; text: string }> {\n\tconst entries = sessionManager.getEntries();\n\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tif (entry.message.role !== \"user\") continue;\n\n\t\tconst text = extractUserMessageText(entry.message.content);\n\t\tif (text) {\n\t\t\tresult.push({ entryId: entry.id, text });\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get the text content of the last non-empty assistant message (for /copy).\n * Returns undefined if no assistant message with text exists.\n */\nexport function getLastAssistantText(messages: AgentMessage[]): string | undefined {\n\tconst lastAssistant = messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => {\n\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\tconst msg = m as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\treturn true;\n\t\t});\n\n\tif (!lastAssistant) return undefined;\n\n\tlet text = \"\";\n\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\tif (content.type === \"text\") {\n\t\t\ttext += content.text;\n\t\t}\n\t}\n\n\treturn text.trim() || undefined;\n}\n\n/**\n * Export the current session branch to a JSONL file.\n * Writes the session header followed by all entries on the current branch path,\n * re-chaining parentIds into a linear sequence.\n * @returns The resolved output file path.\n */\nexport function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string {\n\tconst filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`);\n\tconst dir = dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\n\tconst header: SessionHeader = {\n\t\ttype: \"session\",\n\t\tversion: CURRENT_SESSION_VERSION,\n\t\tid: sessionManager.getSessionId(),\n\t\ttimestamp: new Date().toISOString(),\n\t\tcwd: sessionManager.getCwd(),\n\t};\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst lines = [JSON.stringify(header)];\n\n\t// Re-chain parentIds to form a linear sequence\n\tlet prevId: string | null = null;\n\tfor (const entry of branchEntries) {\n\t\tconst linear = { ...entry, parentId: prevId };\n\t\tlines.push(JSON.stringify(linear));\n\t\tprevId = entry.id;\n\t}\n\n\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\treturn filePath;\n}\n"]}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Pure helpers for AgentSession reporting and export.
3
+ *
4
+ * These functions derive statistics, context-usage, forkable user messages, and
5
+ * JSONL exports from session state without touching the live agent or extension
6
+ * runner. AgentSession delegates to them so the class stays focused on lifecycle
7
+ * and event wiring.
8
+ */
9
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { calculateContextTokens, estimateContextTokens } from "@kolisachint/hoocode-agent-core";
12
+ import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
13
+ /** Extract concatenated text from a user message content value. */
14
+ export function extractUserMessageText(content) {
15
+ if (typeof content === "string")
16
+ return content;
17
+ if (Array.isArray(content)) {
18
+ return content
19
+ .filter((c) => c.type === "text")
20
+ .map((c) => c.text)
21
+ .join("");
22
+ }
23
+ return "";
24
+ }
25
+ /** Compute aggregate statistics (message counts, token usage, cost) for a session. */
26
+ export function computeSessionStats(params) {
27
+ const { messages, sessionFile, sessionId, contextUsage } = params;
28
+ const userMessages = messages.filter((m) => m.role === "user").length;
29
+ const assistantMessages = messages.filter((m) => m.role === "assistant").length;
30
+ const toolResults = messages.filter((m) => m.role === "toolResult").length;
31
+ let toolCalls = 0;
32
+ let totalInput = 0;
33
+ let totalOutput = 0;
34
+ let totalCacheRead = 0;
35
+ let totalCacheWrite = 0;
36
+ let totalCost = 0;
37
+ for (const message of messages) {
38
+ if (message.role === "assistant") {
39
+ const assistantMsg = message;
40
+ toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
41
+ totalInput += assistantMsg.usage.input;
42
+ totalOutput += assistantMsg.usage.output;
43
+ totalCacheRead += assistantMsg.usage.cacheRead;
44
+ totalCacheWrite += assistantMsg.usage.cacheWrite;
45
+ totalCost += assistantMsg.usage.cost.total;
46
+ }
47
+ }
48
+ return {
49
+ sessionFile,
50
+ sessionId,
51
+ userMessages,
52
+ assistantMessages,
53
+ toolCalls,
54
+ toolResults,
55
+ totalMessages: messages.length,
56
+ tokens: {
57
+ input: totalInput,
58
+ output: totalOutput,
59
+ cacheRead: totalCacheRead,
60
+ cacheWrite: totalCacheWrite,
61
+ total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
62
+ },
63
+ cost: totalCost,
64
+ contextUsage,
65
+ };
66
+ }
67
+ /**
68
+ * Estimate current context-window usage.
69
+ *
70
+ * After compaction, the last assistant usage reflects pre-compaction context
71
+ * size, so usage is only trusted from an assistant that responded after the
72
+ * latest compaction boundary. When no such assistant exists yet, tokens are
73
+ * reported as null (unknown until the next LLM response).
74
+ */
75
+ export function computeContextUsage(params) {
76
+ const { model, sessionManager, messages } = params;
77
+ if (!model)
78
+ return undefined;
79
+ const contextWindow = model.contextWindow ?? 0;
80
+ if (contextWindow <= 0)
81
+ return undefined;
82
+ const branchEntries = sessionManager.getBranch();
83
+ const latestCompaction = getLatestCompactionEntry(branchEntries);
84
+ if (latestCompaction) {
85
+ // Check if there's a valid assistant usage after the compaction boundary
86
+ const compactionIndex = branchEntries.lastIndexOf(latestCompaction);
87
+ let hasPostCompactionUsage = false;
88
+ for (let i = branchEntries.length - 1; i > compactionIndex; i--) {
89
+ const entry = branchEntries[i];
90
+ if (entry.type === "message" && entry.message.role === "assistant") {
91
+ const assistant = entry.message;
92
+ if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error") {
93
+ const contextTokens = calculateContextTokens(assistant.usage);
94
+ if (contextTokens > 0) {
95
+ hasPostCompactionUsage = true;
96
+ }
97
+ break;
98
+ }
99
+ }
100
+ }
101
+ if (!hasPostCompactionUsage) {
102
+ return { tokens: null, contextWindow, percent: null };
103
+ }
104
+ }
105
+ const estimate = estimateContextTokens(messages);
106
+ const percent = (estimate.tokens / contextWindow) * 100;
107
+ return {
108
+ tokens: estimate.tokens,
109
+ contextWindow,
110
+ percent,
111
+ };
112
+ }
113
+ /** Collect all user messages on the session (for the fork selector). */
114
+ export function collectUserMessagesForForking(sessionManager) {
115
+ const entries = sessionManager.getEntries();
116
+ const result = [];
117
+ for (const entry of entries) {
118
+ if (entry.type !== "message")
119
+ continue;
120
+ if (entry.message.role !== "user")
121
+ continue;
122
+ const text = extractUserMessageText(entry.message.content);
123
+ if (text) {
124
+ result.push({ entryId: entry.id, text });
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+ /**
130
+ * Get the text content of the last non-empty assistant message (for /copy).
131
+ * Returns undefined if no assistant message with text exists.
132
+ */
133
+ export function getLastAssistantText(messages) {
134
+ const lastAssistant = messages
135
+ .slice()
136
+ .reverse()
137
+ .find((m) => {
138
+ if (m.role !== "assistant")
139
+ return false;
140
+ const msg = m;
141
+ // Skip aborted messages with no content
142
+ if (msg.stopReason === "aborted" && msg.content.length === 0)
143
+ return false;
144
+ return true;
145
+ });
146
+ if (!lastAssistant)
147
+ return undefined;
148
+ let text = "";
149
+ for (const content of lastAssistant.content) {
150
+ if (content.type === "text") {
151
+ text += content.text;
152
+ }
153
+ }
154
+ return text.trim() || undefined;
155
+ }
156
+ /**
157
+ * Export the current session branch to a JSONL file.
158
+ * Writes the session header followed by all entries on the current branch path,
159
+ * re-chaining parentIds into a linear sequence.
160
+ * @returns The resolved output file path.
161
+ */
162
+ export function exportSessionBranchToJsonl(sessionManager, outputPath) {
163
+ const filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
164
+ const dir = dirname(filePath);
165
+ if (!existsSync(dir)) {
166
+ mkdirSync(dir, { recursive: true });
167
+ }
168
+ const header = {
169
+ type: "session",
170
+ version: CURRENT_SESSION_VERSION,
171
+ id: sessionManager.getSessionId(),
172
+ timestamp: new Date().toISOString(),
173
+ cwd: sessionManager.getCwd(),
174
+ };
175
+ const branchEntries = sessionManager.getBranch();
176
+ const lines = [JSON.stringify(header)];
177
+ // Re-chain parentIds to form a linear sequence
178
+ let prevId = null;
179
+ for (const entry of branchEntries) {
180
+ const linear = { ...entry, parentId: prevId };
181
+ lines.push(JSON.stringify(linear));
182
+ prevId = entry.id;
183
+ }
184
+ writeFileSync(filePath, `${lines.join("\n")}\n`);
185
+ return filePath;
186
+ }
187
+ //# sourceMappingURL=agent-session-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-stats.js","sourceRoot":"","sources":["../../src/core/agent-session-stats.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure helpers for AgentSession reporting and export.\n *\n * These functions derive statistics, context-usage, forkable user messages, and\n * JSONL exports from session state without touching the live agent or extension\n * runner. AgentSession delegates to them so the class stays focused on lifecycle\n * and event wiring.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { calculateContextTokens, estimateContextTokens } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport type { ContextUsage } from \"./extensions/index.js\";\nimport type { SessionManager } from \"./session-manager.js\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.js\";\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\n/** Extract concatenated text from a user message content value. */\nexport function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\tif (typeof content === \"string\") return content;\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\t}\n\treturn \"\";\n}\n\n/** Compute aggregate statistics (message counts, token usage, cost) for a session. */\nexport function computeSessionStats(params: {\n\tmessages: AgentMessage[];\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tcontextUsage: ContextUsage | undefined;\n}): SessionStats {\n\tconst { messages, sessionFile, sessionId, contextUsage } = params;\n\tconst userMessages = messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\").length;\n\tconst toolResults = messages.filter((m) => m.role === \"toolResult\").length;\n\n\tlet toolCalls = 0;\n\tlet totalInput = 0;\n\tlet totalOutput = 0;\n\tlet totalCacheRead = 0;\n\tlet totalCacheWrite = 0;\n\tlet totalCost = 0;\n\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t}\n\t}\n\n\treturn {\n\t\tsessionFile,\n\t\tsessionId,\n\t\tuserMessages,\n\t\tassistantMessages,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\ttotalMessages: messages.length,\n\t\ttokens: {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t},\n\t\tcost: totalCost,\n\t\tcontextUsage,\n\t};\n}\n\n/**\n * Estimate current context-window usage.\n *\n * After compaction, the last assistant usage reflects pre-compaction context\n * size, so usage is only trusted from an assistant that responded after the\n * latest compaction boundary. When no such assistant exists yet, tokens are\n * reported as null (unknown until the next LLM response).\n */\nexport function computeContextUsage(params: {\n\tmodel: Model<any> | undefined;\n\tsessionManager: SessionManager;\n\tmessages: AgentMessage[];\n}): ContextUsage | undefined {\n\tconst { model, sessionManager, messages } = params;\n\tif (!model) return undefined;\n\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return undefined;\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\tif (latestCompaction) {\n\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\tlet hasPostCompactionUsage = false;\n\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\tconst entry = branchEntries[i];\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\tconst assistant = entry.message;\n\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!hasPostCompactionUsage) {\n\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t}\n\t}\n\n\tconst estimate = estimateContextTokens(messages);\n\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\treturn {\n\t\ttokens: estimate.tokens,\n\t\tcontextWindow,\n\t\tpercent,\n\t};\n}\n\n/** Collect all user messages on the session (for the fork selector). */\nexport function collectUserMessagesForForking(\n\tsessionManager: SessionManager,\n): Array<{ entryId: string; text: string }> {\n\tconst entries = sessionManager.getEntries();\n\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tif (entry.message.role !== \"user\") continue;\n\n\t\tconst text = extractUserMessageText(entry.message.content);\n\t\tif (text) {\n\t\t\tresult.push({ entryId: entry.id, text });\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get the text content of the last non-empty assistant message (for /copy).\n * Returns undefined if no assistant message with text exists.\n */\nexport function getLastAssistantText(messages: AgentMessage[]): string | undefined {\n\tconst lastAssistant = messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => {\n\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\tconst msg = m as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\treturn true;\n\t\t});\n\n\tif (!lastAssistant) return undefined;\n\n\tlet text = \"\";\n\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\tif (content.type === \"text\") {\n\t\t\ttext += content.text;\n\t\t}\n\t}\n\n\treturn text.trim() || undefined;\n}\n\n/**\n * Export the current session branch to a JSONL file.\n * Writes the session header followed by all entries on the current branch path,\n * re-chaining parentIds into a linear sequence.\n * @returns The resolved output file path.\n */\nexport function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string {\n\tconst filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`);\n\tconst dir = dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\n\tconst header: SessionHeader = {\n\t\ttype: \"session\",\n\t\tversion: CURRENT_SESSION_VERSION,\n\t\tid: sessionManager.getSessionId(),\n\t\ttimestamp: new Date().toISOString(),\n\t\tcwd: sessionManager.getCwd(),\n\t};\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst lines = [JSON.stringify(header)];\n\n\t// Re-chain parentIds to form a linear sequence\n\tlet prevId: string | null = null;\n\tfor (const entry of branchEntries) {\n\t\tconst linear = { ...entry, parentId: prevId };\n\t\tlines.push(JSON.stringify(linear));\n\t\tprevId = entry.id;\n\t}\n\n\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\treturn filePath;\n}\n"]}