praana 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/praana.js +17 -0
  4. package/bin/pran.js +17 -0
  5. package/dist/app-banner.d.ts +11 -0
  6. package/dist/app-banner.js +161 -0
  7. package/dist/app-controller.d.ts +44 -0
  8. package/dist/app-controller.js +143 -0
  9. package/dist/app-identity.d.ts +18 -0
  10. package/dist/app-identity.js +52 -0
  11. package/dist/auto-compact.d.ts +16 -0
  12. package/dist/auto-compact.js +101 -0
  13. package/dist/cli-args.d.ts +14 -0
  14. package/dist/cli-args.js +69 -0
  15. package/dist/compile-classic.d.ts +21 -0
  16. package/dist/compile-classic.js +106 -0
  17. package/dist/compiler.d.ts +75 -0
  18. package/dist/compiler.js +406 -0
  19. package/dist/config.d.ts +3 -0
  20. package/dist/config.js +433 -0
  21. package/dist/context-engine/activity-log.d.ts +9 -0
  22. package/dist/context-engine/activity-log.js +109 -0
  23. package/dist/context-engine/artifact-store.d.ts +32 -0
  24. package/dist/context-engine/artifact-store.js +272 -0
  25. package/dist/context-engine/bm25.d.ts +3 -0
  26. package/dist/context-engine/bm25.js +32 -0
  27. package/dist/context-engine/checkpoint.d.ts +34 -0
  28. package/dist/context-engine/checkpoint.js +430 -0
  29. package/dist/context-engine/classify.d.ts +3 -0
  30. package/dist/context-engine/classify.js +60 -0
  31. package/dist/context-engine/db.d.ts +73 -0
  32. package/dist/context-engine/db.js +505 -0
  33. package/dist/context-engine/distiller.d.ts +30 -0
  34. package/dist/context-engine/distiller.js +67 -0
  35. package/dist/context-engine/engine-compiler.d.ts +23 -0
  36. package/dist/context-engine/engine-compiler.js +297 -0
  37. package/dist/context-engine/error-tracker.d.ts +21 -0
  38. package/dist/context-engine/error-tracker.js +74 -0
  39. package/dist/context-engine/event-lineage.d.ts +26 -0
  40. package/dist/context-engine/event-lineage.js +120 -0
  41. package/dist/context-engine/extraction.d.ts +26 -0
  42. package/dist/context-engine/extraction.js +83 -0
  43. package/dist/context-engine/index.d.ts +82 -0
  44. package/dist/context-engine/index.js +238 -0
  45. package/dist/context-engine/scoring.d.ts +13 -0
  46. package/dist/context-engine/scoring.js +47 -0
  47. package/dist/context-engine/state-snapshot.d.ts +8 -0
  48. package/dist/context-engine/state-snapshot.js +50 -0
  49. package/dist/context-engine/summarize.d.ts +6 -0
  50. package/dist/context-engine/summarize.js +32 -0
  51. package/dist/context-engine/telemetry.d.ts +25 -0
  52. package/dist/context-engine/telemetry.js +64 -0
  53. package/dist/context-engine/turn-digest.d.ts +50 -0
  54. package/dist/context-engine/turn-digest.js +250 -0
  55. package/dist/context-engine/turn-ledger.d.ts +18 -0
  56. package/dist/context-engine/turn-ledger.js +184 -0
  57. package/dist/context-engine/turn-recorder.d.ts +24 -0
  58. package/dist/context-engine/turn-recorder.js +88 -0
  59. package/dist/context-engine/types.d.ts +201 -0
  60. package/dist/context-engine/types.js +4 -0
  61. package/dist/context-pressure.d.ts +19 -0
  62. package/dist/context-pressure.js +36 -0
  63. package/dist/distillers/generic.d.ts +14 -0
  64. package/dist/distillers/generic.js +93 -0
  65. package/dist/distillers/git-diff.d.ts +8 -0
  66. package/dist/distillers/git-diff.js +119 -0
  67. package/dist/distillers/index.d.ts +2 -0
  68. package/dist/distillers/index.js +16 -0
  69. package/dist/distillers/npm-test.d.ts +8 -0
  70. package/dist/distillers/npm-test.js +50 -0
  71. package/dist/distillers/rg-results.d.ts +8 -0
  72. package/dist/distillers/rg-results.js +28 -0
  73. package/dist/distillers/tsc-errors.d.ts +8 -0
  74. package/dist/distillers/tsc-errors.js +52 -0
  75. package/dist/event-log.d.ts +56 -0
  76. package/dist/event-log.js +214 -0
  77. package/dist/llm.d.ts +29 -0
  78. package/dist/llm.js +155 -0
  79. package/dist/logger.d.ts +94 -0
  80. package/dist/logger.js +287 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +54 -0
  83. package/dist/memory/confidence.d.ts +7 -0
  84. package/dist/memory/confidence.js +37 -0
  85. package/dist/memory/consolidation.d.ts +26 -0
  86. package/dist/memory/consolidation.js +166 -0
  87. package/dist/memory/db.d.ts +40 -0
  88. package/dist/memory/db.js +283 -0
  89. package/dist/memory/dedup.d.ts +6 -0
  90. package/dist/memory/dedup.js +50 -0
  91. package/dist/memory/embedder-factory.d.ts +3 -0
  92. package/dist/memory/embedder-factory.js +81 -0
  93. package/dist/memory/embeddings.d.ts +15 -0
  94. package/dist/memory/embeddings.js +67 -0
  95. package/dist/memory/index.d.ts +9 -0
  96. package/dist/memory/index.js +11 -0
  97. package/dist/memory/ollama-summarizer.d.ts +19 -0
  98. package/dist/memory/ollama-summarizer.js +72 -0
  99. package/dist/memory/openai-summarizer.d.ts +21 -0
  100. package/dist/memory/openai-summarizer.js +51 -0
  101. package/dist/memory/store.d.ts +61 -0
  102. package/dist/memory/store.js +502 -0
  103. package/dist/memory/summarizer-factory.d.ts +3 -0
  104. package/dist/memory/summarizer-factory.js +69 -0
  105. package/dist/memory/summarizer.d.ts +4 -0
  106. package/dist/memory/summarizer.js +112 -0
  107. package/dist/memory/types.d.ts +87 -0
  108. package/dist/memory/types.js +17 -0
  109. package/dist/model-context.d.ts +15 -0
  110. package/dist/model-context.js +212 -0
  111. package/dist/project-detector.d.ts +37 -0
  112. package/dist/project-detector.js +604 -0
  113. package/dist/render.d.ts +15 -0
  114. package/dist/render.js +46 -0
  115. package/dist/session.d.ts +118 -0
  116. package/dist/session.js +809 -0
  117. package/dist/skills/index.d.ts +69 -0
  118. package/dist/skills/index.js +885 -0
  119. package/dist/skills/types.d.ts +93 -0
  120. package/dist/skills/types.js +8 -0
  121. package/dist/slash-commands.d.ts +14 -0
  122. package/dist/slash-commands.js +301 -0
  123. package/dist/state-graph.d.ts +38 -0
  124. package/dist/state-graph.js +255 -0
  125. package/dist/status-bar.d.ts +54 -0
  126. package/dist/status-bar.js +184 -0
  127. package/dist/thinking-display.d.ts +21 -0
  128. package/dist/thinking-display.js +37 -0
  129. package/dist/tool-summary.d.ts +4 -0
  130. package/dist/tool-summary.js +67 -0
  131. package/dist/tools/index.d.ts +925 -0
  132. package/dist/tools/index.js +86 -0
  133. package/dist/tools/knowledge.d.ts +140 -0
  134. package/dist/tools/knowledge.js +260 -0
  135. package/dist/tools/memory.d.ts +39 -0
  136. package/dist/tools/memory.js +300 -0
  137. package/dist/tools/search-code.d.ts +134 -0
  138. package/dist/tools/search-code.js +390 -0
  139. package/dist/tools/system.d.ts +16 -0
  140. package/dist/tools/system.js +499 -0
  141. package/dist/tools/tool-def.d.ts +6 -0
  142. package/dist/tools/tool-def.js +3 -0
  143. package/dist/turn-control.d.ts +51 -0
  144. package/dist/turn-control.js +210 -0
  145. package/dist/turn.d.ts +20 -0
  146. package/dist/turn.js +624 -0
  147. package/dist/types.d.ts +233 -0
  148. package/dist/types.js +4 -0
  149. package/dist/ui/readline-ui.d.ts +2 -0
  150. package/dist/ui/readline-ui.js +176 -0
  151. package/dist/ui/tui/app.d.ts +13 -0
  152. package/dist/ui/tui/app.js +270 -0
  153. package/dist/ui/tui/busy-indicator.d.ts +2 -0
  154. package/dist/ui/tui/busy-indicator.js +13 -0
  155. package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
  156. package/dist/ui/tui/components/gutter-rule.js +9 -0
  157. package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
  158. package/dist/ui/tui/components/inline-tool-row.js +8 -0
  159. package/dist/ui/tui/components/prompt-input.d.ts +20 -0
  160. package/dist/ui/tui/components/prompt-input.js +120 -0
  161. package/dist/ui/tui/components/system-line.d.ts +5 -0
  162. package/dist/ui/tui/components/system-line.js +6 -0
  163. package/dist/ui/tui/components/thinking-block.d.ts +11 -0
  164. package/dist/ui/tui/components/thinking-block.js +31 -0
  165. package/dist/ui/tui/components/toast-line.d.ts +4 -0
  166. package/dist/ui/tui/components/toast-line.js +8 -0
  167. package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
  168. package/dist/ui/tui/components/tool-result-line.js +6 -0
  169. package/dist/ui/tui/components/turn-footer.d.ts +5 -0
  170. package/dist/ui/tui/components/turn-footer.js +7 -0
  171. package/dist/ui/tui/components/user-block.d.ts +6 -0
  172. package/dist/ui/tui/components/user-block.js +6 -0
  173. package/dist/ui/tui/logo-banner.d.ts +5 -0
  174. package/dist/ui/tui/logo-banner.js +8 -0
  175. package/dist/ui/tui/markdown-render.d.ts +16 -0
  176. package/dist/ui/tui/markdown-render.js +218 -0
  177. package/dist/ui/tui/palette.d.ts +12 -0
  178. package/dist/ui/tui/palette.js +13 -0
  179. package/dist/ui/tui/reasoning-summary.d.ts +12 -0
  180. package/dist/ui/tui/reasoning-summary.js +27 -0
  181. package/dist/ui/tui/reducer.d.ts +92 -0
  182. package/dist/ui/tui/reducer.js +260 -0
  183. package/dist/ui/tui/run.d.ts +3 -0
  184. package/dist/ui/tui/run.js +40 -0
  185. package/dist/ui/tui/sink.d.ts +4 -0
  186. package/dist/ui/tui/sink.js +89 -0
  187. package/dist/ui/tui/status-bar-view.d.ts +5 -0
  188. package/dist/ui/tui/status-bar-view.js +44 -0
  189. package/dist/ui/tui/terminal-height.d.ts +12 -0
  190. package/dist/ui/tui/terminal-height.js +20 -0
  191. package/dist/ui/tui/terminal-width.d.ts +2 -0
  192. package/dist/ui/tui/terminal-width.js +5 -0
  193. package/dist/ui/tui/tool-display.d.ts +23 -0
  194. package/dist/ui/tui/tool-display.js +217 -0
  195. package/dist/ui/tui/transcript-line.d.ts +12 -0
  196. package/dist/ui/tui/transcript-line.js +43 -0
  197. package/dist/ui/tui/transcript-replay.d.ts +12 -0
  198. package/dist/ui/tui/transcript-replay.js +117 -0
  199. package/dist/ui-events.d.ts +39 -0
  200. package/dist/ui-events.js +33 -0
  201. package/dist/ui.d.ts +77 -0
  202. package/dist/ui.js +179 -0
  203. package/package.json +73 -0
  204. package/praana.config.example.toml +231 -0
