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.
- 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 +8 -4
- package/src/agent/engine.js +261 -8
- package/src/agent/event-log.js +111 -0
- package/src/agent/llm-client.js +352 -59
- package/src/agent/pipelines/base.js +6 -0
- package/src/agent/pipelines/distillation.js +18 -0
- package/src/agent/pipelines/extraction.js +21 -0
- package/src/agent/pipelines/initializer.js +75 -14
- package/src/agent/pipelines/production-qc.js +19 -0
- package/src/agent/pipelines/skill-authoring.js +14 -0
- package/src/agent/pipelines/skill-testing.js +20 -0
- package/src/agent/retry.js +83 -0
- package/src/agent/session-state.js +79 -0
- package/src/agent/skill-loader.js +13 -1
- package/src/agent/token-counter.js +62 -0
- package/src/agent/tools/document-parse.js +104 -21
- package/src/agent/tools/document-search.js +24 -8
- package/src/agent/tools/sandbox-exec.js +16 -5
- package/src/agent/tools/web-search.js +107 -0
- package/src/agent/tools/worker-llm-call.js +14 -5
- package/src/agent/tools/workspace-file.js +47 -20
- package/src/agent/workspace.js +24 -1
- package/src/cli/components.js +24 -5
- package/src/cli/config.js +340 -0
- package/src/cli/index.js +113 -11
- package/src/cli/onboard.js +216 -53
- package/src/config.js +63 -10
- package/src/model-tiers.json +153 -0
- package/src/providers.js +367 -0
- package/template/AGENT.md +20 -0
- package/template/skills/en/meta/compliance-judgment/SKILL.md +10 -42
- package/template/skills/en/meta/document-chunking/SKILL.md +32 -0
- package/template/skills/en/meta/document-parsing/SKILL.md +11 -18
- package/template/skills/en/meta/entity-extraction/SKILL.md +13 -28
- package/template/skills/en/meta/tree-processing/SKILL.md +19 -1
- package/template/skills/en/meta-meta/auto-model-selection/SKILL.md +53 -0
- package/template/skills/en/meta-meta/pdf-review-dashboard/SKILL.md +57 -0
- package/template/skills/en/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
- package/template/skills/en/meta-meta/rule-extraction/SKILL.md +24 -1
- package/template/skills/en/meta-meta/skill-authoring/SKILL.md +6 -0
- package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +4 -0
- package/template/skills/zh/meta/compliance-judgment/SKILL.md +41 -262
- package/template/skills/zh/meta/document-chunking/SKILL.md +32 -0
- package/template/skills/zh/meta/document-parsing/SKILL.md +65 -132
- package/template/skills/zh/meta/entity-extraction/SKILL.md +68 -230
- package/template/skills/zh/meta/tree-processing/SKILL.md +82 -194
- package/template/skills/zh/meta-meta/auto-model-selection/SKILL.md +51 -0
- package/template/skills/zh/meta-meta/pdf-review-dashboard/SKILL.md +55 -0
- package/template/skills/zh/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
- package/template/skills/zh/meta-meta/rule-extraction/SKILL.md +79 -164
- package/template/skills/zh/meta-meta/skill-authoring/SKILL.md +64 -185
- 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
|
-
|
|
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
|
@@ -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
|
+
}
|
package/src/agent/context.js
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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);
|
package/src/agent/engine.js
CHANGED
|
@@ -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:
|
|
89
|
-
baseUrl:
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
ocrModel:
|
|
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:
|
|
370
|
+
workspaceState: this._buildWorkspaceState(),
|
|
156
371
|
});
|
|
157
372
|
const tools = this.toolRegistry.schemasOpenai();
|
|
158
373
|
|
|
159
374
|
while (true) {
|
|
160
|
-
|
|
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
|
}
|