kc-beta 0.1.1 → 0.2.1

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 (34) hide show
  1. package/bin/kc-beta.js +14 -2
  2. package/package.json +1 -1
  3. package/src/agent/context-window.js +151 -0
  4. package/src/agent/context.js +58 -88
  5. package/src/agent/engine.js +267 -38
  6. package/src/agent/event-log.js +111 -0
  7. package/src/agent/llm-client.js +352 -59
  8. package/src/agent/pipelines/_archive_v1/distillation.js +113 -0
  9. package/src/agent/pipelines/_archive_v1/extraction.js +92 -0
  10. package/src/agent/pipelines/_archive_v1/initializer.js +163 -0
  11. package/src/agent/pipelines/_archive_v1/production-qc.js +99 -0
  12. package/src/agent/pipelines/_archive_v1/skill-authoring.js +83 -0
  13. package/src/agent/pipelines/_archive_v1/skill-testing.js +111 -0
  14. package/src/agent/pipelines/base.js +6 -0
  15. package/src/agent/pipelines/distillation.js +25 -11
  16. package/src/agent/pipelines/extraction.js +26 -7
  17. package/src/agent/pipelines/initializer.js +30 -20
  18. package/src/agent/pipelines/production-qc.js +22 -5
  19. package/src/agent/pipelines/skill-authoring.js +19 -8
  20. package/src/agent/pipelines/skill-testing.js +26 -8
  21. package/src/agent/retry.js +83 -0
  22. package/src/agent/session-state.js +78 -0
  23. package/src/agent/skill-loader.js +139 -0
  24. package/src/agent/token-counter.js +62 -0
  25. package/src/agent/tools/document-parse.js +3 -3
  26. package/src/agent/tools/tier-downgrade.js +11 -2
  27. package/src/agent/tools/web-search.js +107 -0
  28. package/src/agent/tools/worker-llm-call.js +14 -5
  29. package/src/cli/components.js +16 -4
  30. package/src/cli/config.js +246 -0
  31. package/src/cli/index.js +99 -10
  32. package/src/cli/onboard.js +154 -48
  33. package/src/config.js +25 -7
  34. package/src/providers.js +370 -0
@@ -18,6 +18,8 @@ import { DashboardRenderTool } from "./tools/dashboard-render.js";
18
18
  import { EvolutionCycleTool } from "./tools/evolution-cycle.js";
19
19
  import { TierDowngradeTool } from "./tools/tier-downgrade.js";
20
20
  import { AgentTool } from "./tools/agent-tool.js";
21
+ import { WebSearchTool } from "./tools/web-search.js";
22
+ import { SkillLoader } from "./skill-loader.js";
21
23
  import { Phase } from "./pipelines/index.js";
22
24
  import { ProjectInitializer } from "./pipelines/initializer.js";
23
25
  import { RuleExtractionPipeline } from "./pipelines/extraction.js";
@@ -25,6 +27,13 @@ import { SkillAuthoringPipeline } from "./pipelines/skill-authoring.js";
25
27
  import { SkillTestingPipeline } from "./pipelines/skill-testing.js";
26
28
  import { DistillationEngine as DistillationPipeline } from "./pipelines/distillation.js";
27
29
  import { ProductionQCPipeline } from "./pipelines/production-qc.js";
30
+ import { EventLog } from "./event-log.js";
31
+ import { ContextWindow } from "./context-window.js";
32
+ import { SessionState } from "./session-state.js";
33
+ import { estimateTokens, estimateMessagesTokens } from "./token-counter.js";
34
+
35
+ // Phases where worker LLM tools are available (DISTILL mode)
36
+ const DISTILL_PHASES = new Set([Phase.DISTILLATION, Phase.PRODUCTION_QC]);
28
37
 
29
38
  /**
30
39
  * The KC Agent conversation engine.
@@ -32,7 +41,7 @@ import { ProductionQCPipeline } from "./pipelines/production-qc.js";
32
41
  * Core loop: user message -> context assembly -> LLM API (streaming) ->
33
42
  * tool execution (if any) -> repeat until no tool calls -> turn complete.
34
43
  *
35
- * Uses OpenAI-compatible API. Yields AgentEvent objects.
44
+ * Tools are phase-gated: worker LLM tools only available in DISTILL mode.
36
45
  */
