kc-beta 0.1.2 → 0.3.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 (55) 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 +8 -4
  5. package/src/agent/engine.js +261 -8
  6. package/src/agent/event-log.js +111 -0
  7. package/src/agent/llm-client.js +352 -59
  8. package/src/agent/pipelines/base.js +6 -0
  9. package/src/agent/pipelines/distillation.js +18 -0
  10. package/src/agent/pipelines/extraction.js +21 -0
  11. package/src/agent/pipelines/initializer.js +75 -14
  12. package/src/agent/pipelines/production-qc.js +19 -0
  13. package/src/agent/pipelines/skill-authoring.js +14 -0
  14. package/src/agent/pipelines/skill-testing.js +20 -0
  15. package/src/agent/retry.js +83 -0
  16. package/src/agent/session-state.js +79 -0
  17. package/src/agent/skill-loader.js +13 -1
  18. package/src/agent/token-counter.js +62 -0
  19. package/src/agent/tools/document-parse.js +104 -21
  20. package/src/agent/tools/document-search.js +24 -8
  21. package/src/agent/tools/sandbox-exec.js +16 -5
  22. package/src/agent/tools/web-search.js +107 -0
  23. package/src/agent/tools/worker-llm-call.js +14 -5
  24. package/src/agent/tools/workspace-file.js +47 -20
  25. package/src/agent/workspace.js +24 -1
  26. package/src/cli/components.js +24 -5
  27. package/src/cli/config.js +340 -0
  28. package/src/cli/index.js +113 -11
  29. package/src/cli/onboard.js +216 -53
  30. package/src/config.js +63 -10
  31. package/src/model-tiers.json +153 -0
  32. package/src/providers.js +367 -0
  33. package/template/AGENT.md +20 -0
  34. package/template/skills/en/meta/compliance-judgment/SKILL.md +10 -42
  35. package/template/skills/en/meta/document-chunking/SKILL.md +32 -0
  36. package/template/skills/en/meta/document-parsing/SKILL.md +11 -18
  37. package/template/skills/en/meta/entity-extraction/SKILL.md +13 -28
  38. package/template/skills/en/meta/tree-processing/SKILL.md +19 -1
  39. package/template/skills/en/meta-meta/auto-model-selection/SKILL.md +53 -0
  40. package/template/skills/en/meta-meta/pdf-review-dashboard/SKILL.md +57 -0
  41. package/template/skills/en/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  42. package/template/skills/en/meta-meta/rule-extraction/SKILL.md +24 -1
  43. package/template/skills/en/meta-meta/skill-authoring/SKILL.md +6 -0
  44. package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +4 -0
  45. package/template/skills/zh/meta/compliance-judgment/SKILL.md +41 -262
  46. package/template/skills/zh/meta/document-chunking/SKILL.md +32 -0
  47. package/template/skills/zh/meta/document-parsing/SKILL.md +65 -132
  48. package/template/skills/zh/meta/entity-extraction/SKILL.md +68 -230
  49. package/template/skills/zh/meta/tree-processing/SKILL.md +82 -194
  50. package/template/skills/zh/meta-meta/auto-model-selection/SKILL.md +51 -0
  51. package/template/skills/zh/meta-meta/pdf-review-dashboard/SKILL.md +55 -0
  52. package/template/skills/zh/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  53. package/template/skills/zh/meta-meta/rule-extraction/SKILL.md +79 -164
  54. package/template/skills/zh/meta-meta/skill-authoring/SKILL.md +64 -185
  55. package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +95 -216
package/bin/kc-beta.js CHANGED
@@ -1,16 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const subcommand = process.argv[2];
3
+ // Parse --en / --zh from anywhere in argv (session-only language override)
4
+ const args = process.argv.slice(2);
5
+ let languageOverride = null;
6
+ const filtered = [];
7
+ for (const arg of args) {
8
+ if (arg === "--en") languageOverride = "en";
9
+ else if (arg === "--zh") languageOverride = "zh";
10
+ else filtered.push(arg);
11
+ }
12
+ const subcommand = filtered[0];
4
13
 