package/dist/turn.js ADDED
@@ -0,0 +1,624 @@
1
+ import { stream as piStream } from "@earendil-works/pi-ai";
2
+ import { appendFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { resolveDefaultSessionLogDir } from "./app-identity.js";
5
+ import { zodToJsonSchema } from "zod-to-json-schema";
6
+ import { compileClassicWithMetrics } from "./compile-classic.js";
7
+ import { compileEngineWithMetrics, resolveContextEngineConfig, } from "./context-engine/index.js";
8
+ import { buildSkillMetadataCatalog } from "./skills/index.js";
9
+ import { createAllTools, describeTools } from "./tools/index.js";
10
+ import { createProvider, resolveModel } from "./llm.js";
11
+ import { formatCompactionBanner, maybeAutoCompactClassic, } from "./auto-compact.js";
12
+ import { TurnRecorder } from "./context-engine/turn-recorder.js";
13
+ import { TurnAbortedError } from "./turn-control.js";
14
+ import { createDefaultTurnSink } from "./ui-events.js";
15
+ import { createSessionLogger, extractLlmErrorMessage, formatUserFacingLlmError, } from "./logger.js";
16
+ import { printDebug, printMemoryBanner } from "./ui.js";
17
+ export async function runTurn(session, userInput, modelOverride, options) {
18
+ /* Always have a sink — default routes to legacy stdout/stderr helpers. */
19
+ const s = options?.sink ?? createDefaultTurnSink();
20
+ const successfulToolResult = (result, isError) => {
21
+ if (isError)
22
+ return false;
23
+ if (result && typeof result === "object" && "ok" in result) {
24
+ return result.ok !== false;
25
+ }
26
+ return true;
27
+ };
28
+ const turnRecorder = new TurnRecorder(userInput);
29
+ const stateBeforeTurn = session.contextEngine?.captureStateSnapshot(session.stateGraph);
30
+ // 1. Append user_message
31
+ session.eventLog.append({
32
+ kind: "user_message",
33
+ actor: "user",
34
+ payload: { text: userInput },
35
+ });
36
+ session.setLastUserInput(userInput);
37
+ // 1b. Auto-hydrate peripheral objects matching user query keywords (engine mode only)
38
+ const contextEngineEnabled = session.isContextEngineEnabled?.() ?? session.config.context_engine?.enabled ?? false;
39
+ const useEngineCompiler = contextEngineEnabled && !!session.contextEngine;
40
+ const classicMode = !useEngineCompiler;
41
+ if (contextEngineEnabled && !session.contextEngine && session.debug) {
42
+ s.onDebug?.("context engine unavailable — falling back to classic compiler");
43
+ }
44
+ let autoHydrated = [];
45
+ if (!classicMode) {
46
+ autoHydrated = session.stateGraph.autoHydrate(userInput);
47
+ if (autoHydrated.length > 0) {
48
+ for (const id of autoHydrated) {
49
+ const obj = session.stateGraph.get(id);
50
+ session.eventLog.append({
51
+ kind: "context_action",
52
+ actor: "kernel",
53
+ payload: {
54
+ action: "setTier",
55
+ id,
56
+ tier: "active",
57
+ lastTouched: obj.lastTouched,
58
+ reason: "auto_hydrate",
59
+ },
60
+ });
61
+ }
62
+ if (session.debug) {
63
+ s.onDebug?.(`auto-hydrated ${autoHydrated.length} object(s): ${autoHydrated.join(", ")}`);
64
+ }
65
+ }
66
+ }
67
+ // 2. Build tools
68
+ const tools = createAllTools({
69
+ eventLog: session.eventLog,
70
+ stateGraph: session.stateGraph,
71
+ memoryStore: session.memoryStore,
72
+ memoryEnabled: session.memoryEnabled,
73
+ incognito: session.isIncognito(),
74
+ contextEngine: session.contextEngine,
75
+ classicMode,
76
+ cwd: session.cwd,
77
+ sandbox: session.config.shell,
78
+ editConfirm: session.config.edit?.confirm,
79
+ getCurrentTurn: () => session.getTurnCount(),
80
+ searchCode: session.config.search_code,
81
+ });
82
+ const modelName = modelOverride ?? session.config.llm.model;
83
+ const contextWindowTokens = await session.ensureModelContextWindow(modelName);
84
+ const reservedOutputTokens = session.config.compiler.reserved_output_tokens ?? 0;
85
+ // 2b. Match skills against user input (engine mode only)
86
+ const tokenBudget = session.config.compiler.token_budget;
87
+ if (!classicMode) {
88
+ session.skillRuntime?.setBudgetBase(tokenBudget);
89
+ session.skillRuntime?.processUserInput(userInput);
90
+ }
91
+ // 3. Compile prompt (system only, user input passed as message)
92
+ const recentEvents = session.eventLog.readLastUncompressed(session.config.compiler.recent_turns);
93
+ const toolDescs = describeTools({ contextEngineEnabled, classicMode });
94
+ const skillsSection = classicMode
95
+ ? buildSkillMetadataCatalog(session.skills) || null
96
+ : session.skillRuntime?.buildPromptSection(tokenBudget) ?? null;
97
+ const agentsBudgetRatio = session.config.compiler.agents_budget_ratio ?? session.config.compiler.skills_budget_ratio;
98
+ const engineConfig = resolveContextEngineConfig(session.config);
99
+ const checkpointSection = contextEngineEnabled && session.contextEngine
100
+ ? session.contextEngine.renderCheckpointSection()
101
+ : null;
102
+ const compileInput = {
103
+ stateGraph: session.stateGraph,
104
+ memoryDigest: session.digest,
105
+ recentEvents,
106
+ userInput,
107
+ toolSchemas: toolDescs,
108
+ cwd: session.cwd,
109
+ sessionId: session.id,
110
+ tokenBudget,
111
+ recentTurnsTokenBudget: session.config.compiler.recent_turns_token_budget,
112
+ agentsContext: session.agentsContext,
113
+ skillsPromptSection: skillsSection,
114
+ checkpointSection,
115
+ memoriesBudgetRatio: session.config.compiler.memories_budget_ratio,
116
+ agentsBudgetRatio,
117
+ skillsSectionBudgetRatio: session.config.skills.max_token_budget_ratio,
118
+ reservedOutputTokens: session.config.compiler.reserved_output_tokens,
119
+ };
120
+ let compiledPrompt;
121
+ let promptMetrics;
122
+ if (useEngineCompiler) {
123
+ const engineResult = compileEngineWithMetrics({
124
+ ...compileInput,
125
+ currentTurn: session.getTurnCount(),
126
+ turnRecords: session.contextEngine.ledger.list(),
127
+ activityEntries: session.contextEngine.getRecentActivity(),
128
+ engineConfig,
129
+ contextWindowTokens,
130
+ });
131
+ compiledPrompt = engineResult.prompt;
132
+ promptMetrics = engineResult.metrics;
133
+ session.setLastCompileScoreRecords(engineResult.scoreRecords, engineResult.pressureMode, engineResult.pressureRatio);
134
+ session.contextEngine.recordCompileTelemetry({
135
+ turn: session.getTurnCount(),
136
+ pressureMode: engineResult.pressureMode,
137
+ excludedScoredUnits: engineResult.excludedScoredUnits,
138
+ });
139
+ if (session.debug && engineResult.scoreRecords.length > 0) {
140
+ const scoresPath = join(session.promptDir, "scores.jsonl");
141
+ if (!existsSync(session.promptDir)) {
142
+ mkdirSync(session.promptDir, { recursive: true });
143
+ }
144
+ appendFileSync(scoresPath, engineResult.scoreRecords.map((r) => JSON.stringify(r)).join("\n") + "\n");
145
+ }
146
+ }
147
+ else {
148
+ let classicResult = compileClassicWithMetrics({
149
+ cwd: session.cwd,
150
+ sessionId: session.id,
151
+ toolSchemas: toolDescs,
152
+ agentsContext: session.agentsContext,
153
+ projectContext: session.projectContext,
154
+ skillsCatalog: skillsSection,
155
+ memoryDigest: session.digest,
156
+ events: session.eventLog.readAllUncompressed(),
157
+ userInput,
158
+ });
159
+ compiledPrompt = classicResult.prompt;
160
+ promptMetrics = classicResult.metrics;
161
+ const compaction = await maybeAutoCompactClassic(session, promptMetrics.totalTokens, modelName);
162
+ const compactionBanner = formatCompactionBanner(compaction);
163
+ if (compactionBanner) {
164
+ s.onDebug?.(compactionBanner);
165
+ if (!session.debug)
166
+ printDebug(compactionBanner);
167
+ }
168
+ if (compaction.compacted) {
169
+ classicResult = compileClassicWithMetrics({
170
+ cwd: session.cwd,
171
+ sessionId: session.id,
172
+ toolSchemas: toolDescs,
173
+ agentsContext: session.agentsContext,
174
+ projectContext: session.projectContext,
175
+ skillsCatalog: skillsSection,
176
+ memoryDigest: session.digest,
177
+ events: session.eventLog.readAllUncompressed(),
178
+ userInput,
179
+ });
180
+ compiledPrompt = classicResult.prompt;
181
+ promptMetrics = classicResult.metrics;
182
+ }
183
+ session.setLastCompileScoreRecords([], "normal", 0);
184
+ }
185
+ session.setLastCompileMetrics(promptMetrics);
186
+ // Track input tokens
187
+ session.recordInputTokens(promptMetrics.totalTokens);
188
+ if (session.debug) {
189
+ const turnNum = session.getTurnCount() + 1;
190
+ const promptDir = session.promptDir;
191
+ if (!existsSync(promptDir))
192
+ mkdirSync(promptDir, { recursive: true });
193
+ const promptFile = join(promptDir, `turn-${String(turnNum).padStart(3, "0")}.md`);
194
+ writeFileSync(promptFile, compiledPrompt, "utf-8");
195
+ s.onDebug?.(`prompt saved → ${promptFile}`);
196
+ }
197
+ // 4. Create LLM provider and model
198
+ const providerFn = createProvider(session.config.llm, contextWindowTokens);
199
+ const model = providerFn(resolveModel(modelName));
200
+ const logger = await createSessionLogger({
201
+ sessionId: session.id,
202
+ sessionLogDir: session.config.session?.log_dir ?? resolveDefaultSessionLogDir(),
203
+ debug: session.debug,
204
+ });
205
+ const llmLogger = logger.child("llm");
206
+ const providerName = session.config.llm.provider;
207
+ // 5. Stream response
208
+ let fullResponse = "";
209
+ let stepIndex = 0;
210
+ let lastStreamReason = "stop";
211
+ let lastLlmErrorMessage;
212
+ const history = [
213
+ {
214
+ role: "user",
215
+ content: userInput,
216
+ timestamp: Date.now(),
217
+ },
218
+ ];
219
+ const maxSteps = 25;
220
+ let interrupted = false;
221
+ for (let step = 0; step < maxSteps; step++) {
222
+ if (options?.signal?.aborted) {
223
+ interrupted = true;
224
+ break;
225
+ }
226
+ const piTools = Object.entries(tools).map(([name, def]) => ({
227
+ name,
228
+ description: String(def.description ?? ""),
229
+ parameters: normalizeToolParameters(def.parameters),
230
+ }));
231
+ const modelOptions = {
232
+ ...(model.__piOptions ?? {}),
233
+ ...(options?.signal ? { signal: options.signal } : {}),
234
+ };
235
+ const stream = piStream(model, {
236
+ systemPrompt: compiledPrompt,
237
+ messages: history,
238
+ tools: piTools,
239
+ }, modelOptions);
240
+ const pendingToolCalls = [];
241
+ let finalReason = "stop";
242
+ let finalMessage = null;
243
+ for await (const event of stream) {
244
+ if (options?.signal?.aborted) {
245
+ interrupted = true;
246
+ break;
247
+ }
248
+ if (event.type === "text_delta" && typeof event.delta === "string") {
249
+ s.onTextDelta?.(event.delta);
250
+ fullResponse += event.delta;
251
+ }
252
+ if (event.type === "thinking_delta" && typeof event.delta === "string") {
253
+ s.onThinkingDelta?.(event.delta);
254
+ }
255
+ if (event.type === "toolcall_end") {
256
+ pendingToolCalls.push({
257
+ toolName: event.toolCall.name,
258
+ args: (event.toolCall.arguments ?? {}),
259
+ toolCallId: event.toolCall.id,
260
+ });
261
+ }
262
+ if (event.type === "done") {
263
+ finalReason = event.reason;
264
+ finalMessage = event.message;
265
+ }
266
+ if (event.type === "error") {
267
+ finalReason = event.reason;
268
+ finalMessage = event.error;
269
+ const llmMessage = extractLlmErrorMessage(finalMessage);
270
+ lastStreamReason = finalReason;
271
+ lastLlmErrorMessage = llmMessage;
272
+ if (finalReason === "aborted") {
273
+ llmLogger.warn("LLM stream aborted", {
274
+ code: "LLM_ABORTED",
275
+ details: { model: modelName, provider: providerName, message: llmMessage },
276
+ });
277
+ interrupted = true;
278
+ break;
279
+ }
280
+ llmLogger.error("LLM stream error", {
281
+ code: "LLM_STREAM_ERROR",
282
+ details: { model: modelName, provider: providerName, reason: finalReason, message: llmMessage },
283
+ });
284
+ const errorEntry = {
285
+ level: "error",
286
+ domain: "llm",
287
+ message: llmMessage ?? "LLM stream error",
288
+ code: "LLM_STREAM_ERROR",
289
+ details: { model: modelName, provider: providerName, reason: finalReason },
290
+ };
291
+ s.onError?.(errorEntry);
292
+ }
293
+ }
294
+ lastStreamReason = finalReason;
295
+ if (finalReason === "error" || finalReason === "aborted") {
296
+ lastLlmErrorMessage =
297
+ extractLlmErrorMessage(finalMessage) ?? lastLlmErrorMessage;
298
+ }
299
+ if (interrupted)
300
+ break;
301
+ if (finalMessage) {
302
+ history.push(finalMessage);
303
+ }
304
+ if (!pendingToolCalls.length || finalReason !== "toolUse") {
305
+ break;
306
+ }
307
+ const toolResults = [];
308
+ const recalledEntryIdsThisTurn = new Set();
309
+ let executionHydrated = false;
310
+ // Notify caller that tool calls are about to execute (e.g. close thinking block)
311
+ s.onToolCallsStart?.();
312
+ for (const tc of pendingToolCalls) {
313
+ if (options?.signal?.aborted) {
314
+ interrupted = true;
315
+ break;
316
+ }
317
+ session.eventLog.append({
318
+ kind: "tool_call",
319
+ actor: "tool",
320
+ payload: { tool: tc.toolName, args: tc.args },
321
+ });
322
+ if (!session.debug)
323
+ s.onSpinnerStart?.(tc.toolName);
324
+ const toolDef = tools[tc.toolName];
325
+ let result;
326
+ let isError = false;
327
+ if (!executionHydrated && !classicMode) {
328
+ session.skillRuntime?.hydrateExecutionForHotSkills();
329
+ executionHydrated = true;
330
+ }
331
+ if (!toolDef || typeof toolDef.execute !== "function") {
332
+ isError = true;
333
+ result = { ok: false, error: `Unknown tool: ${tc.toolName}` };
334
+ }
335
+ else {
336
+ try {
337
+ result = await toolDef.execute(tc.args);
338
+ }
339
+ catch (err) {
340
+ isError = true;
341
+ result = { ok: false, error: err?.message ?? "Tool execution failed" };
342
+ }
343
+ }
344
+ if (isError && !classicMode) {
345
+ session.skillRuntime?.hydrateRecoveryForHotSkills();
346
+ }
347
+ if (tc.toolName === "recall" && successfulToolResult(result, isError)) {
348
+ const entries = result.entries;
349
+ if (Array.isArray(entries)) {
350
+ for (const entry of entries) {
351
+ if (typeof entry?.id === "string" && entry.id) {
352
+ recalledEntryIdsThisTurn.add(entry.id);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ else if (tc.toolName !== "recall" &&
358
+ successfulToolResult(result, isError) &&
359
+ recalledEntryIdsThisTurn.size > 0) {
360
+ session.memoryStore?.reinforceFromSuccessfulToolOutcome(Array.from(recalledEntryIdsThisTurn));
361
+ }
362
+ if (!session.debug) {
363
+ s.onSpinnerStop?.();
364
+ s.onToolCall?.(tc.toolName, tc.args);
365
+ }
366
+ toolResults.push({ toolName: tc.toolName, result });
367
+ let promptResultText = toolResultRawText(result);
368
+ let artifactId;
369
+ if (session.contextEngine) {
370
+ const ingested = session.contextEngine.ingestToolResult({
371
+ sourceTool: tc.toolName,
372
+ command: toolCommandFromArgs(tc.toolName, tc.args),
373
+ rawText: promptResultText,
374
+ createdTurn: session.getTurnCount(),
375
+ });
376
+ promptResultText = ingested.promptText;
377
+ artifactId = ingested.artifactId;
378
+ }
379
+ turnRecorder.recordToolCall({
380
+ tool: tc.toolName,
381
+ args: tc.args,
382
+ result,
383
+ isError,
384
+ artifactId,
385
+ });
386
+ history.push({
387
+ role: "toolResult",
388
+ toolCallId: tc.toolCallId,
389
+ toolName: tc.toolName,
390
+ content: [{ type: "text", text: promptResultText }],
391
+ isError,
392
+ timestamp: Date.now(),
393
+ });
394
+ session.eventLog.append({
395
+ kind: "tool_result",
396
+ actor: "tool",
397
+ payload: { tool: tc.toolName, result },
398
+ });
399
+ // Notify UI sink of tool result for distinct rendering (e.g. TUI)
400
+ s.onToolResult?.(tc.toolName, promptResultText, isError);
401
+ if (options?.signal?.aborted) {
402
+ interrupted = true;
403
+ break;
404
+ }
405
+ }
406
+ if (interrupted)
407
+ break;
408
+ stepIndex++;
409
+ if (session.debug) {
410
+ s.onDebugBlock?.(stepIndex, pendingToolCalls, toolResults);
411
+ }
412
+ }
413
+ if (interrupted) {
414
+ return finalizeInterruptedTurn(session, fullResponse, autoHydrated.length, promptMetrics.totalTokens, turnRecorder, userInput, stateBeforeTurn, classicMode, s);
415
+ }
416
+ if (fullResponse && !fullResponse.endsWith("\n")) {
417
+ s.onNewline?.();
418
+ fullResponse += "\n";
419
+ }
420
+ if (!fullResponse.trim()) {
421
+ const code = lastStreamReason === "error"
422
+ ? "LLM_STREAM_ERROR"
423
+ : lastStreamReason === "aborted"
424
+ ? "LLM_ABORTED"
425
+ : "LLM_EMPTY_RESPONSE";
426
+ if (lastStreamReason !== "error" && lastStreamReason !== "aborted") {
427
+ llmLogger.warn("LLM returned no text content", {
428
+ code: "LLM_EMPTY_RESPONSE",
429
+ details: { model: modelName, provider: providerName, reason: lastStreamReason },
430
+ });
431
+ }
432
+ const fallback = formatUserFacingLlmError({
433
+ reason: lastStreamReason,
434
+ llmMessage: lastLlmErrorMessage,
435
+ model: modelName,
436
+ provider: providerName,
437
+ });
438
+ const errorEntry = {
439
+ level: lastStreamReason === "error" ? "error" : "warn",
440
+ domain: "llm",
441
+ message: lastLlmErrorMessage ?? "No text content in LLM response",
442
+ code,
443
+ details: { model: modelName, provider: providerName, reason: lastStreamReason },
444
+ };
445
+ if (lastStreamReason !== "error") {
446
+ s.onError?.(errorEntry);
447
+ }
448
+ s.onFallback?.(fallback);
449
+ fullResponse = fallback;
450
+ }
451
+ // 6. Append agent_message
452
+ session.eventLog.append({
453
+ kind: "agent_message",
454
+ actor: "agent",
455
+ payload: { text: fullResponse },
456
+ });
457
+ // Track output tokens (estimate from response)
458
+ const outputTokens = estimateTokens(fullResponse);
459
+ session.recordOutputTokens(outputTokens);
460
+ // 6b. Backfill deferred distillations and persist ledger + turn digest
461
+ if (session.contextEngine && stateBeforeTurn) {
462
+ await session.contextEngine.flushDeferredDistillation();
463
+ const turnRecord = turnRecorder.toRecord(fullResponse, session.getTurnCount(), promptMetrics.totalTokens + outputTokens);
464
+ session.contextEngine.appendTurn(turnRecord);
465
+ session.contextEngine.processTurnExtraction({
466
+ userMessage: userInput,
467
+ record: turnRecord,
468
+ stateBefore: stateBeforeTurn,
469
+ stateGraph: session.stateGraph,
470
+ });
471
+ }
472
+ // 7. Increment turn and run tier management (engine mode only)
473
+ session.incrementTurn();
474
+ if (!classicMode) {
475
+ applyTierManagement(session);
476
+ session.skillRuntime?.endTurn();
477
+ flushSkillTelemetry(session);
478
+ }
479
+ // 8. Memory banner — count recall calls & hits from this turn's events
480
+ const stats = computeMemoryStats(session, autoHydrated.length, promptMetrics.totalTokens, outputTokens);
481
+ s.onMemoryBanner?.(stats);
482
+ return fullResponse;
483
+ }
484
+ function finalizeInterruptedTurn(session, partialResponse, autoHydrated, promptTokens, turnRecorder, userInput, stateBeforeTurn, classicMode, sink) {
485
+ const trimmed = partialResponse.trim();
486
+ const messageText = trimmed
487
+ ? `${trimmed}\n\n[interrupted]`
488
+ : "[interrupted]";
489
+ session.eventLog.append({
490
+ kind: "system_note",
491
+ actor: "kernel",
492
+ payload: { type: "turn_interrupted", partial: trimmed.length > 0 },
493
+ });
494
+ session.eventLog.append({
495
+ kind: "agent_message",
496
+ actor: "agent",
497
+ payload: { text: messageText },
498
+ });
499
+ if (session.contextEngine && stateBeforeTurn) {
500
+ void session.contextEngine.flushDeferredDistillation();
501
+ const turnRecord = turnRecorder.toRecord(messageText, session.getTurnCount(), promptTokens + estimateTokens(trimmed));
502
+ session.contextEngine.appendTurn(turnRecord);
503
+ session.contextEngine.processTurnExtraction({
504
+ userMessage: userInput,
505
+ record: turnRecord,
506
+ stateBefore: stateBeforeTurn,
507
+ stateGraph: session.stateGraph,
508
+ });
509
+ }
510
+ session.incrementTurn();
511
+ if (!classicMode) {
512
+ applyTierManagement(session);
513
+ session.skillRuntime?.endTurn();
514
+ flushSkillTelemetry(session);
515
+ }
516
+ const stats = computeMemoryStats(session, autoHydrated, promptTokens, estimateTokens(trimmed));
517
+ if (sink)
518
+ sink.onMemoryBanner?.(stats);
519
+ else
520
+ printMemoryBanner(stats);
521
+ throw new TurnAbortedError(trimmed);
522
+ }
523
+ function flushSkillTelemetry(session) {
524
+ const events = session.skillRuntime?.drainEvents();
525
+ if (!events?.length)
526
+ return;
527
+ for (const event of events) {
528
+ session.eventLog.append({
529
+ kind: "system_note",
530
+ actor: "kernel",
531
+ payload: { type: "skill_telemetry", event },
532
+ });
533
+ }
534
+ }
535
+ /**
536
+ * Heuristic for estimating output tokens from response text length.
537
+ * Note: this is a rough estimate that degrades for non-ASCII text.
538
+ * Ideally we'd get `usage` from the provider directly in the future.
539
+ */
540
+ function estimateTokens(text) {
541
+ return Math.ceil(text.length / 4);
542
+ }
543
+ export function isZodSchema(schema) {
544
+ return !!schema && typeof schema === "object" && "_def" in schema;
545
+ }
546
+ export function normalizeToolParameters(schema) {
547
+ if (isZodSchema(schema)) {
548
+ const json = zodToJsonSchema(schema, {
549
+ $refStrategy: "none",
550
+ target: "jsonSchema7",
551
+ });
552
+ delete json.$schema;
553
+ delete json.$ref;
554
+ delete json.definitions;
555
+ return json;
556
+ }
557
+ return {
558
+ type: "object",
559
+ additionalProperties: false,
560
+ properties: {},
561
+ };
562
+ }
563
+ export function applyTierManagement(session) {
564
+ const { idle_soft_after_turns, idle_hard_after_turns } = session.config.tiers;
565
+ const sg = session.stateGraph;
566
+ const currentTurn = sg.getTurnCount();
567
+ for (const obj of sg.getActive()) {
568
+ const touchedTurn = sg.getTouchedTurn(obj.id);
569
+ const idleTurns = currentTurn - touchedTurn;
570
+ if (idleTurns >= idle_hard_after_turns) {
571
+ sg.setTier(obj.id, "hard");
572
+ }
573
+ else if (idleTurns >= idle_soft_after_turns) {
574
+ sg.setTier(obj.id, "soft");
575
+ }
576
+ }
577
+ for (const obj of sg.getPeripheral()) {
578
+ if (obj.tier !== "soft")
579
+ continue;
580
+ const touchedTurn = sg.getTouchedTurn(obj.id);
581
+ const idleTurns = currentTurn - touchedTurn;
582
+ if (idleTurns >= idle_hard_after_turns) {
583
+ sg.setTier(obj.id, "hard");
584
+ }
585
+ }
586
+ }
587
+ export function computeMemoryStats(session, autoHydrated, promptTokens, outputTokens) {
588
+ const memStats = session.getMemoryStats();
589
+ const recentEvents = session.eventLog.readLast(50);
590
+ let recallCalls = 0;
591
+ let recallHits = 0;
592
+ for (const ev of recentEvents) {
593
+ if (ev.kind === "system_note" && ev.payload.type === "memory_recall") {
594
+ recallCalls++;
595
+ recallHits += ev.payload.hits ?? 0;
596
+ }
597
+ }
598
+ return {
599
+ activeState: memStats.active,
600
+ totalState: memStats.total,
601
+ digestLen: session.digest?.length ?? 0,
602
+ recallCalls,
603
+ recallHits,
604
+ autoHydrated,
605
+ promptTokens: promptTokens ?? 0,
606
+ outputTokens: outputTokens ?? 0,
607
+ };
608
+ }
609
+ function toolResultRawText(result) {
610
+ if (typeof result === "string")
611
+ return result;
612
+ return JSON.stringify(result);
613
+ }
614
+ function toolCommandFromArgs(toolName, args) {
615
+ if (typeof args.command === "string")
616
+ return args.command;
617
+ if (typeof args.path === "string")
618
+ return args.path;
619
+ if (typeof args.query === "string")
620
+ return args.query;
621
+ if (toolName === "shell" && typeof args.command === "string")
622
+ return args.command;
623
+ return undefined;
624
+ }