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
@@ -0,0 +1,250 @@
1
+ import { diffStateGraph } from "./state-snapshot.js";
2
+ const USER_INTENT_MAX = 120;
3
+ const NARRATIVE_MAX_CHARS = 160;
4
+ export function extractUserIntent(userMessage) {
5
+ const trimmed = userMessage.trim().replace(/\s+/g, " ");
6
+ if (trimmed.length <= USER_INTENT_MAX)
7
+ return trimmed;
8
+ return trimmed.slice(0, USER_INTENT_MAX - 3) + "...";
9
+ }
10
+ export function buildToolSummary(record) {
11
+ if (record.toolCalls.length === 0)
12
+ return "no tools";
13
+ const counts = new Map();
14
+ for (const tc of record.toolCalls) {
15
+ counts.set(tc.tool, (counts.get(tc.tool) ?? 0) + 1);
16
+ }
17
+ const parts = [...counts.entries()].map(([tool, count]) => count > 1 ? `${tool}×${count}` : tool);
18
+ return parts.join(", ");
19
+ }
20
+ export function decisionSummary(decision) {
21
+ return typeof decision === "string" ? decision : decision.summary;
22
+ }
23
+ export function normalizeTurnDigest(raw) {
24
+ const decisions = (raw.decisions ?? []).map((d) => typeof d === "string" ? { summary: d } : d);
25
+ return {
26
+ ...raw,
27
+ filesWritten: raw.filesWritten ?? [],
28
+ decisions,
29
+ };
30
+ }
31
+ /**
32
+ * Extract implicit constraints from user messages.
33
+ *
34
+ * **Architecture note:** Regex is a minimal safety net for the most syntactically
35
+ * unambiguous patterns only. It is NOT the primary mechanism for capturing
36
+ * implicit knowledge — the system prompt nudge (see `compiler.ts` "Implicit
37
+ * Knowledge Capture") is the primary mechanism, because the LLM is the
38
+ * language-understanding component, not regex.
39
+ *
40
+ * We only capture patterns where the syntax is unambiguous enough that missing
41
+ * them would be worse than occasionally over-capturing. Specifically:
42
+ * - "not X, Y" corrections (the user directly reversing a wrong choice)
43
+ *
44
+ * Patterns like "we use", "let's use", "I prefer", "make sure", "how about"
45
+ * are NOT captured here because they are too variable in natural language.
46
+ * Those are the LLM's responsibility via the system prompt nudge.
47
+ */
48
+ export function extractImplicitConstraints(userMessage) {
49
+ const constraints = [];
50
+ const text = userMessage.trim();
51
+ if (!text)
52
+ return constraints;
53
+ // "not X, Y" — the most unambiguous correction pattern.
54
+ // "not npm, pnpm" → "Use pnpm, not npm"
55
+ // "not npm, but actually pnpm" → "Use pnpm, not npm"
56
+ const notPattern = text.match(/\bnot\s+([^,]+?),\s*(?:but\s+)?(?:actually\s+)?(.+?)(?:[.!?]|$)/i);
57
+ if (notPattern) {
58
+ const avoided = notPattern[1].trim();
59
+ const preferred = notPattern[2].trim().replace(/[.!?]$/, "");
60
+ constraints.push(`Use ${preferred}, not ${avoided}`);
61
+ }
62
+ return constraints;
63
+ }
64
+ export function isNarrativeWorthy(digest, previousIntent) {
65
+ if (digest.decisions.length > 0)
66
+ return true;
67
+ if (digest.constraints.length > 0)
68
+ return true;
69
+ if (digest.errorsNew.length > 0 || digest.errorsFixed.length > 0)
70
+ return true;
71
+ if (digest.filesWritten.length > 0)
72
+ return true;
73
+ if (previousIntent && digest.userIntent !== previousIntent)
74
+ return true;
75
+ return false;
76
+ }
77
+ export function buildNarrativeEntry(digest, previousIntent) {
78
+ const parts = [];
79
+ if (previousIntent && digest.userIntent !== previousIntent) {
80
+ parts.push(digest.userIntent);
81
+ }
82
+ if (digest.decisions.length > 0) {
83
+ const summaries = digest.decisions.map(decisionSummary).join(", ");
84
+ parts.push(`Decided: ${summaries}`);
85
+ }
86
+ if (digest.errorsFixed.length > 0) {
87
+ parts.push(`Fixed: ${digest.errorsFixed.join(", ")}`);
88
+ }
89
+ if (digest.errorsNew.length > 0) {
90
+ parts.push(`Error: ${digest.errorsNew.join(", ")}`);
91
+ }
92
+ if (digest.filesWritten.length > 0) {
93
+ parts.push(`Wrote: ${digest.filesWritten.join(", ")}`);
94
+ }
95
+ if (digest.constraints.length > 0) {
96
+ parts.push(`Constraints: ${digest.constraints.join(", ")}`);
97
+ }
98
+ if (parts.length === 0)
99
+ return null;
100
+ let text = parts.join(". ");
101
+ if (text.length > NARRATIVE_MAX_CHARS) {
102
+ text = text.slice(0, NARRATIVE_MAX_CHARS - 1) + "…";
103
+ }
104
+ return text;
105
+ }
106
+ const PLAN_KEYWORDS = /\b(?:the plan|my approach|i'll start by|next i'll)\b/i;
107
+ const NUMBERED_LIST = /(?:^|\n)\s*\d+\.\s+.+/m;
108
+ const STEP_MARKERS = /(?:^|\n)\s*(?:-\s*step\s*\d+:|step\s*\d+:|first:|next:)/i;
109
+ const MARKDOWN_TASKS = /(?:^|\n)\s*-\s*\[[ x]\]\s+.+/m;
110
+ const BULLET_LIST = /(?:^|\n)\s*[-*]\s+.+/m;
111
+ export function extractPlan(assistantMessage) {
112
+ const text = assistantMessage.trim();
113
+ if (!text)
114
+ return null;
115
+ // Skip content inside code blocks (plans don't live there)
116
+ const stripped = text.replace(/```[\s\S]*?```/g, "");
117
+ const hasPlanSignal = PLAN_KEYWORDS.test(stripped) ||
118
+ NUMBERED_LIST.test(stripped) ||
119
+ STEP_MARKERS.test(stripped) ||
120
+ MARKDOWN_TASKS.test(stripped);
121
+ if (!hasPlanSignal)
122
+ return null;
123
+ const lines = stripped.split("\n");
124
+ const planLines = [];
125
+ let inPlan = false;
126
+ for (const line of lines) {
127
+ // Numbered items
128
+ if (/^\s*\d+\.\s+/.test(line)) {
129
+ inPlan = true;
130
+ planLines.push(line.trim());
131
+ continue;
132
+ }
133
+ // Step markers
134
+ if (STEP_MARKERS.test(line)) {
135
+ inPlan = true;
136
+ planLines.push(line.trim());
137
+ continue;
138
+ }
139
+ // Markdown task items
140
+ if (/^\s*-\s*\[[ x]\]\s+/.test(line)) {
141
+ inPlan = true;
142
+ planLines.push(line.trim());
143
+ continue;
144
+ }
145
+ // Sub-items / continuation bullets within a plan block
146
+ if (inPlan && /^\s*[-*]\s+/.test(line)) {
147
+ planLines.push(line.trim());
148
+ continue;
149
+ }
150
+ // Indented sub-items (under a numbered step)
151
+ if (inPlan && /^\s{2,}\S/.test(line)) {
152
+ planLines.push(line.trim());
153
+ continue;
154
+ }
155
+ // Blank lines within plan block — skip
156
+ if (inPlan && line.trim() === "") {
157
+ continue;
158
+ }
159
+ // Non-plan content breaks the block
160
+ if (inPlan && planLines.length > 0) {
161
+ break;
162
+ }
163
+ }
164
+ if (planLines.length >= 2) {
165
+ return planLines.join("\n");
166
+ }
167
+ const planMatch = stripped.match(/(?:the plan is|my approach is|i'll start by)\s*[:\-]?\s*(?:to\s+)?(.+?)(?:\n\n|$)/i);
168
+ if (planMatch) {
169
+ return planMatch[1].trim();
170
+ }
171
+ if (NUMBERED_LIST.test(stripped)) {
172
+ const numbered = lines.filter((line) => /^\s*\d+\.\s+/.test(line));
173
+ if (numbered.length >= 2) {
174
+ return numbered.map((line) => line.trim()).join("\n");
175
+ }
176
+ }
177
+ return null;
178
+ }
179
+ export function extractPlanItems(planText) {
180
+ const items = [];
181
+ for (const line of planText.split("\n")) {
182
+ const numbered = line.match(/^\s*\d+\.\s+(.+)/);
183
+ if (numbered) {
184
+ items.push(numbered[1].trim());
185
+ continue;
186
+ }
187
+ const step = line.match(/^\s*(?:-\s*)?(?:step\s*\d+:|first:|next:)\s*(.+)/i);
188
+ if (step) {
189
+ items.push(step[1].trim());
190
+ continue;
191
+ }
192
+ const task = line.match(/^\s*-\s*\[[ x]\]\s+(.+)/);
193
+ if (task) {
194
+ items.push(task[1].trim());
195
+ continue;
196
+ }
197
+ const bullet = line.match(/^\s*[-*]\s+(.+)/);
198
+ if (bullet) {
199
+ items.push(bullet[1].trim());
200
+ }
201
+ }
202
+ if (items.length === 0 && planText.trim()) {
203
+ return planText
204
+ .split(/[,;]/)
205
+ .map((part) => part.trim())
206
+ .filter(Boolean);
207
+ }
208
+ return items;
209
+ }
210
+ export function detectCompletedPlanItems(planItems, digest) {
211
+ const signals = [
212
+ ...digest.filesChanged,
213
+ ...digest.filesWritten,
214
+ ...digest.decisions.map(decisionSummary),
215
+ ...digest.constraints,
216
+ ]
217
+ .join(" ")
218
+ .toLowerCase();
219
+ return planItems.filter((item) => {
220
+ const keywords = item
221
+ .toLowerCase()
222
+ .split(/\s+/)
223
+ .filter((word) => word.length > 3);
224
+ return keywords.some((keyword) => signals.includes(keyword));
225
+ });
226
+ }
227
+ export function extractTurnDigest(input) {
228
+ const { decisions, constraints: stateConstraints } = diffStateGraph(input.stateBefore, input.stateGraph.snapshot());
229
+ const implicitConstraints = extractImplicitConstraints(input.userMessage);
230
+ const constraints = [
231
+ ...new Set([...stateConstraints, ...implicitConstraints]),
232
+ ];
233
+ const filesChanged = [
234
+ ...new Set([...input.record.filesRead, ...input.record.filesWritten]),
235
+ ];
236
+ const extractedPlan = extractPlan(input.record.assistantMessage);
237
+ return {
238
+ turnId: input.turn,
239
+ userIntent: extractUserIntent(input.userMessage),
240
+ filesChanged,
241
+ filesWritten: [...input.record.filesWritten],
242
+ artifactRefs: [...input.record.artifactIds],
243
+ decisions,
244
+ constraints,
245
+ errorsNew: input.errorsNew,
246
+ errorsFixed: input.errorsFixed,
247
+ toolSummary: buildToolSummary(input.record),
248
+ extractedPlan: extractedPlan ?? undefined,
249
+ };
250
+ }
@@ -0,0 +1,18 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { Event } from "../types.js";
3
+ import type { TurnRecord, TurnSearchMatch } from "./types.js";
4
+ export declare function groupEventsIntoTurns(events: Event[]): Array<{
5
+ userMessage: string;
6
+ events: Event[];
7
+ }>;
8
+ export declare class TurnLedger {
9
+ private readonly db;
10
+ private readonly sessionId;
11
+ constructor(db: Database.Database, sessionId: string);
12
+ append(record: TurnRecord): void;
13
+ get(turn: number): TurnRecord | null;
14
+ list(): TurnRecord[];
15
+ getMaxTurn(): number | null;
16
+ migrateFromEvents(events: Event[]): number;
17
+ search(query: string, limit?: number): TurnSearchMatch[];
18
+ }
@@ -0,0 +1,184 @@
1
+ import { getMaxLedgerTurn, getTurnRecord, hasLedgerTurn, insertTurnRecord, listArtifactIdsForTurn, listTurnRecords, } from "./db.js";
2
+ import { buildTurnSearchText, extractFilePathsFromTool, extractToolError, } from "./turn-recorder.js";
3
+ import { bm25Score, tokenize } from "./bm25.js";
4
+ function buildExcerpt(record, maxLen = 400) {
5
+ const text = [
6
+ record.userMessage,
7
+ record.assistantMessage,
8
+ ...record.errors,
9
+ ]
10
+ .join(" ")
11
+ .replace(/\s+/g, " ")
12
+ .trim();
13
+ if (text.length <= maxLen)
14
+ return text;
15
+ return text.slice(0, maxLen - 3) + "...";
16
+ }
17
+ function pairToolCalls(events) {
18
+ const calls = [];
19
+ const pending = [];
20
+ for (const ev of events) {
21
+ if (ev.kind === "tool_call") {
22
+ pending.push({
23
+ tool: String(ev.payload.tool ?? ""),
24
+ args: ev.payload.args ?? {},
25
+ });
26
+ continue;
27
+ }
28
+ if (ev.kind !== "tool_result")
29
+ continue;
30
+ const tool = String(ev.payload.tool ?? "");
31
+ const idx = pending.findIndex((p) => p.tool === tool);
32
+ const call = idx >= 0 ? pending.splice(idx, 1)[0] : { tool, args: {} };
33
+ const result = ev.payload.result;
34
+ const isError = !!result &&
35
+ typeof result === "object" &&
36
+ (("ok" in result && result.ok === false) ||
37
+ "error" in result);
38
+ calls.push({
39
+ tool: call.tool,
40
+ args: call.args,
41
+ isError,
42
+ });
43
+ }
44
+ return calls;
45
+ }
46
+ function collectFilesAndErrors(toolCalls, events) {
47
+ const filesRead = new Set();
48
+ const filesWritten = new Set();
49
+ const errors = [];
50
+ for (const tc of toolCalls) {
51
+ const paths = extractFilePathsFromTool(tc.tool, tc.args);
52
+ if (paths.read)
53
+ filesRead.add(paths.read);
54
+ if (paths.written)
55
+ filesWritten.add(paths.written);
56
+ }
57
+ for (const ev of events) {
58
+ if (ev.kind !== "tool_result")
59
+ continue;
60
+ const result = ev.payload.result;
61
+ const err = extractToolError(result, false);
62
+ if (err)
63
+ errors.push(err);
64
+ }
65
+ return {
66
+ filesRead: [...filesRead],
67
+ filesWritten: [...filesWritten],
68
+ errors,
69
+ };
70
+ }
71
+ export function groupEventsIntoTurns(events) {
72
+ const turns = [];
73
+ let current = null;
74
+ for (const ev of events) {
75
+ if (ev.kind === "user_message") {
76
+ if (current)
77
+ turns.push(current);
78
+ current = {
79
+ userMessage: String(ev.payload.text ?? ""),
80
+ events: [],
81
+ };
82
+ continue;
83
+ }
84
+ if (!current)
85
+ continue;
86
+ current.events.push(ev);
87
+ }
88
+ if (current)
89
+ turns.push(current);
90
+ return turns;
91
+ }
92
+ export class TurnLedger {
93
+ db;
94
+ sessionId;
95
+ constructor(db, sessionId) {
96
+ this.db = db;
97
+ this.sessionId = sessionId;
98
+ }
99
+ append(record) {
100
+ if (hasLedgerTurn(this.db, this.sessionId, record.turn))
101
+ return;
102
+ insertTurnRecord(this.db, this.sessionId, record, buildTurnSearchText(record));
103
+ }
104
+ get(turn) {
105
+ return getTurnRecord(this.db, this.sessionId, turn);
106
+ }
107
+ list() {
108
+ return listTurnRecords(this.db, this.sessionId);
109
+ }
110
+ getMaxTurn() {
111
+ return getMaxLedgerTurn(this.db, this.sessionId);
112
+ }
113
+ migrateFromEvents(events) {
114
+ const grouped = groupEventsIntoTurns(events);
115
+ let inserted = 0;
116
+ grouped.forEach((group, turnIndex) => {
117
+ if (hasLedgerTurn(this.db, this.sessionId, turnIndex))
118
+ return;
119
+ const assistantMessage = group.events
120
+ .filter((e) => e.kind === "agent_message")
121
+ .map((e) => String(e.payload.text ?? ""))
122
+ .join("\n") || "";
123
+ const turnEvents = group.events.filter((e) => e.kind === "tool_call" || e.kind === "tool_result");
124
+ const toolCalls = pairToolCalls(turnEvents);
125
+ const { filesRead, filesWritten, errors } = collectFilesAndErrors(toolCalls, group.events);
126
+ const artifactIds = listArtifactIdsForTurn(this.db, this.sessionId, turnIndex);
127
+ const record = {
128
+ turn: turnIndex,
129
+ userMessage: group.userMessage,
130
+ assistantMessage,
131
+ toolCalls,
132
+ artifactIds,
133
+ filesRead,
134
+ filesWritten,
135
+ errors,
136
+ tokenCount: 0,
137
+ timestamp: group.events[group.events.length - 1]?.timestamp ?? Date.now(),
138
+ };
139
+ insertTurnRecord(this.db, this.sessionId, record, buildTurnSearchText(record));
140
+ inserted++;
141
+ });
142
+ return inserted;
143
+ }
144
+ search(query, limit = 20) {
145
+ const trimmed = query.trim();
146
+ if (!trimmed)
147
+ return [];
148
+ const records = this.list();
149
+ if (records.length === 0)
150
+ return [];
151
+ const queryTokens = tokenize(trimmed);
152
+ if (queryTokens.length === 0)
153
+ return [];
154
+ const docs = records.map((r) => tokenize(buildTurnSearchText(r)));
155
+ const totalDocs = docs.length;
156
+ const avgDocLen = docs.reduce((sum, d) => sum + d.length, 0) / totalDocs;
157
+ const docFreq = new Map();
158
+ for (const doc of docs) {
159
+ const seen = new Set(doc);
160
+ for (const t of seen)
161
+ docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
162
+ }
163
+ const scored = records
164
+ .map((record, i) => ({
165
+ record,
166
+ score: bm25Score(queryTokens, docs[i], avgDocLen, totalDocs, docFreq),
167
+ }))
168
+ .filter((entry) => entry.score > 0)
169
+ .sort((a, b) => b.score - a.score)
170
+ .slice(0, limit);
171
+ return scored.map(({ record, score }) => ({
172
+ turn: record.turn,
173
+ score,
174
+ userMessage: record.userMessage,
175
+ assistantMessage: record.assistantMessage,
176
+ excerpt: buildExcerpt(record),
177
+ artifactIds: record.artifactIds,
178
+ filesRead: record.filesRead,
179
+ filesWritten: record.filesWritten,
180
+ errors: record.errors,
181
+ timestamp: record.timestamp,
182
+ }));
183
+ }
184
+ }
@@ -0,0 +1,24 @@
1
+ import type { TurnRecord } from "./types.js";
2
+ export declare function extractToolError(result: unknown, isError: boolean): string | null;
3
+ export declare function extractFilePathsFromTool(tool: string, args: Record<string, unknown>): {
4
+ read?: string;
5
+ written?: string;
6
+ };
7
+ export declare function buildTurnSearchText(record: TurnRecord): string;
8
+ export declare class TurnRecorder {
9
+ readonly userMessage: string;
10
+ private readonly toolCalls;
11
+ private readonly artifactIds;
12
+ private readonly filesRead;
13
+ private readonly filesWritten;
14
+ private readonly errors;
15
+ constructor(userMessage: string);
16
+ recordToolCall(input: {
17
+ tool: string;
18
+ args: Record<string, unknown>;
19
+ result: unknown;
20
+ isError: boolean;
21
+ artifactId?: string;
22
+ }): void;
23
+ toRecord(assistantMessage: string, turn: number, tokenCount: number): TurnRecord;
24
+ }
@@ -0,0 +1,88 @@
1
+ export function extractToolError(result, isError) {
2
+ if (isError) {
3
+ if (result && typeof result === "object" && "error" in result) {
4
+ return String(result.error ?? "tool error");
5
+ }
6
+ return "tool error";
7
+ }
8
+ if (result && typeof result === "object" && "ok" in result) {
9
+ if (result.ok === false) {
10
+ return String(result.error ?? "tool failed");
11
+ }
12
+ }
13
+ return null;
14
+ }
15
+ export function extractFilePathsFromTool(tool, args) {
16
+ const path = typeof args.path === "string" ? args.path : undefined;
17
+ if (!path)
18
+ return {};
19
+ if (tool === "read_file" || tool === "read_and_summarize") {
20
+ return { read: path };
21
+ }
22
+ if (tool === "write_file" || tool === "edit_file") {
23
+ return { written: path };
24
+ }
25
+ return {};
26
+ }
27
+ export function buildTurnSearchText(record) {
28
+ const parts = [
29
+ record.userMessage,
30
+ record.assistantMessage,
31
+ ...record.errors,
32
+ ...record.filesRead,
33
+ ...record.filesWritten,
34
+ ...record.artifactIds,
35
+ ...record.toolCalls.map((tc) => `${tc.tool} ${JSON.stringify(tc.args)}`),
36
+ ];
37
+ return parts.filter(Boolean).join("\n");
38
+ }
39
+ export class TurnRecorder {
40
+ userMessage;
41
+ toolCalls = [];
42
+ artifactIds = new Set();
43
+ filesRead = new Set();
44
+ filesWritten = new Set();
45
+ errors = [];
46
+ constructor(userMessage) {
47
+ this.userMessage = userMessage;
48
+ }
49
+ recordToolCall(input) {
50
+ this.toolCalls.push({
51
+ tool: input.tool,
52
+ args: input.args,
53
+ resultArtifactId: input.artifactId,
54
+ isError: input.isError,
55
+ resultText: truncateResultText(input.result),
56
+ });
57
+ if (input.artifactId)
58
+ this.artifactIds.add(input.artifactId);
59
+ const paths = extractFilePathsFromTool(input.tool, input.args);
60
+ if (paths.read)
61
+ this.filesRead.add(paths.read);
62
+ if (paths.written)
63
+ this.filesWritten.add(paths.written);
64
+ const err = extractToolError(input.result, input.isError);
65
+ if (err)
66
+ this.errors.push(err);
67
+ }
68
+ toRecord(assistantMessage, turn, tokenCount) {
69
+ return {
70
+ turn,
71
+ userMessage: this.userMessage,
72
+ assistantMessage,
73
+ toolCalls: [...this.toolCalls],
74
+ artifactIds: [...this.artifactIds],
75
+ filesRead: [...this.filesRead],
76
+ filesWritten: [...this.filesWritten],
77
+ errors: [...this.errors],
78
+ tokenCount,
79
+ timestamp: Date.now(),
80
+ };
81
+ }
82
+ }
83
+ function truncateResultText(result, maxLen = 400) {
84
+ const text = typeof result === "string" ? result : JSON.stringify(result);
85
+ if (text.length <= maxLen)
86
+ return text;
87
+ return text.slice(0, maxLen) + "…";
88
+ }