5
14
  (async () => {
6
15
  if (subcommand === "onboard" || subcommand === "setup") {
7
16
  const { onboard } = await import("../src/cli/onboard.js");
8
17
  await onboard();
18
+ } else if (subcommand === "config") {
19
+ const { configEditor } = await import("../src/cli/config.js");
20
+ await configEditor();
9
21
  } else if (subcommand === "init") {
10
22
  const { init } = await import("../src/cli/init.js");
11
23
  await init();
12
24
  } else {
13
25
  const { main } = await import("../src/cli/index.js");
14
- await main();
26
+ await main({ languageOverride });
15
27
  }
16
28
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kc-beta",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "KC Agent — LLM document verification agent (pure Node.js CLI)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,151 @@
1
+ import { estimateTokens, estimateMessagesTokens } from "./token-counter.js";
2
+
3
+ /**
4
+ * Automatic context windowing for long conversations.
5
+ * When messages approach the model's context limit, older messages
6
+ * are compressed into summaries while keeping recent messages intact.
7
+ */
8
+ export class ContextWindow {
9
+ /**
10
+ * @param {object} opts
11
+ * @param {number} opts.contextLimit - Total model context limit in tokens
12
+ * @param {number} [opts.reserveForResponse=8192] - Tokens reserved for model output
13
+ * @param {number} [opts.recentWindowSize=30] - Number of recent messages to always keep
14
+ */
15
+ constructor({ contextLimit, reserveForResponse = 8192, recentWindowSize = 30 }) {
16
+ this.contextLimit = contextLimit;
17
+ this.reserveForResponse = reserveForResponse;
18
+ this.recentWindowSize = recentWindowSize;
19
+ }
20
+
21
+ /**
22
+ * Apply windowing to a message array if it exceeds the token budget.
23
+ * @param {Array<object>} messages - Full message history
24
+ * @param {string[]} [phaseSummaries] - Summaries from completed pipeline phases
25
+ * @returns {{ messages: Array, wasWindowed: boolean, removedCount: number }}
26
+ */
27
+ window(messages, phaseSummaries = []) {
28
+ const totalTokens = estimateMessagesTokens(messages);
29
+ const budget = this.contextLimit - this.reserveForResponse;
30
+
31
+ // If within budget, return as-is
32
+ if (totalTokens <= budget * 0.85) {
33
+ return { messages, wasWindowed: false, removedCount: 0 };
34
+ }
35
+
36
+ // Split into older and recent
37
+ const splitPoint = Math.max(0, messages.length - this.recentWindowSize);
38
+ const recentMessages = messages.slice(splitPoint);
39
+ const olderMessages = messages.slice(0, splitPoint);
40
+
41
+ if (olderMessages.length === 0) {
42
+ return { messages, wasWindowed: false, removedCount: 0 };
43
+ }
44
+
45
+ // Build a compact summary of older messages
46
+ const recentTokens = estimateMessagesTokens(recentMessages);
47
+ const summaryBudget = budget - recentTokens - 500; // 500 tokens buffer
48
+ const compactedSummary = this._compactMessages(olderMessages, phaseSummaries, summaryBudget);
49
+
50
+ const windowedMessages = [
51
+ {
52
+ role: "user",
53
+ content: `[Context Summary - Earlier conversation compressed]\n\n${compactedSummary}`,
54
+ },
55
+ {
56
+ role: "assistant",
57
+ content: "Understood. I have the context from the summary above. Continuing with the current work.",
58
+ },
59
+ ...recentMessages,
60
+ ];
61
+
62
+ return {
63
+ messages: windowedMessages,
64
+ wasWindowed: true,
65
+ removedCount: olderMessages.length,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Create a mechanical compact summary of messages.
71
+ * Groups into conversational turns and extracts key info.
72
+ * @param {Array<object>} messages
73
+ * @param {string[]} phaseSummaries
74
+ * @param {number} tokenBudget
75
+ * @returns {string}
76
+ */
77
+ _compactMessages(messages, phaseSummaries, tokenBudget) {
78
+ const parts = [];
79
+
80
+ // Phase summaries first (high signal)
81
+ if (phaseSummaries.length > 0) {
82
+ parts.push("## Phase History");
83
+ for (const s of phaseSummaries) {
84
+ parts.push(`- ${s}`);
85
+ }
86
+ parts.push("");
87
+ }
88
+
89
+ // Extract key events from older messages
90
+ parts.push("## Conversation Summary");
91
+ const turns = this._groupIntoTurns(messages);
92
+
93
+ for (const turn of turns) {
94
+ const line = this._summarizeTurn(turn);
95
+ if (line) {
96
+ parts.push(`- ${line}`);
97
+ // Check budget
98
+ if (estimateTokens(parts.join("\n")) > tokenBudget * 0.9) {
99
+ parts.push("- [earlier history truncated]");
100
+ break;
101
+ }
102
+ }
103
+ }
104
+
105
+ return parts.join("\n");
106
+ }
107
+
108
+ /**
109
+ * Group messages into user-turn blocks.
110
+ * Each turn: { user: string, tools: [{name, summary}], assistantSummary: string }
111
+ */
112
+ _groupIntoTurns(messages) {
113
+ const turns = [];
114
+ let current = null;
115
+
116
+ for (const msg of messages) {
117
+ if (msg.role === "user") {
118
+ if (current) turns.push(current);
119
+ current = { user: msg.content || "", tools: [], assistant: "" };
120
+ } else if (msg.role === "assistant" && current) {
121
+ if (msg.content) current.assistant = msg.content;
122
+ if (msg.tool_calls) {
123
+ for (const tc of msg.tool_calls) {
124
+ current.tools.push(tc.function?.name || "unknown");
125
+ }
126
+ }
127
+ }
128
+ // tool results are captured implicitly via tool names
129
+ }
130
+ if (current) turns.push(current);
131
+ return turns;
132
+ }
133
+
134
+ /**
135
+ * Summarize a single conversational turn into one line.
136
+ */
137
+ _summarizeTurn(turn) {
138
+ const userSnippet = (turn.user || "").slice(0, 80).replace(/\n/g, " ");
139
+ if (!userSnippet) return null;
140
+
141
+ let line = `User: "${userSnippet}"`;
142
+ if (turn.tools.length > 0) {
143
+ line += ` → Tools: ${turn.tools.join(", ")}`;
144
+ }
145
+ if (turn.assistant) {
146
+ const aSnippet = turn.assistant.slice(0, 60).replace(/\n/g, " ");
147
+ line += ` → "${aSnippet}..."`;
148
+ }
149
+ return line;
150
+ }
151
+ }
@@ -32,9 +32,11 @@ outcome. Handle ambiguity explicitly — note it, ask the developer user. After
32
32
  audit which regulation sections are not yet covered.
33
33
 
34
34
  ### Entity Extraction
35
- Prefer regex/Python for predictable formats. Use LLM only when semantic understanding \
36
- is required. Every extraction captures: value, evidence, source location, confidence, \
37
- method used.
35
+ Choose the cheapest method that meets accuracy threshold. Regex is the smallest \
36
+ "model" zero cost, instant, deterministic. Worker LLM handles semantic tasks \
37
+ regex cannot (contextual interpretation, misleading language, adequacy judgment). \
38
+ Try different methods, find the cost-accuracy balance. Every extraction captures: \
39
+ value, evidence, source location, confidence, method used.
38
40
 
39
41
  ### Skill Authoring
40
42
  Write each rule into a skill folder following the Anthropic skill-creator format. A \
@@ -79,13 +81,15 @@ unclear regulations with them. Present results and let them judge.`;
79
81
  export class ContextAssembler {
80
82
  /**
81
83
  * @param {object} [opts]
84
+ * @param {string} [opts.agentMd] - Content of workspace AGENT.md (per-project context)
82
85
  * @param {string} [opts.pipelineState]
83
86
  * @param {string} [opts.workspaceState]
84
87
  * @param {string} [opts.skillIndex] - Brief index of available meta skills
85
88
  * @returns {string}
86
89
  */
87
- build({ pipelineState, workspaceState, skillIndex } = {}) {
90
+ build({ agentMd, pipelineState, workspaceState, skillIndex } = {}) {
88
91
  const parts = [AGENT_IDENTITY];
92
+ if (agentMd) parts.push(agentMd);
89
93
  if (skillIndex) parts.push(skillIndex);
90
94
  if (pipelineState) parts.push(pipelineState);
91
95
  if (workspaceState) parts.push(workspaceState);
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { AgentEvent } from "./events.js";
2
4
  import { ContextAssembler } from "./context.js";
3
5
  import { ConversationHistory } from "./history.js";
@@ -18,6 +20,7 @@ import { DashboardRenderTool } from "./tools/dashboard-render.js";
18
20
  import { EvolutionCycleTool } from "./tools/evolution-cycle.js";
19
21
  import { TierDowngradeTool } from "./tools/tier-downgrade.js";
20
22
  import { AgentTool } from "./tools/agent-tool.js";
23
+ import { WebSearchTool } from "./tools/web-search.js";
21
24
  import { SkillLoader } from "./skill-loader.js";
22
25
  import { Phase } from "./pipelines/index.js";
23
26
  import { ProjectInitializer } from "./pipelines/initializer.js";
@@ -26,6 +29,10 @@ import { SkillAuthoringPipeline } from "./pipelines/skill-authoring.js";
26
29
  import { SkillTestingPipeline } from "./pipelines/skill-testing.js";
27
30
  import { DistillationEngine as DistillationPipeline } from "./pipelines/distillation.js";
28
31
  import { ProductionQCPipeline } from "./pipelines/production-qc.js";
32
+ import { EventLog } from "./event-log.js";
33
+ import { ContextWindow } from "./context-window.js";
34
+ import { SessionState } from "./session-state.js";
35
+ import { estimateTokens, estimateMessagesTokens } from "./token-counter.js";
29
36
 
30
37
  // Phases where worker LLM tools are available (DISTILL mode)
31
38
  const DISTILL_PHASES = new Set([Phase.DISTILLATION, Phase.PRODUCTION_QC]);
@@ -51,14 +58,27 @@ export class AgentEngine {
51
58
  this.context = new ContextAssembler();
52
59
 
53
60
  // Workspace + structural components
54
- this.workspace = new Workspace(config.kcWorkspaceRoot, sessionId);
61
+ this.workspace = new Workspace(config.kcWorkspaceRoot, sessionId, config.projectDir);
55
62
  this.history = new ConversationHistory(this.workspace.cwd);
56
63
  this.versionManager = new VersionManager(this.workspace.cwd);
57
64
  this.cornerCases = new CornerCaseRegistry(this.workspace.cwd);
58
65
  this.confidence = new ConfidenceScorer(this.workspace.cwd, this.cornerCases);
59
66
 
67
+ // Event log (append-only JSONL, source of truth)
68
+ this.eventLog = new EventLog(this.workspace.cwd);
69
+
70
+ // Context windowing
71
+ this.contextWindow = new ContextWindow({
72
+ contextLimit: config.kcContextLimit || 200000,
73
+ reserveForResponse: config.kcMaxTokens || 65536,
74
+ });
75
+
76
+ // Session state persistence
77
+ this.sessionState = new SessionState(this.workspace.cwd);
78
+
60
79
  // Build all tool instances (but register phase-appropriate ones)
61
80
  this._buildTools = this._createAllTools();
81
+ this._phaseSummaries = [];
62
82
 
63
83
  // Pipeline system (meta-meta skills as code)
64
84
  this.currentPhase = Phase.BOOTSTRAP;
@@ -84,11 +104,20 @@ export class AgentEngine {
84
104
  * re-register per phase without recreating.
85
105
  */
86
106
  _createAllTools() {
107
+ // Worker LLM uses separate config if set, otherwise falls back to conductor
108
+ const workerApiKey = this.config.effectiveWorkerApiKey();
109
+ const workerBaseUrl = this.config.effectiveWorkerBaseUrl();
110
+ const workerAuthType = this.config.effectiveWorkerAuthType();
111
+
87
112
  const workerLlm = new WorkerLLMCallTool(this.workspace, {
88
- apiKey: this.config.siliconflowApiKey,
89
- baseUrl: this.config.siliconflowBaseUrl,
113
+ apiKey: workerApiKey,
114
+ baseUrl: workerBaseUrl,
115
+ authType: workerAuthType,
90
116
  });
91
117
 
118
+ // OCR/VLM uses worker config (VLM is a type of worker LLM)
119
+ const vlmModel = this.config.vlmTier1 || "";
120
+
92
121
  return {
93
122
  // Always available (BUILD + DISTILL)
94
123
  core: [
@@ -97,9 +126,9 @@ export class AgentEngine {
97
126
  new DocumentParseTool(this.workspace, {
98
127
  mineruApiUrl: this.config.mineruApiUrl,
99
128
  mineruApiKey: this.config.mineruApiKey,
100
- siliconflowApiKey: this.config.siliconflowApiKey,
101
- siliconflowBaseUrl: this.config.siliconflowBaseUrl,
102
- ocrModel: this.config.ocrModelTier1,
129
+ llmApiKey: workerApiKey,
130
+ llmBaseUrl: workerBaseUrl,
131
+ ocrModel: vlmModel,
103
132
  }),
104
133
  new DocumentSearchTool(this.workspace),
105
134
  new RuleCatalogTool(this.workspace),
@@ -108,6 +137,7 @@ export class AgentEngine {
108
137
  new AgentTool(this.workspace, (sid) => new AgentEngine({
109
138
  client: this.client, config: this.config, sessionId: sid,
110
139
  })),
140
+ new WebSearchTool(this.config.tavilyApiKey),
111
141
  ],
112
142
  // Distillation+ only (DISTILL mode)
113
143
  distill: [
@@ -136,6 +166,189 @@ export class AgentEngine {
136
166
  }
137
167
  }
138
168
 
169
+ /**
170
+ * Read AGENT.md from workspace (per-project context).
171
+ * Returns content string or empty string if not found.
172
+ */
173
+ _readAgentMd() {
174
+ const agentMdPath = path.join(this.workspace.cwd, "AGENT.md");
175
+ try {
176
+ if (fs.existsSync(agentMdPath)) {
177
+ return fs.readFileSync(agentMdPath, "utf-8");
178
+ }
179
+ } catch { /* ignore */ }
180
+ return "";
181
+ }
182
+
183
+ /**
184
+ * Build the workspace/project directory state string for the system prompt.
185
+ */
186
+ _buildWorkspaceState() {
187
+ const lines = [
188
+ `## Directory Layout`,
189
+ `**KC Workspace:** ${this.workspace.cwd}`,
190
+ ` Use scope="workspace" (default). Write all working files here (rules, skills, workflows, results, logs).`,
191
+ ];
192
+ if (this.workspace.projectDir) {
193
+ lines.push(
194
+ `**Project Directory:** ${this.workspace.projectDir}`,
195
+ ` Use scope="project" to read/write files in the user's project folder.`,
196
+ ` This is where the user's source regulations, samples, and reference documents are.`,
197
+ ``,
198
+ `Read source documents from the project directory. Write KC outputs to the workspace.`,
199
+ `Write user-facing exports (reports, results) to the project directory when the user asks.`,
200
+ );
201
+ }
202
+ return lines.join("\n");
203
+ }
204
+
205
+ /**
206
+ * Get current context usage statistics.
207
+ * @returns {{ totalTokens: number, limit: number, percentage: number }}
208
+ */
209
+ getContextStats() {
210
+ const systemPrompt = this.context.build({
211
+ agentMd: this._readAgentMd(),
212
+ skillIndex: this._skillLoader.formatForContext(),
213
+ pipelineState: this.pipelines[this.currentPhase]?.describeState?.() || null,
214
+ workspaceState: this._buildWorkspaceState(),
215
+ });
216
+ const systemTokens = estimateTokens(systemPrompt);
217
+ const messageTokens = estimateMessagesTokens(this.history.messages);
218
+ const totalTokens = systemTokens + messageTokens;
219
+ const limit = this.config.kcContextLimit || 200000;
220
+ return {
221
+ totalTokens,
222
+ limit,
223
+ percentage: Math.round((totalTokens / limit) * 100),
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Compact conversation history by summarizing older messages via LLM.
229
+ * Keeps the most recent messages intact.
230
+ * @param {object} [opts]
231
+ * @param {number} [opts.recentCount=20] - Number of recent messages to keep
232
+ * @returns {Promise<{removedCount: number, retainedCount: number, summaryTokens: number}|null>}
233
+ */
234
+ async compact({ recentCount = 20 } = {}) {
235
+ if (this.history.messages.length <= recentCount) return null;
236
+
237
+ const olderMessages = this.history.messages.slice(0, -recentCount);
238
+ const recentMessages = this.history.messages.slice(-recentCount);
239
+
240
+ let summary;
241
+ try {
242
+ const summaryResp = await this.client.chat({
243
+ model: this.config.kcModel,
244
+ messages: [
245
+ {
246
+ role: "system",
247
+ content:
248
+ "You are a conversation summarizer. Produce a concise summary of the following conversation. " +
249
+ "Focus on: decisions made, files created or modified, current state of work, key findings, " +
250
+ "unresolved questions. Be specific about file paths, rule IDs, and results. Keep under 2000 tokens.",
251
+ },
252
+ {
253
+ role: "user",
254
+ content: `Summarize this conversation:\n\n${JSON.stringify(olderMessages)}`,
255
+ },
256
+ ],
257
+ maxTokens: 2048,
258
+ });
259
+ summary = summaryResp.choices?.[0]?.message?.content || null;
260
+ } catch {
261
+ // LLM summary failed — do mechanical fallback
262
+ summary = null;
263
+ }
264
+
265
+ if (!summary) {
266
+ // Mechanical fallback: extract tool names and outcomes
267
+ const lines = ["Previous conversation summary (mechanical):"];
268
+ for (const msg of olderMessages) {
269
+ if (msg.role === "user") {
270
+ lines.push(`- User: ${(msg.content || "").slice(0, 100)}`);
271
+ } else if (msg.role === "assistant" && msg.tool_calls) {
272
+ for (const tc of msg.tool_calls) {
273
+ lines.push(`- Tool call: ${tc.function?.name}`);
274
+ }
275
+ }
276
+ }
277
+ summary = lines.join("\n");
278
+ }
279
+
280
+ // Replace history
281
+ this.history._messages = [
282
+ { role: "user", content: `[Previous conversation summary]\n${summary}` },
283
+ { role: "assistant", content: "Understood. I have the context from the summary above. Continuing from where we left off." },
284
+ ...recentMessages,
285
+ ];
286
+ this.history._save();
287
+
288
+ // Log compaction event
289
+ this.eventLog.append("compact", {
290
+ removedCount: olderMessages.length,
291
+ retainedCount: recentMessages.length,
292
+ summary,
293
+ });
294
+
295
+ return {
296
+ removedCount: olderMessages.length,
297
+ retainedCount: recentMessages.length,
298
+ summaryTokens: estimateTokens(summary),
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Restore an engine from a persisted session.
304
+ * @param {object} opts
305
+ * @param {import('./llm-client.js').LLMClient} opts.client
306
+ * @param {object} opts.config
307
+ * @param {string} opts.sessionId
308
+ * @returns {Promise<AgentEngine>}
309
+ */
310
+ static async resume({ client, config, sessionId }) {
311
+ const engine = new AgentEngine({ client, config, sessionId });
312
+ const state = engine.sessionState;
313
+
314
+ if (state.exists) {
315
+ const data = state.load();
316
+ engine.currentPhase = data.currentPhase || Phase.BOOTSTRAP;
317
+ engine._phaseSummaries = data.phaseSummaries || [];
318
+ engine._registerToolsForPhase(engine.currentPhase);
319
+
320
+ // Restore project directory from saved state
321
+ if (data.projectDir) {
322
+ if (fs.existsSync(data.projectDir)) {
323
+ engine.workspace.projectDir = data.projectDir;
324
+ }
325
+ // If dir no longer exists, projectDir stays as whatever was passed at launch
326
+ }
327
+
328
+ // Restore pipeline milestones
329
+ const milestones = data.pipelineMilestones || {};
330
+ for (const [phase, mData] of Object.entries(milestones)) {
331
+ if (engine.pipelines[phase]?.importState) {
332
+ engine.pipelines[phase].importState(mData);
333
+ }
334
+ }
335
+
336
+ engine.eventLog.append("session_resume", {
337
+ resumedPhase: engine.currentPhase,
338
+ resumedFromSeq: data.lastEventSeq,
339
+ });
340
+ }
341
+
342
+ return engine;
343
+ }
344
+
345
+ /**
346
+ * Save current session state for future resume.
347
+ */
348
+ saveState() {
349
+ this.sessionState.save(this);
350
+ }
351
+
139
352
  /**
140
353
  * Run one conversation turn. Yields AgentEvent objects.
141
354
  * Loops: LLM call -> tool execution -> LLM call ... until no tool calls.
@@ -144,20 +357,36 @@ export class AgentEngine {
144
357
  */
145
358
  async *runTurn(userMessage) {
146
359
  this.history.addUser(userMessage);
360
+ this.eventLog.append("user_message", { content: userMessage });
147
361
 
148
362
  // Pipeline state injection
149
363
  const pipeline = this.pipelines[this.currentPhase];
150
364
  const pipelineState = pipeline?.describeState?.() || null;
151
365
 
152
366
  const systemPrompt = this.context.build({
367
+ agentMd: this._readAgentMd(),
153
368
  skillIndex: this._skillLoader.formatForContext(),
154
369
  pipelineState,
155
- workspaceState: `Your workspace directory is: ${this.workspace.cwd}`,
370
+ workspaceState: this._buildWorkspaceState(),
156
371
  });
157
372
  const tools = this.toolRegistry.schemasOpenai();
158
373
 
159
374
  while (true) {
160
- const messages = [{ role: "system", content: systemPrompt }, ...this.history.messages];
375
+ // Apply context windowing before sending to LLM
376
+ const windowed = this.contextWindow.window(this.history.messages, this._phaseSummaries);
377
+ const messages = [{ role: "system", content: systemPrompt }, ...windowed.messages];
378
+
379
+ if (windowed.wasWindowed) {
380
+ this.eventLog.append("context_windowed", {
381
+ removedCount: windowed.removedCount,
382
+ totalBefore: this.history.messages.length,
383
+ });
384
+ }
385
+
386
+ this.eventLog.append("llm_start", {
387
+ model: this.config.kcModel,
388
+ messageCount: messages.length,
389
+ });
161
390
 
162
391
  try {
163
392
  let collectedText = "";
@@ -194,6 +423,7 @@ export class AgentEngine {
194
423
  }
195
424
  }
196
425
 
426
+ // Log the complete assistant message (coalesced, not per-delta)
197
427
  const assistantMsg = { role: "assistant", content: collectedText || null };
198
428
  if (toolCallsAcc.size > 0) {
199
429
  assistantMsg.tool_calls = Array.from(toolCallsAcc.values()).map((tc) => ({
@@ -203,8 +433,14 @@ export class AgentEngine {
203
433
  }));
204
434
  }
205
435
  this.history.addRaw(assistantMsg);
436
+ this.eventLog.append("assistant_message", {
437
+ content: collectedText || null,
438
+ toolCalls: assistantMsg.tool_calls || [],
439
+ });
206
440
 
207
441
  if (toolCallsAcc.size === 0) {
442
+ this.eventLog.append("turn_complete", {});
443
+ this.saveState();
208
444
  yield new AgentEvent({ type: "turn_complete" });
209
445
  return;
210
446
  }
@@ -216,8 +452,16 @@ export class AgentEngine {
216
452
  inputData = tc.arguments ? JSON.parse(tc.arguments) : {};
217
453
  } catch { /* ignore */ }
218
454
 
455
+ this.eventLog.append("tool_start", { name: tc.name, input: inputData });
219
456
  yield new AgentEvent({ type: "tool_start", name: tc.name, input: inputData });
457
+
220
458
  const result = await this.toolRegistry.execute(tc.name, inputData);
459
+
460
+ this.eventLog.append("tool_result", {
461
+ name: tc.name,
462
+ output: result.content?.slice(0, 5000) || "",
463
+ isError: result.isError,
464
+ });
221
465
  yield new AgentEvent({
222
466
  type: "tool_result",
223
467
  name: tc.name,
@@ -236,8 +480,16 @@ export class AgentEngine {
236
480
  const pEvent = pipeline.onToolResult(tc.name, inputData, result);
237
481
  if (pEvent) {
238
482
  if (pEvent.type === "phase_ready" && pEvent.nextPhase) {
483
+ const phaseSummary = `[${this.currentPhase.toUpperCase()} completed]: ${pEvent.message || ""}`;
484
+ this._phaseSummaries.push(phaseSummary);
485
+ this.eventLog.append("phase_transition", {
486
+ from: this.currentPhase,
487
+ to: pEvent.nextPhase,
488
+ summary: phaseSummary,
489
+ });
239
490
  this.currentPhase = pEvent.nextPhase;
240
491
  this._registerToolsForPhase(this.currentPhase);
492
+ this.saveState();
241
493
  }
242
494
  yield new AgentEvent({
243
495
  type: "pipeline_event",
@@ -248,6 +500,7 @@ export class AgentEngine {
248
500
  }
249
501
 
250
502
  } catch (err) {
503
+ this.eventLog.append("error", { message: err.message });
251
504
  yield new AgentEvent({ type: "error", message: err.message });
252
505
  return;
253
506
  }