37
46
  export class AgentEngine {
38
47
  /**
@@ -45,7 +54,6 @@ export class AgentEngine {
45
54
  this.client = client;
46
55
  this.config = config;
47
56
  this.context = new ContextAssembler();
48
- this.toolRegistry = new ToolRegistry();
49
57
 
50
58
  // Workspace + structural components
51
59
  this.workspace = new Workspace(config.kcWorkspaceRoot, sessionId);
@@ -54,30 +62,21 @@ export class AgentEngine {
54
62
  this.cornerCases = new CornerCaseRegistry(this.workspace.cwd);
55
63
  this.confidence = new ConfidenceScorer(this.workspace.cwd, this.cornerCases);
56
64
 
57
- // Register tools
58
- this.toolRegistry.register(new SandboxExecTool(this.workspace, config.kcExecTimeout));
59
- this.toolRegistry.register(new WorkspaceFileTool(this.workspace, this.versionManager));
60
- this.toolRegistry.register(new DocumentParseTool(this.workspace, {
61
- mineruApiUrl: config.mineruApiUrl,
62
- mineruApiKey: config.mineruApiKey,
63
- siliconflowApiKey: config.siliconflowApiKey,
64
- siliconflowBaseUrl: config.siliconflowBaseUrl,
65
- ocrModel: config.ocrModelTier1,
66
- }));
67
- this.toolRegistry.register(new DocumentSearchTool(this.workspace));
65
+ // Event log (append-only JSONL, source of truth)
66
+ this.eventLog = new EventLog(this.workspace.cwd);
68
67
 
69
- const workerLlm = new WorkerLLMCallTool(this.workspace, {
70
- apiKey: config.siliconflowApiKey,
71
- baseUrl: config.siliconflowBaseUrl,
68
+ // Context windowing
69
+ this.contextWindow = new ContextWindow({
70
+ contextLimit: config.kcContextLimit || 200000,
71
+ reserveForResponse: config.kcMaxTokens || 65536,
72
72
  });
73
- this.toolRegistry.register(workerLlm);
74
- this.toolRegistry.register(new WorkflowRunTool(this.workspace, this.versionManager, this.confidence));
75
- this.toolRegistry.register(new TierDowngradeTool(this.workspace, workerLlm));
76
- this.toolRegistry.register(new EvolutionCycleTool(this.workspace, this.cornerCases));
77
- this.toolRegistry.register(new RuleCatalogTool(this.workspace));
78
- this.toolRegistry.register(new QCSampleTool(this.workspace));
79
- this.toolRegistry.register(new DashboardRenderTool(this.workspace));
80
- this.toolRegistry.register(new AgentTool(this.workspace, (sid) => new AgentEngine({ client, config, sessionId: sid })));
73
+
74
+ // Session state persistence
75
+ this.sessionState = new SessionState(this.workspace.cwd);
76
+
77
+ // Build all tool instances (but register phase-appropriate ones)
78
+ this._buildTools = this._createAllTools();
79
+ this._phaseSummaries = [];
81
80
 
82
81
  // Pipeline system (meta-meta skills as code)
83
82
  this.currentPhase = Phase.BOOTSTRAP;
@@ -89,14 +88,210 @@ export class AgentEngine {
89
88
  [Phase.DISTILLATION]: new DistillationPipeline(this.workspace),
90
89
  [Phase.PRODUCTION_QC]: new ProductionQCPipeline(this.workspace),
91
90
  };
91
+
92
+ // Skill discovery (Claude Code pattern: index in context, full content on demand)
93
+ this._skillLoader = new SkillLoader(config.language);
94
+
95
+ // Register tools for initial phase
96
+ this.toolRegistry = new ToolRegistry();
97
+ this._registerToolsForPhase(this.currentPhase);
92
98
  }
93
99
 
94
100
  /**
95
- * Register additional tools (called after construction for tools that need extra deps).
96
- * @param {import('./tools/base.js').BaseTool} tool
101
+ * Create all tool instances. Separated from registration so we can
102
+ * re-register per phase without recreating.
97
103
  */
98
- registerTool(tool) {
99
- this.toolRegistry.register(tool);
104
+ _createAllTools() {
105
+ const workerLlm = new WorkerLLMCallTool(this.workspace, {
106
+ apiKey: this.config.llmApiKey,
107
+ baseUrl: this.config.llmBaseUrl,
108
+ authType: this.config.authType,
109
+ });
110
+
111
+ return {
112
+ // Always available (BUILD + DISTILL)
113
+ core: [
114
+ new SandboxExecTool(this.workspace, this.config.kcExecTimeout),
115
+ new WorkspaceFileTool(this.workspace, this.versionManager),
116
+ new DocumentParseTool(this.workspace, {
117
+ mineruApiUrl: this.config.mineruApiUrl,
118
+ mineruApiKey: this.config.mineruApiKey,
119
+ llmApiKey: this.config.llmApiKey,
120
+ llmBaseUrl: this.config.llmBaseUrl,
121
+ ocrModel: this.config.ocrModelTier1,
122
+ }),
123
+ new DocumentSearchTool(this.workspace),
124
+ new RuleCatalogTool(this.workspace),
125
+ new EvolutionCycleTool(this.workspace, this.cornerCases),
126
+ new DashboardRenderTool(this.workspace),
127
+ new AgentTool(this.workspace, (sid) => new AgentEngine({
128
+ client: this.client, config: this.config, sessionId: sid,
129
+ })),
130
+ new WebSearchTool(this.config.tavilyApiKey),
131
+ ],
132
+ // Distillation+ only (DISTILL mode)
133
+ distill: [
134
+ workerLlm,
135
+ new WorkflowRunTool(this.workspace, this.versionManager, this.confidence),
136
+ new TierDowngradeTool(this.workspace, workerLlm),
137
+ new QCSampleTool(this.workspace),
138
+ ],
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Register tools appropriate for the given phase.
144
+ * BUILD phases get core tools only.
145
+ * DISTILL phases get core + worker LLM tools.
146
+ */
147
+ _registerToolsForPhase(phase) {
148
+ this.toolRegistry = new ToolRegistry();
149
+ for (const tool of this._buildTools.core) {
150
+ this.toolRegistry.register(tool);
151
+ }
152
+ if (DISTILL_PHASES.has(phase)) {
153
+ for (const tool of this._buildTools.distill) {
154
+ this.toolRegistry.register(tool);
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Get current context usage statistics.
161
+ * @returns {{ totalTokens: number, limit: number, percentage: number }}
162
+ */
163
+ getContextStats() {
164
+ const systemPrompt = this.context.build({
165
+ skillIndex: this._skillLoader.formatForContext(),
166
+ pipelineState: this.pipelines[this.currentPhase]?.describeState?.() || null,
167
+ workspaceState: `Your workspace directory is: ${this.workspace.cwd}`,
168
+ });
169
+ const systemTokens = estimateTokens(systemPrompt);
170
+ const messageTokens = estimateMessagesTokens(this.history.messages);
171
+ const totalTokens = systemTokens + messageTokens;
172
+ const limit = this.config.kcContextLimit || 200000;
173
+ return {
174
+ totalTokens,
175
+ limit,
176
+ percentage: Math.round((totalTokens / limit) * 100),
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Compact conversation history by summarizing older messages via LLM.
182
+ * Keeps the most recent messages intact.
183
+ * @param {object} [opts]
184
+ * @param {number} [opts.recentCount=20] - Number of recent messages to keep
185
+ * @returns {Promise<{removedCount: number, retainedCount: number, summaryTokens: number}|null>}
186
+ */
187
+ async compact({ recentCount = 20 } = {}) {
188
+ if (this.history.messages.length <= recentCount) return null;
189
+
190
+ const olderMessages = this.history.messages.slice(0, -recentCount);
191
+ const recentMessages = this.history.messages.slice(-recentCount);
192
+
193
+ let summary;
194
+ try {
195
+ const summaryResp = await this.client.chat({
196
+ model: this.config.kcModel,
197
+ messages: [
198
+ {
199
+ role: "system",
200
+ content:
201
+ "You are a conversation summarizer. Produce a concise summary of the following conversation. " +
202
+ "Focus on: decisions made, files created or modified, current state of work, key findings, " +
203
+ "unresolved questions. Be specific about file paths, rule IDs, and results. Keep under 2000 tokens.",
204
+ },
205
+ {
206
+ role: "user",
207
+ content: `Summarize this conversation:\n\n${JSON.stringify(olderMessages)}`,
208
+ },
209
+ ],
210
+ maxTokens: 2048,
211
+ });
212
+ summary = summaryResp.choices?.[0]?.message?.content || null;
213
+ } catch {
214
+ // LLM summary failed — do mechanical fallback
215
+ summary = null;
216
+ }
217
+
218
+ if (!summary) {
219
+ // Mechanical fallback: extract tool names and outcomes
220
+ const lines = ["Previous conversation summary (mechanical):"];
221
+ for (const msg of olderMessages) {
222
+ if (msg.role === "user") {
223
+ lines.push(`- User: ${(msg.content || "").slice(0, 100)}`);
224
+ } else if (msg.role === "assistant" && msg.tool_calls) {
225
+ for (const tc of msg.tool_calls) {
226
+ lines.push(`- Tool call: ${tc.function?.name}`);
227
+ }
228
+ }
229
+ }
230
+ summary = lines.join("\n");
231
+ }
232
+
233
+ // Replace history
234
+ this.history._messages = [
235
+ { role: "user", content: `[Previous conversation summary]\n${summary}` },
236
+ { role: "assistant", content: "Understood. I have the context from the summary above. Continuing from where we left off." },
237
+ ...recentMessages,
238
+ ];
239
+ this.history._save();
240
+
241
+ // Log compaction event
242
+ this.eventLog.append("compact", {
243
+ removedCount: olderMessages.length,
244
+ retainedCount: recentMessages.length,
245
+ summary,
246
+ });
247
+
248
+ return {
249
+ removedCount: olderMessages.length,
250
+ retainedCount: recentMessages.length,
251
+ summaryTokens: estimateTokens(summary),
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Restore an engine from a persisted session.
257
+ * @param {object} opts
258
+ * @param {import('./llm-client.js').LLMClient} opts.client
259
+ * @param {object} opts.config
260
+ * @param {string} opts.sessionId
261
+ * @returns {Promise<AgentEngine>}
262
+ */
263
+ static async resume({ client, config, sessionId }) {
264
+ const engine = new AgentEngine({ client, config, sessionId });
265
+ const state = engine.sessionState;
266
+
267
+ if (state.exists) {
268
+ const data = state.load();
269
+ engine.currentPhase = data.currentPhase || Phase.BOOTSTRAP;
270
+ engine._phaseSummaries = data.phaseSummaries || [];
271
+ engine._registerToolsForPhase(engine.currentPhase);
272
+
273
+ // Restore pipeline milestones
274
+ const milestones = data.pipelineMilestones || {};
275
+ for (const [phase, mData] of Object.entries(milestones)) {
276
+ if (engine.pipelines[phase]?.importState) {
277
+ engine.pipelines[phase].importState(mData);
278
+ }
279
+ }
280
+
281
+ engine.eventLog.append("session_resume", {
282
+ resumedPhase: engine.currentPhase,
283
+ resumedFromSeq: data.lastEventSeq,
284
+ });
285
+ }
286
+
287
+ return engine;
288
+ }
289
+
290
+ /**
291
+ * Save current session state for future resume.
292
+ */
293
+ saveState() {
294
+ this.sessionState.save(this);
100
295
  }
101
296
 
102
297
  /**
@@ -107,19 +302,35 @@ export class AgentEngine {
107
302
  */
108
303
  async *runTurn(userMessage) {
109
304
  this.history.addUser(userMessage);
305
+ this.eventLog.append("user_message", { content: userMessage });
110
306
 
111
307
  // Pipeline state injection
112
308
  const pipeline = this.pipelines[this.currentPhase];
113
309
  const pipelineState = pipeline?.describeState?.() || null;
114
310
 
115
311
  const systemPrompt = this.context.build({
312
+ skillIndex: this._skillLoader.formatForContext(),
116
313
  pipelineState,
117
314
  workspaceState: `Your workspace directory is: ${this.workspace.cwd}`,
118
315
  });
119
316
  const tools = this.toolRegistry.schemasOpenai();
120
317
 
121
318
  while (true) {
122
- const messages = [{ role: "system", content: systemPrompt }, ...this.history.messages];
319
+ // Apply context windowing before sending to LLM
320
+ const windowed = this.contextWindow.window(this.history.messages, this._phaseSummaries);
321
+ const messages = [{ role: "system", content: systemPrompt }, ...windowed.messages];
322
+
323
+ if (windowed.wasWindowed) {
324
+ this.eventLog.append("context_windowed", {
325
+ removedCount: windowed.removedCount,
326
+ totalBefore: this.history.messages.length,
327
+ });
328
+ }
329
+
330
+ this.eventLog.append("llm_start", {
331
+ model: this.config.kcModel,
332
+ messageCount: messages.length,
333
+ });
123
334
 
124
335
  try {
125
336
  let collectedText = "";
@@ -137,13 +348,11 @@ export class AgentEngine {
137
348
  const delta = chunk.choices?.[0]?.delta;
138
349
  if (!delta) continue;
139
350
 
140
- // Stream text content
141
351
  if (delta.content) {
142
352
  yield new AgentEvent({ type: "text_delta", text: delta.content });
143
353
  collectedText += delta.content;
144
354
  }
145
355
 
146
- // Accumulate tool calls from deltas
147
356
  if (delta.tool_calls) {
148
357
  for (const tcDelta of delta.tool_calls) {
149
358
  const idx = tcDelta.index;
@@ -158,7 +367,7 @@ export class AgentEngine {
158
367
  }
159
368
  }
160
369
 
161
- // Build assistant message for history
370
+ // Log the complete assistant message (coalesced, not per-delta)
162
371
  const assistantMsg = { role: "assistant", content: collectedText || null };
163
372
  if (toolCallsAcc.size > 0) {
164
373
  assistantMsg.tool_calls = Array.from(toolCallsAcc.values()).map((tc) => ({
@@ -168,9 +377,14 @@ export class AgentEngine {
168
377
  }));
169
378
  }
170
379
  this.history.addRaw(assistantMsg);
380
+ this.eventLog.append("assistant_message", {
381
+ content: collectedText || null,
382
+ toolCalls: assistantMsg.tool_calls || [],
383
+ });
171
384
 
172
- // No tool calls → turn complete
173
385
  if (toolCallsAcc.size === 0) {
386
+ this.eventLog.append("turn_complete", {});
387
+ this.saveState();
174
388
  yield new AgentEvent({ type: "turn_complete" });
175
389
  return;
176
390
  }
@@ -180,10 +394,18 @@ export class AgentEngine {
180
394
  let inputData = {};
181
395
  try {
182
396
  inputData = tc.arguments ? JSON.parse(tc.arguments) : {};
183
- } catch { /* ignore parse errors */ }
397
+ } catch { /* ignore */ }
184
398
 
399
+ this.eventLog.append("tool_start", { name: tc.name, input: inputData });
185
400
  yield new AgentEvent({ type: "tool_start", name: tc.name, input: inputData });
401
+
186
402
  const result = await this.toolRegistry.execute(tc.name, inputData);
403
+
404
+ this.eventLog.append("tool_result", {
405
+ name: tc.name,
406
+ output: result.content?.slice(0, 5000) || "",
407
+ isError: result.isError,
408
+ });
187
409
  yield new AgentEvent({
188
410
  type: "tool_result",
189
411
  name: tc.name,
@@ -191,19 +413,27 @@ export class AgentEngine {
191
413
  isError: result.isError,
192
414
  });
193
415
 
194
- // Add tool result message
195
416
  this.history.addRaw({
196
417
  role: "tool",
197
418
  tool_call_id: tc.id,
198
419
  content: result.content,
199
420
  });
200
421
 
201
- // Pipeline controller: update state after tool execution
422
+ // Pipeline controller: update state and re-register tools on phase change
202
423
  if (pipeline?.onToolResult) {
203
424
  const pEvent = pipeline.onToolResult(tc.name, inputData, result);
204
425
  if (pEvent) {
205
426
  if (pEvent.type === "phase_ready" && pEvent.nextPhase) {
427
+ const phaseSummary = `[${this.currentPhase.toUpperCase()} completed]: ${pEvent.message || ""}`;
428
+ this._phaseSummaries.push(phaseSummary);
429
+ this.eventLog.append("phase_transition", {
430
+ from: this.currentPhase,
431
+ to: pEvent.nextPhase,
432
+ summary: phaseSummary,
433
+ });
206
434
  this.currentPhase = pEvent.nextPhase;
435
+ this._registerToolsForPhase(this.currentPhase);
436
+ this.saveState();
207
437
  }
208
438
  yield new AgentEvent({
209
439
  type: "pipeline_event",
@@ -213,9 +443,8 @@ export class AgentEngine {
213
443
  }
214
444
  }
215
445
 
216
- // Loop continues — send tool results back to LLM
217
-
218
446
  } catch (err) {
447
+ this.eventLog.append("error", { message: err.message });
219
448
  yield new AgentEvent({ type: "error", message: err.message });
220
449
  return;
221
450
  }
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { estimateTokens } from "./token-counter.js";
4
+
5
+ /**
6
+ * Append-only JSONL event log for KC agent sessions.
7
+ * Each line is a JSON object: { seq, ts, type, data }
8
+ *
9
+ * This is the source of truth for session history. ConversationHistory
10
+ * and display logs become views over this log.
11
+ */
12
+ export class EventLog {
13
+ /**
14
+ * @param {string} workspacePath - Session workspace directory
15
+ */
16
+ constructor(workspacePath) {
17
+ this._dir = path.join(workspacePath, "logs");
18
+ this._logPath = path.join(this._dir, "events.jsonl");
19
+ this._seq = 0;
20
+ this._estimatedTokens = 0;
21
+ this._initFromExisting();
22
+ }
23
+
24
+ /** Current sequence number */
25
+ get currentSeq() { return this._seq; }
26
+
27
+ /** Estimated total tokens across all events */
28
+ get estimatedTokens() { return this._estimatedTokens; }
29
+
30
+ /** Path to the log file */
31
+ get logPath() { return this._logPath; }
32
+
33
+ /**
34
+ * Initialize sequence counter and token estimate from existing log file.
35
+ */
36
+ _initFromExisting() {
37
+ if (!fs.existsSync(this._logPath)) return;
38
+ try {
39
+ const content = fs.readFileSync(this._logPath, "utf-8");
40
+ const lines = content.split("\n").filter((l) => l.trim());
41
+ for (const line of lines) {
42
+ try {
43
+ const event = JSON.parse(line);
44
+ if (event.seq > this._seq) this._seq = event.seq;
45
+ this._estimatedTokens += this._eventTokens(event);
46
+ } catch { /* skip malformed lines */ }
47
+ }
48
+ } catch { /* file read error, start fresh */ }
49
+ }
50
+
51
+ /**
52
+ * Append an event to the log.
53
+ * @param {string} type - Event type
54
+ * @param {object} [data] - Event payload
55
+ * @returns {number} The sequence number of the appended event
56
+ */
57
+ append(type, data = {}) {
58
+ this._seq++;
59
+ const event = {
60
+ seq: this._seq,
61
+ ts: new Date().toISOString(),
62
+ type,
63
+ data,
64
+ };
65
+
66
+ fs.mkdirSync(this._dir, { recursive: true });
67
+ fs.appendFileSync(this._logPath, JSON.stringify(event) + "\n", "utf-8");
68
+
69
+ this._estimatedTokens += this._eventTokens(event);
70
+ return this._seq;
71
+ }
72
+
73
+ /**
74
+ * Read events from the log with optional filtering.
75
+ * @param {object} [opts]
76
+ * @param {number} [opts.fromSeq] - Start reading from this sequence (inclusive)
77
+ * @param {number} [opts.toSeq] - Stop reading at this sequence (inclusive)
78
+ * @param {string[]} [opts.types] - Only return events of these types
79
+ * @returns {Array<object>}
80
+ */
81
+ read({ fromSeq = 0, toSeq = Infinity, types } = {}) {
82
+ if (!fs.existsSync(this._logPath)) return [];
83
+
84
+ const events = [];
85
+ const content = fs.readFileSync(this._logPath, "utf-8");
86
+ const lines = content.split("\n").filter((l) => l.trim());
87
+
88
+ for (const line of lines) {
89
+ try {
90
+ const event = JSON.parse(line);
91
+ if (event.seq < fromSeq || event.seq > toSeq) continue;
92
+ if (types && !types.includes(event.type)) continue;
93
+ events.push(event);
94
+ } catch { /* skip */ }
95
+ }
96
+
97
+ return events;
98
+ }
99
+
100
+ /**
101
+ * Estimate tokens for a single event (for running total).
102
+ * @param {object} event
103
+ * @returns {number}
104
+ */
105
+ _eventTokens(event) {
106
+ const dataStr = typeof event.data === "string"
107
+ ? event.data
108
+ : JSON.stringify(event.data || {});
109
+ return estimateTokens(dataStr);
110
+ }
111
+ }