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.
- package/bin/kc-beta.js +14 -2
- package/package.json +1 -1
- package/src/agent/context-window.js +151 -0
- package/src/agent/context.js +58 -88
- package/src/agent/engine.js +267 -38
- package/src/agent/event-log.js +111 -0
- package/src/agent/llm-client.js +352 -59
- package/src/agent/pipelines/_archive_v1/distillation.js +113 -0
- package/src/agent/pipelines/_archive_v1/extraction.js +92 -0
- package/src/agent/pipelines/_archive_v1/initializer.js +163 -0
- package/src/agent/pipelines/_archive_v1/production-qc.js +99 -0
- package/src/agent/pipelines/_archive_v1/skill-authoring.js +83 -0
- package/src/agent/pipelines/_archive_v1/skill-testing.js +111 -0
- package/src/agent/pipelines/base.js +6 -0
- package/src/agent/pipelines/distillation.js +25 -11
- package/src/agent/pipelines/extraction.js +26 -7
- package/src/agent/pipelines/initializer.js +30 -20
- package/src/agent/pipelines/production-qc.js +22 -5
- package/src/agent/pipelines/skill-authoring.js +19 -8
- package/src/agent/pipelines/skill-testing.js +26 -8
- package/src/agent/retry.js +83 -0
- package/src/agent/session-state.js +78 -0
- package/src/agent/skill-loader.js +139 -0
- package/src/agent/token-counter.js +62 -0
- package/src/agent/tools/document-parse.js +3 -3
- package/src/agent/tools/tier-downgrade.js +11 -2
- package/src/agent/tools/web-search.js +107 -0
- package/src/agent/tools/worker-llm-call.js +14 -5
- package/src/cli/components.js +16 -4
- package/src/cli/config.js +246 -0
- package/src/cli/index.js +99 -10
- package/src/cli/onboard.js +154 -48
- package/src/config.js +25 -7
- package/src/providers.js +370 -0
package/src/agent/engine.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
58
|
-
this.
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// Context windowing
|
|
69
|
+
this.contextWindow = new ContextWindow({
|
|
70
|
+
contextLimit: config.kcContextLimit || 200000,
|
|
71
|
+
reserveForResponse: config.kcMaxTokens || 65536,
|
|
72
72
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
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
|
-
*
|
|
96
|
-
*
|
|
101
|
+
* Create all tool instances. Separated from registration so we can
|
|
102
|
+
* re-register per phase without recreating.
|
|
97
103
|
*/
|
|
98
|
-
|
|
99
|
-
this.
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
+
}
|