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
|
@@ -8,15 +8,27 @@ import { BaseTool, ToolResult } from "./base.js";
|
|
|
8
8
|
* the configured API provider.
|
|
9
9
|
*/
|
|
10
10
|
export class WorkerLLMCallTool extends BaseTool {
|
|
11
|
-
constructor(workspace, { apiKey, baseUrl } = {}) {
|
|
11
|
+
constructor(workspace, { apiKey, baseUrl, authType = "bearer" } = {}) {
|
|
12
12
|
super();
|
|
13
13
|
this._workspace = workspace;
|
|
14
14
|
this._apiKey = apiKey || "";
|
|
15
15
|
this._baseUrl = (baseUrl || "https://api.siliconflow.cn/v1").replace(/\/+$/, "");
|
|
16
|
+
this._authType = authType;
|
|
16
17
|
this._tierModels = {};
|
|
17
18
|
this._loadTiers();
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
_buildHeaders() {
|
|
22
|
+
const headers = { "Content-Type": "application/json" };
|
|
23
|
+
if (this._authType === "x-api-key") {
|
|
24
|
+
headers["x-api-key"] = this._apiKey;
|
|
25
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
26
|
+
} else {
|
|
27
|
+
headers["Authorization"] = `Bearer ${this._apiKey}`;
|
|
28
|
+
}
|
|
29
|
+
return headers;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
_loadTiers() {
|
|
21
33
|
const envPath = path.join(this._workspace.cwd, ".env");
|
|
22
34
|
if (!fs.existsSync(envPath)) return;
|
|
@@ -78,10 +90,7 @@ export class WorkerLLMCallTool extends BaseTool {
|
|
|
78
90
|
try {
|
|
79
91
|
const resp = await fetch(`${this._baseUrl}/chat/completions`, {
|
|
80
92
|
method: "POST",
|
|
81
|
-
headers:
|
|
82
|
-
"Authorization": `Bearer ${this._apiKey}`,
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
},
|
|
93
|
+
headers: this._buildHeaders(),
|
|
85
94
|
body: JSON.stringify({ model, messages, max_tokens: maxTokens }),
|
|
86
95
|
signal: AbortSignal.timeout(120000),
|
|
87
96
|
});
|
package/src/cli/components.js
CHANGED
|
@@ -12,7 +12,7 @@ const COOKING_WORDS = [
|
|
|
12
12
|
"Stewing", "Tempering", "Whisking", "Zesting", "Garnishing", "Drizzling",
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
export function CookingSpinner() {
|
|
15
|
+
export function CookingSpinner({ status }) {
|
|
16
16
|
const [idx, setIdx] = useState(Math.floor(Math.random() * COOKING_WORDS.length));
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
@@ -20,9 +20,11 @@ export function CookingSpinner() {
|
|
|
20
20
|
return () => clearInterval(timer);
|
|
21
21
|
}, []);
|
|
22
22
|
|
|
23
|
+
const displayText = status || `${COOKING_WORDS[idx]}...`;
|
|
24
|
+
|
|
23
25
|
return h(Box, null,
|
|
24
26
|
h(Text, { color: "yellow" }, " * "),
|
|
25
|
-
h(Text, { dimColor: true },
|
|
27
|
+
h(Text, { dimColor: true }, displayText),
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -30,13 +32,23 @@ export function CookingSpinner() {
|
|
|
30
32
|
|
|
31
33
|
const LENAT_QUOTE = "Intelligence is ten million rules.";
|
|
32
34
|
|
|
33
|
-
export function StatusBar({ sessionId, phase }) {
|
|
35
|
+
export function StatusBar({ sessionId, phase, contextTokens, contextLimit }) {
|
|
36
|
+
const pct = contextLimit ? Math.round((contextTokens / contextLimit) * 100) : 0;
|
|
37
|
+
const ctxColor = pct > 80 ? "red" : pct > 60 ? "yellow" : "green";
|
|
38
|
+
const ctxLabel = contextTokens >= 1000
|
|
39
|
+
? `${(contextTokens / 1000).toFixed(1)}k`
|
|
40
|
+
: `${contextTokens || 0}`;
|
|
41
|
+
const limitLabel = contextLimit >= 1000
|
|
42
|
+
? `${(contextLimit / 1000).toFixed(0)}k`
|
|
43
|
+
: `${contextLimit || 0}`;
|
|
44
|
+
|
|
34
45
|
return h(Box, { marginTop: 0 },
|
|
35
46
|
h(Text, { dimColor: true }, " ⏵⏵ KC Agent CLI "),
|
|
36
47
|
h(Text, { dimColor: true }, sessionId ? `[${sessionId}]` : ""),
|
|
37
48
|
phase ? h(Text, { color: "cyan" }, ` ${phase.toUpperCase()}`) : null,
|
|
38
49
|
h(Text, { color: "green" }, " ● "),
|
|
39
|
-
h(Text, {
|
|
50
|
+
h(Text, { color: ctxColor }, `CTX: ${ctxLabel}/${limitLabel} (${pct}%)`),
|
|
51
|
+
h(Text, { dimColor: true }, ` · ${LENAT_QUOTE}`),
|
|
40
52
|
);
|
|
41
53
|
}
|
|
42
54
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { getProviders, getProviderById, getProviderLabels } from "../providers.js";
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), ".kc_agent");
|
|
8
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
9
|
+
|
|
10
|
+
const ESC = "\x1b[";
|
|
11
|
+
const RESET = `${ESC}0m`;
|
|
12
|
+
const BOLD = `${ESC}1m`;
|
|
13
|
+
const DIM = `${ESC}2m`;
|
|
14
|
+
const GREEN = `${ESC}32m`;
|
|
15
|
+
const CYAN = `${ESC}36m`;
|
|
16
|
+
const GRAY = `${ESC}90m`;
|
|
17
|
+
const YELLOW = `${ESC}33m`;
|
|
18
|
+
const RED = `${ESC}31m`;
|
|
19
|
+
|
|
20
|
+
const L = {
|
|
21
|
+
en: {
|
|
22
|
+
title: "KC Agent Configuration",
|
|
23
|
+
noConfig: "No config found. Run 'kc-beta onboard' first.",
|
|
24
|
+
menu: "Configuration Categories",
|
|
25
|
+
choose: "Choose category (q to quit)",
|
|
26
|
+
categories: ["LLM Provider & API Key", "Model Tiers", "Quality Thresholds", "Language"],
|
|
27
|
+
saved: "Saved.",
|
|
28
|
+
back: "← Back to menu",
|
|
29
|
+
enterKeep: "Press Enter to keep",
|
|
30
|
+
enterDefault: "Press Enter to use default",
|
|
31
|
+
currentValue: "current",
|
|
32
|
+
provider: "Provider",
|
|
33
|
+
baseUrl: "Base URL",
|
|
34
|
+
apiKey: "API Key",
|
|
35
|
+
conductor: "Conductor Model",
|
|
36
|
+
language: "Language",
|
|
37
|
+
langOptions: ["English", "中文"],
|
|
38
|
+
},
|
|
39
|
+
zh: {
|
|
40
|
+
title: "KC Agent 配置",
|
|
41
|
+
noConfig: "未找到配置。请先运行 'kc-beta onboard'。",
|
|
42
|
+
menu: "配置类别",
|
|
43
|
+
choose: "选择类别(q 退出)",
|
|
44
|
+
categories: ["大模型服务商 & API 密钥", "模型分层", "质量阈值", "语言"],
|
|
45
|
+
saved: "已保存。",
|
|
46
|
+
back: "← 返回菜单",
|
|
47
|
+
enterKeep: "回车保留当前值",
|
|
48
|
+
enterDefault: "回车使用默认值",
|
|
49
|
+
currentValue: "当前",
|
|
50
|
+
provider: "服务商",
|
|
51
|
+
baseUrl: "接口地址",
|
|
52
|
+
apiKey: "API 密钥",
|
|
53
|
+
conductor: "主模型",
|
|
54
|
+
language: "语言",
|
|
55
|
+
langOptions: ["English", "中文"],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function loadConfig() {
|
|
60
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
61
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")); } catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function saveConfig(config) {
|
|
67
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
68
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ask(rl, question, defaultValue = "", hint = "") {
|
|
72
|
+
const suffix = defaultValue ? ` ${DIM}[${defaultValue}]${RESET}` : "";
|
|
73
|
+
const hintText = hint
|
|
74
|
+
? ` ${GRAY}(${hint})${RESET}`
|
|
75
|
+
: defaultValue
|
|
76
|
+
? ` ${GRAY}(Press Enter to keep)${RESET}`
|
|
77
|
+
: "";
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
rl.question(`${question}${suffix}${hintText}: `, (answer) => resolve(answer.trim() || defaultValue));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function maskKey(key) {
|
|
84
|
+
if (!key || key.length < 10) return key || "";
|
|
85
|
+
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Category 1: LLM Provider & API Key
|
|
90
|
+
*/
|
|
91
|
+
async function editProvider(rl, config, t) {
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(` ${BOLD}${t.categories[0]}${RESET}`);
|
|
94
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
95
|
+
|
|
96
|
+
// Show current
|
|
97
|
+
const currentProvider = getProviderById(config.provider);
|
|
98
|
+
console.log(` ${DIM}${t.currentValue}: ${config.provider} (${currentProvider?.name || "unknown"})${RESET}`);
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
// Provider selection
|
|
102
|
+
const providers = getProviders();
|
|
103
|
+
const labels = getProviderLabels(config.language || "en");
|
|
104
|
+
console.log(` ${CYAN}${t.provider}:${RESET}`);
|
|
105
|
+
for (let i = 0; i < labels.length; i++) {
|
|
106
|
+
const marker = providers[i].id === config.provider ? ` ${GREEN}(${t.currentValue})${RESET}` : "";
|
|
107
|
+
console.log(` ${i + 1}. ${labels[i].label}${marker}`);
|
|
108
|
+
}
|
|
109
|
+
const currentIdx = providers.findIndex((p) => p.id === config.provider);
|
|
110
|
+
const providerChoice = await ask(rl, ` ${GRAY}>${RESET} ${t.choose.split(" (")[0]}`, String(currentIdx + 1 || 1));
|
|
111
|
+
const provIdx = parseInt(providerChoice, 10) - 1;
|
|
112
|
+
const provider = providers[Math.max(0, Math.min(provIdx, providers.length - 1))];
|
|
113
|
+
config.provider = provider.id;
|
|
114
|
+
config.auth_type = provider.authType;
|
|
115
|
+
config.api_format = provider.apiFormat;
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
// Base URL
|
|
119
|
+
if (provider.id === "custom") {
|
|
120
|
+
config.base_url = await ask(rl, ` ${CYAN}${t.baseUrl}${RESET}`, config.base_url || "");
|
|
121
|
+
} else {
|
|
122
|
+
const defaultUrl = provider.baseUrl;
|
|
123
|
+
console.log(` ${CYAN}${t.baseUrl}${RESET}: ${DIM}${defaultUrl}${RESET}`);
|
|
124
|
+
config.base_url = defaultUrl;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// API Key
|
|
128
|
+
const masked = maskKey(config.api_key);
|
|
129
|
+
const keyPrompt = masked
|
|
130
|
+
? ` ${CYAN}${t.apiKey}${RESET} ${DIM}(${masked})${RESET}`
|
|
131
|
+
: ` ${CYAN}${t.apiKey}${RESET}`;
|
|
132
|
+
const newKey = await ask(rl, keyPrompt, "", masked ? t.enterKeep : "");
|
|
133
|
+
if (newKey) config.api_key = newKey;
|
|
134
|
+
|
|
135
|
+
// Conductor model
|
|
136
|
+
console.log();
|
|
137
|
+
const defaultModel = provider.defaultModel || config.conductor_model || "";
|
|
138
|
+
config.conductor_model = await ask(rl, ` ${CYAN}${t.conductor}${RESET}`, config.conductor_model || defaultModel, t.enterKeep);
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Category 2: Model Tiers
|
|
144
|
+
*/
|
|
145
|
+
async function editTiers(rl, config, t) {
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(` ${BOLD}${t.categories[1]}${RESET}`);
|
|
148
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
149
|
+
console.log();
|
|
150
|
+
|
|
151
|
+
const tiers = config.tiers || {};
|
|
152
|
+
const provider = getProviderById(config.provider);
|
|
153
|
+
const defaults = provider?.defaultTiers || {};
|
|
154
|
+
|
|
155
|
+
for (const tier of ["tier1", "tier2", "tier3", "tier4"]) {
|
|
156
|
+
const current = tiers[tier] || defaults[tier] || "";
|
|
157
|
+
tiers[tier] = await ask(rl, ` ${CYAN}${tier.toUpperCase()}${RESET}`, current, t.enterKeep);
|
|
158
|
+
}
|
|
159
|
+
config.tiers = tiers;
|
|
160
|
+
console.log();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Category 3: Quality Thresholds
|
|
165
|
+
*/
|
|
166
|
+
async function editThresholds(rl, config, t) {
|
|
167
|
+
console.log();
|
|
168
|
+
console.log(` ${BOLD}${t.categories[2]}${RESET}`);
|
|
169
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
170
|
+
console.log();
|
|
171
|
+
|
|
172
|
+
config.accuracy_threshold = parseFloat(
|
|
173
|
+
await ask(rl, ` ${CYAN}Accuracy threshold${RESET}`, String(config.accuracy_threshold ?? 0.9), t.enterKeep)
|
|
174
|
+
);
|
|
175
|
+
config.systemic_threshold = parseFloat(
|
|
176
|
+
await ask(rl, ` ${CYAN}Systemic threshold${RESET}`, String(config.systemic_threshold ?? 0.10), t.enterKeep)
|
|
177
|
+
);
|
|
178
|
+
config.spot_check_rate = parseFloat(
|
|
179
|
+
await ask(rl, ` ${CYAN}Spot-check rate${RESET}`, String(config.spot_check_rate ?? 0.10), t.enterKeep)
|
|
180
|
+
);
|
|
181
|
+
config.tier_tolerance = parseFloat(
|
|
182
|
+
await ask(rl, ` ${CYAN}Tier downgrade tolerance${RESET}`, String(config.tier_tolerance ?? 0.05), t.enterKeep)
|
|
183
|
+
);
|
|
184
|
+
console.log();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Category 4: Language
|
|
189
|
+
*/
|
|
190
|
+
async function editLanguage(rl, config, t) {
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(` ${BOLD}${t.categories[3]}${RESET}`);
|
|
193
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
194
|
+
console.log();
|
|
195
|
+
|
|
196
|
+
console.log(` ${CYAN}${t.language}:${RESET}`);
|
|
197
|
+
console.log(` 1. ${t.langOptions[0]}${config.language === "en" ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
|
|
198
|
+
console.log(` 2. ${t.langOptions[1]}${config.language === "zh" ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
|
|
199
|
+
const langDefault = config.language === "zh" ? "2" : "1";
|
|
200
|
+
const langChoice = await ask(rl, ` ${GRAY}>${RESET}`, langDefault);
|
|
201
|
+
config.language = langChoice === "2" ? "zh" : "en";
|
|
202
|
+
console.log();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const CATEGORY_HANDLERS = [editProvider, editTiers, editThresholds, editLanguage];
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Main config editor loop.
|
|
209
|
+
*/
|
|
210
|
+
export async function configEditor() {
|
|
211
|
+
const config = loadConfig();
|
|
212
|
+
if (!config) {
|
|
213
|
+
console.log(`\n ${RED}${L.en.noConfig}${RESET}\n`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const lang = config.language || "en";
|
|
218
|
+
const t = L[lang];
|
|
219
|
+
|
|
220
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
221
|
+
|
|
222
|
+
while (true) {
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(` ${BOLD}${t.title}${RESET}`);
|
|
225
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
226
|
+
console.log();
|
|
227
|
+
for (let i = 0; i < t.categories.length; i++) {
|
|
228
|
+
console.log(` ${i + 1}. ${t.categories[i]}`);
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
|
|
232
|
+
const choice = await ask(rl, ` ${GRAY}>${RESET} ${t.choose}`, "");
|
|
233
|
+
|
|
234
|
+
if (choice === "q" || choice === "Q" || choice === "") break;
|
|
235
|
+
|
|
236
|
+
const idx = parseInt(choice, 10) - 1;
|
|
237
|
+
if (idx >= 0 && idx < CATEGORY_HANDLERS.length) {
|
|
238
|
+
await CATEGORY_HANDLERS[idx](rl, config, t);
|
|
239
|
+
saveConfig(config);
|
|
240
|
+
console.log(` ${GREEN}✓${RESET} ${t.saved}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
rl.close();
|
|
245
|
+
console.log();
|
|
246
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -29,11 +29,23 @@ function App({ engine, config }) {
|
|
|
29
29
|
const [sessionId, setSessionId] = useState(engine.workspace.sessionId);
|
|
30
30
|
const [phase, setPhase] = useState(engine.currentPhase);
|
|
31
31
|
const [showWelcome, setShowWelcome] = useState(true);
|
|
32
|
+
const [spinnerStatus, setSpinnerStatus] = useState(null);
|
|
33
|
+
const [contextTokens, setContextTokens] = useState(0);
|
|
34
|
+
const [contextLimit, setContextLimit] = useState(config.kcContextLimit || 200000);
|
|
32
35
|
|
|
33
36
|
const engineRef = useRef(engine);
|
|
34
37
|
const streamingRef = useRef(false);
|
|
35
38
|
const queueRef = useRef([]);
|
|
36
39
|
|
|
40
|
+
// Update context stats
|
|
41
|
+
const updateContextStats = useCallback(() => {
|
|
42
|
+
try {
|
|
43
|
+
const stats = engineRef.current.getContextStats();
|
|
44
|
+
setContextTokens(stats.totalTokens);
|
|
45
|
+
setContextLimit(stats.limit);
|
|
46
|
+
} catch { /* ignore */ }
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
37
49
|
const addMessage = useCallback((msg) => {
|
|
38
50
|
setMessages((prev) => [...prev, msg]);
|
|
39
51
|
}, []);
|
|
@@ -43,6 +55,7 @@ function App({ engine, config }) {
|
|
|
43
55
|
setStreaming(true);
|
|
44
56
|
setStreamingText("");
|
|
45
57
|
setCurrentTool(null);
|
|
58
|
+
setSpinnerStatus("Thinking...");
|
|
46
59
|
|
|
47
60
|
let accumulated = "";
|
|
48
61
|
|
|
@@ -52,6 +65,7 @@ function App({ engine, config }) {
|
|
|
52
65
|
case "text_delta":
|
|
53
66
|
accumulated += event.text ?? "";
|
|
54
67
|
setStreamingText(accumulated);
|
|
68
|
+
setSpinnerStatus("Thinking...");
|
|
55
69
|
break;
|
|
56
70
|
|
|
57
71
|
case "turn_complete":
|
|
@@ -61,6 +75,8 @@ function App({ engine, config }) {
|
|
|
61
75
|
accumulated = "";
|
|
62
76
|
setStreamingText("");
|
|
63
77
|
setCurrentTool(null);
|
|
78
|
+
setSpinnerStatus(null);
|
|
79
|
+
updateContextStats();
|
|
64
80
|
break;
|
|
65
81
|
|
|
66
82
|
case "tool_start":
|
|
@@ -71,6 +87,7 @@ function App({ engine, config }) {
|
|
|
71
87
|
setStreamingText("");
|
|
72
88
|
}
|
|
73
89
|
setCurrentTool({ name: event.name, input: event.input, output: null, isError: false, isRunning: true });
|
|
90
|
+
setSpinnerStatus(`Running ${event.name}...`);
|
|
74
91
|
break;
|
|
75
92
|
|
|
76
93
|
case "tool_result":
|
|
@@ -83,6 +100,7 @@ function App({ engine, config }) {
|
|
|
83
100
|
toolIsError: event.isError,
|
|
84
101
|
});
|
|
85
102
|
setCurrentTool(null);
|
|
103
|
+
setSpinnerStatus("Analyzing results...");
|
|
86
104
|
break;
|
|
87
105
|
|
|
88
106
|
case "pipeline_event": {
|
|
@@ -103,13 +121,15 @@ function App({ engine, config }) {
|
|
|
103
121
|
|
|
104
122
|
streamingRef.current = false;
|
|
105
123
|
setStreaming(false);
|
|
124
|
+
setSpinnerStatus(null);
|
|
125
|
+
updateContextStats();
|
|
106
126
|
|
|
107
127
|
// Process queue
|
|
108
128
|
if (queueRef.current.length > 0) {
|
|
109
129
|
const next = queueRef.current.shift();
|
|
110
130
|
runTurn(next);
|
|
111
131
|
}
|
|
112
|
-
}, [addMessage]);
|
|
132
|
+
}, [addMessage, updateContextStats]);
|
|
113
133
|
|
|
114
134
|
const handleSlashCommand = useCallback((text) => {
|
|
115
135
|
const parts = text.split(/\s+/);
|
|
@@ -125,6 +145,7 @@ function App({ engine, config }) {
|
|
|
125
145
|
" /help Show this help\n" +
|
|
126
146
|
" /status Show session info, model, phase, workspace\n" +
|
|
127
147
|
" /clear Clear conversation history (keep workspace)\n" +
|
|
148
|
+
" /compact Summarize older messages to reduce context\n" +
|
|
128
149
|
" /sessions List all sessions\n" +
|
|
129
150
|
" /resume <name> Resume a previous session\n" +
|
|
130
151
|
" /rename <name> Rename current session\n" +
|
|
@@ -132,26 +153,53 @@ function App({ engine, config }) {
|
|
|
132
153
|
});
|
|
133
154
|
return true;
|
|
134
155
|
|
|
135
|
-
case "/status":
|
|
156
|
+
case "/status": {
|
|
157
|
+
const stats = engineRef.current.getContextStats();
|
|
136
158
|
addMessage({
|
|
137
159
|
role: "system",
|
|
138
160
|
content:
|
|
139
161
|
`Session: ${engineRef.current.workspace.sessionId}\n` +
|
|
140
162
|
`Phase: ${engineRef.current.currentPhase.toUpperCase()}\n` +
|
|
141
163
|
`Model: ${config.kcModel}\n` +
|
|
164
|
+
`Provider: ${config.provider || "unknown"}\n` +
|
|
142
165
|
`LLM URL: ${config.llmBaseUrl}\n` +
|
|
143
166
|
`Workspace: ${engineRef.current.workspace.cwd}\n` +
|
|
144
167
|
`Tools: ${engineRef.current.toolRegistry.size} registered\n` +
|
|
145
|
-
`History: ${engineRef.current.history.messages.length} messages
|
|
168
|
+
`History: ${engineRef.current.history.messages.length} messages\n` +
|
|
169
|
+
`Context: ~${stats.totalTokens} tokens (${stats.percentage}% of ${stats.limit})`,
|
|
146
170
|
});
|
|
147
171
|
return true;
|
|
172
|
+
}
|
|
148
173
|
|
|
149
174
|
case "/clear":
|
|
150
175
|
engineRef.current.history = new ConversationHistory(engineRef.current.workspace.cwd);
|
|
151
176
|
setMessages([]);
|
|
152
177
|
addMessage({ role: "system", content: "Conversation cleared. Workspace and pipeline state preserved." });
|
|
178
|
+
updateContextStats();
|
|
153
179
|
return true;
|
|
154
180
|
|
|
181
|
+
case "/compact": {
|
|
182
|
+
addMessage({ role: "system", content: "Compacting conversation history..." });
|
|
183
|
+
// Run compact asynchronously
|
|
184
|
+
(async () => {
|
|
185
|
+
try {
|
|
186
|
+
const result = await engineRef.current.compact();
|
|
187
|
+
if (result) {
|
|
188
|
+
addMessage({
|
|
189
|
+
role: "system",
|
|
190
|
+
content: `Compacted: removed ${result.removedCount} messages, kept ${result.retainedCount}. Summary: ~${result.summaryTokens} tokens.`,
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
addMessage({ role: "system", content: "Nothing to compact (conversation is short enough)." });
|
|
194
|
+
}
|
|
195
|
+
updateContextStats();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
addMessage({ role: "system", content: `Compact failed: ${err.message}` });
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
155
203
|
case "/rename":
|
|
156
204
|
if (!arg) {
|
|
157
205
|
addMessage({ role: "system", content: "Usage: /rename <new_name>" });
|
|
@@ -193,19 +241,46 @@ function App({ engine, config }) {
|
|
|
193
241
|
addMessage({ role: "system", content: "Sessions:\n" + lines.join("\n") + "\n\nUsage: /resume <name>" });
|
|
194
242
|
}
|
|
195
243
|
} else {
|
|
196
|
-
|
|
244
|
+
// Resume a previous session
|
|
245
|
+
(async () => {
|
|
246
|
+
try {
|
|
247
|
+
const client = new LLMClient({
|
|
248
|
+
apiKey: config.llmApiKey,
|
|
249
|
+
baseUrl: config.llmBaseUrl,
|
|
250
|
+
authType: config.authType,
|
|
251
|
+
apiFormat: config.apiFormat,
|
|
252
|
+
});
|
|
253
|
+
const resumed = await AgentEngine.resume({ client, config, sessionId: arg });
|
|
254
|
+
engineRef.current = resumed;
|
|
255
|
+
setSessionId(resumed.workspace.sessionId);
|
|
256
|
+
setPhase(resumed.currentPhase);
|
|
257
|
+
setMessages([]);
|
|
258
|
+
addMessage({
|
|
259
|
+
role: "system",
|
|
260
|
+
content:
|
|
261
|
+
`Resumed session: ${arg}\n` +
|
|
262
|
+
`Phase: ${resumed.currentPhase.toUpperCase()}\n` +
|
|
263
|
+
`History: ${resumed.history.messages.length} messages restored`,
|
|
264
|
+
});
|
|
265
|
+
updateContextStats();
|
|
266
|
+
} catch (err) {
|
|
267
|
+
addMessage({ role: "system", content: `Resume failed: ${err.message}` });
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
197
270
|
}
|
|
198
271
|
return true;
|
|
199
272
|
|
|
200
273
|
case "/exit":
|
|
201
274
|
case "/quit":
|
|
275
|
+
// Save state before exit
|
|
276
|
+
try { engineRef.current.saveState(); } catch { /* ignore */ }
|
|
202
277
|
exit();
|
|
203
278
|
return true;
|
|
204
279
|
|
|
205
280
|
default:
|
|
206
281
|
return false;
|
|
207
282
|
}
|
|
208
|
-
}, [addMessage, config, exit]);
|
|
283
|
+
}, [addMessage, config, exit, updateContextStats]);
|
|
209
284
|
|
|
210
285
|
const handleSubmit = useCallback((text) => {
|
|
211
286
|
const trimmed = text.trim();
|
|
@@ -233,10 +308,12 @@ function App({ engine, config }) {
|
|
|
233
308
|
queueRef.current.length = 0;
|
|
234
309
|
addMessage({ role: "system", content: "[Queue cleared]" });
|
|
235
310
|
} else {
|
|
311
|
+
try { engineRef.current.saveState(); } catch { /* ignore */ }
|
|
236
312
|
exit();
|
|
237
313
|
}
|
|
238
314
|
}
|
|
239
315
|
if (key.ctrl && input === "d") {
|
|
316
|
+
try { engineRef.current.saveState(); } catch { /* ignore */ }
|
|
240
317
|
exit();
|
|
241
318
|
}
|
|
242
319
|
});
|
|
@@ -291,9 +368,9 @@ function App({ engine, config }) {
|
|
|
291
368
|
isRunning: true,
|
|
292
369
|
}) : null,
|
|
293
370
|
|
|
294
|
-
//
|
|
295
|
-
streaming
|
|
296
|
-
? h(CookingSpinner)
|
|
371
|
+
// Activity indicator while KC is working
|
|
372
|
+
streaming
|
|
373
|
+
? h(CookingSpinner, { status: spinnerStatus })
|
|
297
374
|
: null,
|
|
298
375
|
|
|
299
376
|
// Separator + Input
|
|
@@ -305,13 +382,18 @@ function App({ engine, config }) {
|
|
|
305
382
|
isActive: !streaming,
|
|
306
383
|
}),
|
|
307
384
|
h(HRule),
|
|
308
|
-
h(StatusBar, { sessionId, phase }),
|
|
385
|
+
h(StatusBar, { sessionId, phase, contextTokens, contextLimit }),
|
|
309
386
|
);
|
|
310
387
|
}
|
|
311
388
|
|
|
312
|
-
export async function main() {
|
|
389
|
+
export async function main({ languageOverride } = {}) {
|
|
313
390
|
const config = loadSettings();
|
|
314
391
|
|
|
392
|
+
// Session-only language override (does NOT persist to config)
|
|
393
|
+
if (languageOverride) {
|
|
394
|
+
config.language = languageOverride;
|
|
395
|
+
}
|
|
396
|
+
|
|
315
397
|
if (!config.llmApiKey) {
|
|
316
398
|
console.error("Error: No API key configured. Run 'kc-beta onboard' first.");
|
|
317
399
|
process.exit(1);
|
|
@@ -320,10 +402,17 @@ export async function main() {
|
|
|
320
402
|
const client = new LLMClient({
|
|
321
403
|
apiKey: config.llmApiKey,
|
|
322
404
|
baseUrl: config.llmBaseUrl,
|
|
405
|
+
authType: config.authType,
|
|
406
|
+
apiFormat: config.apiFormat,
|
|
323
407
|
});
|
|
324
408
|
|
|
325
409
|
const engine = new AgentEngine({ client, config });
|
|
326
410
|
|
|
411
|
+
// Save state on process exit
|
|
412
|
+
const saveOnExit = () => { try { engine.saveState(); } catch { /* ignore */ } };
|
|
413
|
+
process.on("SIGINT", saveOnExit);
|
|
414
|
+
process.on("SIGTERM", saveOnExit);
|
|
415
|
+
|
|
327
416
|
const instance = render(h(App, { engine, config }));
|
|
328
417
|
await instance.waitUntilExit();
|
|
329
418
|
}
|