kc-beta 0.5.4 → 0.5.6
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/package.json +1 -1
- package/src/agent/context-window.js +12 -2
- package/src/agent/engine.js +4 -1
- package/src/cli/index.js +35 -1
- package/src/cli/onboard.js +58 -36
- package/src/model-tiers.json +19 -2
- package/src/providers.js +31 -1
- package/template/kc-skills.zip +0 -0
package/package.json
CHANGED
|
@@ -38,8 +38,18 @@ export class ContextWindow {
|
|
|
38
38
|
return { messages, wasWindowed: false, removedCount: 0 };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Split into older and recent
|
|
42
|
-
|
|
41
|
+
// Split into older and recent. The recent slice is fed directly to the
|
|
42
|
+
// LLM, so it must not begin with an orphan "tool" message — those carry a
|
|
43
|
+
// tool_call_id that references an assistant `tool_calls` entry, and if
|
|
44
|
+
// that assistant message ended up in the compressed older slice the
|
|
45
|
+
// provider rejects the request (OpenAI: "tool messages must follow an
|
|
46
|
+
// assistant with tool_calls"; Anthropic: unpaired tool_use/tool_result).
|
|
47
|
+
// Walk the split point forward past any leading tool rows so the recent
|
|
48
|
+
// window always starts on a turn boundary.
|
|
49
|
+
let splitPoint = Math.max(0, messages.length - this.recentWindowSize);
|
|
50
|
+
while (splitPoint < messages.length && messages[splitPoint]?.role === "tool") {
|
|
51
|
+
splitPoint++;
|
|
52
|
+
}
|
|
43
53
|
const recentMessages = messages.slice(splitPoint);
|
|
44
54
|
const olderMessages = messages.slice(0, splitPoint);
|
|
45
55
|
|
package/src/agent/engine.js
CHANGED
|
@@ -341,7 +341,10 @@ export class AgentEngine {
|
|
|
341
341
|
if (!this.contextWindow) return;
|
|
342
342
|
const windowed = this.contextWindow.window(this.history.messages, this._phaseSummaries);
|
|
343
343
|
if (windowed.wasWindowed) {
|
|
344
|
-
|
|
344
|
+
// `messages` is a getter-only property on ConversationHistory; write the
|
|
345
|
+
// backing field and persist (same pattern as compact()).
|
|
346
|
+
this.history._messages = windowed.messages;
|
|
347
|
+
this.history._save();
|
|
345
348
|
this.eventLog.append("context_windowed", {
|
|
346
349
|
removed: windowed.removedCount,
|
|
347
350
|
trigger: "post_tool_result",
|
package/src/cli/index.js
CHANGED
|
@@ -235,6 +235,7 @@ function App({ engine, config }) {
|
|
|
235
235
|
return true;
|
|
236
236
|
}
|
|
237
237
|
const ok = engine._advancePhase(next, "manual /phase advance");
|
|
238
|
+
if (ok) setPhase(engine.currentPhase);
|
|
238
239
|
addMessage({
|
|
239
240
|
role: "system",
|
|
240
241
|
content: ok
|
|
@@ -246,12 +247,28 @@ function App({ engine, config }) {
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
// /phase <name> — force-jump. Uses {force:true} to allow backward jumps.
|
|
250
|
+
// Whitelist against known phases first so an unknown name doesn't
|
|
251
|
+
// silently corrupt engine state (_advancePhase with {force:true}
|
|
252
|
+
// would otherwise accept any string and mutate currentPhase).
|
|
253
|
+
const validPhases = Object.keys(engine.pipelines);
|
|
254
|
+
if (!validPhases.includes(sub)) {
|
|
255
|
+
addMessage({
|
|
256
|
+
role: "system",
|
|
257
|
+
content: `Unknown phase: ${sub}. Valid: ${validPhases.join(", ")}`,
|
|
258
|
+
});
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (sub === engine.currentPhase) {
|
|
262
|
+
addMessage({ role: "system", content: `Already in phase ${sub.toUpperCase()}.` });
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
249
265
|
const ok = engine._advancePhase(sub, "manual /phase <name>", { force: true });
|
|
266
|
+
if (ok) setPhase(engine.currentPhase);
|
|
250
267
|
addMessage({
|
|
251
268
|
role: "system",
|
|
252
269
|
content: ok
|
|
253
270
|
? `→ phase set to ${sub.toUpperCase()}.`
|
|
254
|
-
: `
|
|
271
|
+
: `Failed to set phase to ${sub}.`,
|
|
255
272
|
});
|
|
256
273
|
updateContextStats();
|
|
257
274
|
return true;
|
|
@@ -290,6 +307,15 @@ function App({ engine, config }) {
|
|
|
290
307
|
|
|
291
308
|
case "/compact": {
|
|
292
309
|
addMessage({ role: "system", content: "Compacting conversation history..." });
|
|
310
|
+
// Gate the prompt while compact() is in flight. Without this,
|
|
311
|
+
// InputPrompt stays active (isActive: !streaming) and a concurrent
|
|
312
|
+
// user submission routes into runTurn → history.addUser(...), which
|
|
313
|
+
// appends to _messages AFTER compact()'s pre-await snapshot. When
|
|
314
|
+
// compact resolves it overwrites _messages with [summary, ack,
|
|
315
|
+
// ...recentMessages] and silently drops the concurrent turn.
|
|
316
|
+
streamingRef.current = true;
|
|
317
|
+
setStreaming(true);
|
|
318
|
+
setSpinnerStatus("Compacting...");
|
|
293
319
|
(async () => {
|
|
294
320
|
try {
|
|
295
321
|
const result = await engineRef.current.compact();
|
|
@@ -312,6 +338,14 @@ function App({ engine, config }) {
|
|
|
312
338
|
updateContextStats();
|
|
313
339
|
} catch (err) {
|
|
314
340
|
addMessage({ role: "system", content: `Compact failed: ${err.message}` });
|
|
341
|
+
} finally {
|
|
342
|
+
streamingRef.current = false;
|
|
343
|
+
setStreaming(false);
|
|
344
|
+
setSpinnerStatus(null);
|
|
345
|
+
if (queueRef.current.length > 0) {
|
|
346
|
+
const next = queueRef.current.shift();
|
|
347
|
+
runTurn(next);
|
|
348
|
+
}
|
|
315
349
|
}
|
|
316
350
|
})();
|
|
317
351
|
return true;
|
package/src/cli/onboard.js
CHANGED
|
@@ -171,13 +171,19 @@ export async function onboard() {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// --- API Key ---
|
|
174
|
-
|
|
174
|
+
// Only offer to "keep" an existing key when the user hasn't switched
|
|
175
|
+
// providers. Otherwise an accidental Enter would silently save the OLD
|
|
176
|
+
// provider's key against the NEW provider's base URL — silent breakage.
|
|
177
|
+
const keyIsForSameProvider = existing.provider === provider.id;
|
|
178
|
+
const maskedExisting = keyIsForSameProvider && existing.api_key
|
|
179
|
+
? existing.api_key.slice(0, 6) + "..." + existing.api_key.slice(-4)
|
|
180
|
+
: "";
|
|
175
181
|
const keyHint = maskedExisting ? t.apiKeyKeep : t.apiKeyRequired;
|
|
176
182
|
const keyPrompt = maskedExisting
|
|
177
183
|
? ` ${CYAN}${t.apiKey}${RESET} ${DIM}(${maskedExisting})${RESET}`
|
|
178
184
|
: ` ${CYAN}${t.apiKey}${RESET} ${YELLOW}(${t.apiKeyRequired})${RESET}`;
|
|
179
185
|
const apiKey = await ask(rl, keyPrompt, "", keyHint);
|
|
180
|
-
const finalKey = apiKey || existing.api_key || "";
|
|
186
|
+
const finalKey = apiKey || (keyIsForSameProvider ? existing.api_key : "") || "";
|
|
181
187
|
if (!finalKey) { console.log(` ${RED}${t.apiKeyMissing}${RESET}`); rl.close(); process.exit(1); }
|
|
182
188
|
console.log();
|
|
183
189
|
|
|
@@ -245,34 +251,12 @@ export async function onboard() {
|
|
|
245
251
|
);
|
|
246
252
|
console.log();
|
|
247
253
|
|
|
248
|
-
// --- Worker LLM tiers ---
|
|
249
|
-
console.log(` ${CYAN}${t.workerTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
|
|
250
|
-
const tiers = {};
|
|
251
|
-
for (const tier of ["tier1", "tier2", "tier3", "tier4"]) {
|
|
252
|
-
const def = suggestedTiers?.[tier] || provider.defaultTiers[tier] || existing?.tiers?.[tier] || "";
|
|
253
|
-
tiers[tier] = await ask(
|
|
254
|
-
rl,
|
|
255
|
-
` ${tier.toUpperCase()}`,
|
|
256
|
-
def,
|
|
257
|
-
t.discoveryAccept ? "" : "",
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
console.log();
|
|
261
|
-
|
|
262
|
-
// --- VLM tiers (vision/OCR) ---
|
|
263
|
-
console.log(` ${CYAN}${t.vlmTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
|
|
264
|
-
const vlmTiers = {};
|
|
265
|
-
for (const tier of ["tier1", "tier2", "tier3"]) {
|
|
266
|
-
const def = provider.defaultVlm?.[tier] || existing?.vlm_tiers?.[tier] || "";
|
|
267
|
-
vlmTiers[tier] = await ask(
|
|
268
|
-
rl,
|
|
269
|
-
` ${tier.toUpperCase()}`,
|
|
270
|
-
def,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
console.log();
|
|
274
|
-
|
|
275
254
|
// --- Worker LLM provider (optional) ---
|
|
255
|
+
// Ask worker-provider BEFORE tier prompts so that when worker differs from
|
|
256
|
+
// conductor (e.g. conductor=xfyun single-model, worker=siliconflow) the
|
|
257
|
+
// tier-default suggestions come from the WORKER provider's model-tiers.json
|
|
258
|
+
// entry, not the conductor's. Previous ordering defaulted tiers from the
|
|
259
|
+
// conductor and produced nonsensical defaults for single-model conductors.
|
|
276
260
|
console.log(` ${CYAN}${t.workerConfig}${RESET}`);
|
|
277
261
|
const sameProvider = await ask(rl, ` ${t.workerSameProvider}`, "Y", t.yesNo);
|
|
278
262
|
let workerProvider = "";
|
|
@@ -280,6 +264,7 @@ export async function onboard() {
|
|
|
280
264
|
let workerBaseUrl = "";
|
|
281
265
|
let workerAuthType = "";
|
|
282
266
|
let workerApiFormat = "";
|
|
267
|
+
let tierProviderDef = provider; // where tier defaults come from
|
|
283
268
|
|
|
284
269
|
if (sameProvider.toLowerCase() === "n" || sameProvider.toLowerCase() === "no") {
|
|
285
270
|
// Pick a different provider for workers
|
|
@@ -294,21 +279,58 @@ export async function onboard() {
|
|
|
294
279
|
workerAuthType = wp.authType;
|
|
295
280
|
workerApiFormat = wp.apiFormat;
|
|
296
281
|
workerBaseUrl = wp.baseUrl;
|
|
282
|
+
tierProviderDef = wp;
|
|
297
283
|
|
|
298
284
|
if (wp.id === "custom") {
|
|
299
285
|
workerBaseUrl = await ask(rl, ` ${t.baseUrl}`, existing.worker_base_url || "");
|
|
300
286
|
}
|
|
301
287
|
|
|
302
|
-
// Worker API key
|
|
303
|
-
|
|
288
|
+
// Worker API key. Show masked existing key in the prompt (matches the
|
|
289
|
+
// main-provider prompt style) so the user can confirm what's saved
|
|
290
|
+
// without guessing. Like the main key, only offer to "keep" the existing
|
|
291
|
+
// value if the WORKER provider itself hasn't changed — otherwise Enter
|
|
292
|
+
// would silently carry the previous worker provider's key across.
|
|
293
|
+
const workerKeyIsForSameProvider = existing.worker_provider === wp.id;
|
|
294
|
+
const wMasked = workerKeyIsForSameProvider && existing.worker_api_key
|
|
295
|
+
? existing.worker_api_key.slice(0, 6) + "..." + existing.worker_api_key.slice(-4)
|
|
296
|
+
: "";
|
|
304
297
|
const wKeyHint = wMasked ? t.apiKeyKeep : t.apiKeyRequired;
|
|
305
|
-
|
|
298
|
+
const wKeyPrompt = wMasked
|
|
299
|
+
? ` ${CYAN}${t.apiKey} (Worker)${RESET} ${DIM}(${wMasked})${RESET}`
|
|
300
|
+
: ` ${CYAN}${t.apiKey} (Worker)${RESET} ${YELLOW}(${t.apiKeyRequired})${RESET}`;
|
|
301
|
+
workerApiKey = await ask(rl, wKeyPrompt, "", wKeyHint);
|
|
302
|
+
workerApiKey = workerApiKey || (workerKeyIsForSameProvider ? existing.worker_api_key : "") || "";
|
|
303
|
+
}
|
|
304
|
+
console.log();
|
|
305
|
+
|
|
306
|
+
// --- Worker LLM tiers (defaults come from tierProviderDef set above) ---
|
|
307
|
+
// When worker==conductor, these default from the conductor's model-tiers.json
|
|
308
|
+
// entry. When worker is a separate provider, they default from the WORKER
|
|
309
|
+
// provider's entry — so e.g. siliconflow's GLM-5.1 tier1 defaults apply.
|
|
310
|
+
console.log(` ${CYAN}${t.workerTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
|
|
311
|
+
const tiers = {};
|
|
312
|
+
const tierSuggested = tierProviderDef.id === provider.id ? suggestedTiers : null;
|
|
313
|
+
for (const tier of ["tier1", "tier2", "tier3", "tier4"]) {
|
|
314
|
+
const def = tierSuggested?.[tier] || tierProviderDef.defaultTiers?.[tier] || existing?.tiers?.[tier] || "";
|
|
315
|
+
tiers[tier] = await ask(
|
|
316
|
+
rl,
|
|
317
|
+
` ${tier.toUpperCase()}`,
|
|
318
|
+
def,
|
|
319
|
+
t.discoveryAccept ? "" : "",
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
console.log();
|
|
323
|
+
|
|
324
|
+
// --- VLM tiers (vision/OCR) — also from worker provider when split ---
|
|
325
|
+
console.log(` ${CYAN}${t.vlmTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
|
|
326
|
+
const vlmTiers = {};
|
|
327
|
+
for (const tier of ["tier1", "tier2", "tier3"]) {
|
|
328
|
+
const def = tierProviderDef.defaultVlm?.[tier] || existing?.vlm_tiers?.[tier] || "";
|
|
329
|
+
vlmTiers[tier] = await ask(
|
|
306
330
|
rl,
|
|
307
|
-
`
|
|
308
|
-
|
|
309
|
-
wKeyHint,
|
|
331
|
+
` ${tier.toUpperCase()}`,
|
|
332
|
+
def,
|
|
310
333
|
);
|
|
311
|
-
workerApiKey = workerApiKey || existing.worker_api_key || "";
|
|
312
334
|
}
|
|
313
335
|
console.log();
|
|
314
336
|
|
package/src/model-tiers.json
CHANGED
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
},
|
|
33
33
|
|
|
34
34
|
"volcanocloud": {
|
|
35
|
-
"
|
|
35
|
+
"_comment": "Coding plan (api/coding/v3) serves glm-5.1 only. Regular plan (api/v3) serves doubao/deepseek/glm-4-7. Pick conductor per the plan you onboarded.",
|
|
36
|
+
"conductor": "glm-5.1",
|
|
36
37
|
"llm": {
|
|
37
|
-
"tier1": "doubao-seed-2-0-pro-260215, deepseek-v3-2-251201",
|
|
38
|
+
"tier1": "glm-5.1, doubao-seed-2-0-pro-260215, deepseek-v3-2-251201",
|
|
38
39
|
"tier2": "glm-4-7-251222, doubao-1-5-pro-32k-250115",
|
|
39
40
|
"tier3": "doubao-seed-2-0-mini-260215",
|
|
40
41
|
"tier4": "doubao-seed-2-0-lite-260215, doubao-1-5-lite-32k-250115"
|
|
@@ -46,6 +47,22 @@
|
|
|
46
47
|
}
|
|
47
48
|
},
|
|
48
49
|
|
|
50
|
+
"xfyun": {
|
|
51
|
+
"_comment": "iFlytek Astro coding plan exposes only astron-code-latest; no VLM.",
|
|
52
|
+
"conductor": "astron-code-latest",
|
|
53
|
+
"llm": {
|
|
54
|
+
"tier1": "astron-code-latest",
|
|
55
|
+
"tier2": "",
|
|
56
|
+
"tier3": "",
|
|
57
|
+
"tier4": ""
|
|
58
|
+
},
|
|
59
|
+
"vlm": {
|
|
60
|
+
"tier1": "",
|
|
61
|
+
"tier2": "",
|
|
62
|
+
"tier3": ""
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
49
66
|
"anthropic": {
|
|
50
67
|
"conductor": "claude-sonnet-4-20250514",
|
|
51
68
|
"llm": {
|
package/src/providers.js
CHANGED
|
@@ -77,14 +77,20 @@ const PROVIDERS = [
|
|
|
77
77
|
{
|
|
78
78
|
id: "volcanocloud",
|
|
79
79
|
name: "VolcanoCloud",
|
|
80
|
+
// Regular Ark API — serves doubao / deepseek / glm-4-7-251222.
|
|
81
|
+
// Coding plan uses api/coding/v3 and serves glm-5.1 (aliased to glm-4.7
|
|
82
|
+
// server-side, thinking model).
|
|
80
83
|
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
84
|
+
codingPlanUrl: "https://ark.cn-beijing.volces.com/api/coding/v3",
|
|
81
85
|
authType: "bearer",
|
|
82
86
|
apiFormat: "openai",
|
|
83
|
-
modelsEndpoint: null, // VolcanoCloud
|
|
87
|
+
modelsEndpoint: null, // VolcanoCloud — use curated list
|
|
88
|
+
supportsCodingPlanKey: true,
|
|
84
89
|
defaultModel: getTierConfig("volcanocloud").conductor || "doubao-seed-2-0-pro-260215",
|
|
85
90
|
defaultTiers: getTierConfig("volcanocloud").llm,
|
|
86
91
|
defaultVlm: getTierConfig("volcanocloud").vlm,
|
|
87
92
|
curatedModels: [
|
|
93
|
+
{ id: "glm-5.1", ownedBy: "zhipu" },
|
|
88
94
|
{ id: "doubao-seed-2-0-pro-260215", ownedBy: "bytedance" },
|
|
89
95
|
{ id: "deepseek-v3-2-251201", ownedBy: "deepseek" },
|
|
90
96
|
{ id: "glm-4-7-251222", ownedBy: "zhipu" },
|
|
@@ -98,6 +104,27 @@ const PROVIDERS = [
|
|
|
98
104
|
zh: "火山云(字节跳动)",
|
|
99
105
|
},
|
|
100
106
|
},
|
|
107
|
+
{
|
|
108
|
+
id: "xfyun",
|
|
109
|
+
name: "XfYun Astro",
|
|
110
|
+
// iFlytek Astro coding plan — OpenAI-compatible endpoint. Only exposes
|
|
111
|
+
// a single model (astron-code-latest) today, so no /models discovery and
|
|
112
|
+
// the tier assignment in model-tiers.json only fills tier1 / conductor.
|
|
113
|
+
baseUrl: "https://maas-coding-api.cn-huabei-1.xf-yun.com/v2",
|
|
114
|
+
authType: "bearer",
|
|
115
|
+
apiFormat: "openai",
|
|
116
|
+
modelsEndpoint: null,
|
|
117
|
+
defaultModel: getTierConfig("xfyun").conductor || "astron-code-latest",
|
|
118
|
+
defaultTiers: getTierConfig("xfyun").llm,
|
|
119
|
+
defaultVlm: getTierConfig("xfyun").vlm,
|
|
120
|
+
curatedModels: [
|
|
121
|
+
{ id: "astron-code-latest", ownedBy: "iflytek" },
|
|
122
|
+
],
|
|
123
|
+
labels: {
|
|
124
|
+
en: "iFlytek XfYun Astro (coding plan, single-model)",
|
|
125
|
+
zh: "科大讯飞 Astro 编程套餐(单模型)",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
101
128
|
{
|
|
102
129
|
id: "anthropic",
|
|
103
130
|
name: "Anthropic",
|
|
@@ -238,12 +265,15 @@ const MODEL_RANKING = {
|
|
|
238
265
|
"qwen3.5-122b": 75,
|
|
239
266
|
"qwen3.5-35b": 65,
|
|
240
267
|
// Zhipu
|
|
268
|
+
"glm-5.1": 92,
|
|
241
269
|
"glm-5": 90,
|
|
242
270
|
"glm-4.7": 80,
|
|
243
271
|
"glm-4": 75,
|
|
244
272
|
// Others
|
|
245
273
|
"kimi-k2.5": 85,
|
|
246
274
|
"kimi-k2": 80,
|
|
275
|
+
// iFlytek Astro
|
|
276
|
+
"astron-code": 90,
|
|
247
277
|
"minimax-m2": 80,
|
|
248
278
|
"deepseek-v3": 85,
|
|
249
279
|
"deepseek-r1": 90,
|
|
Binary file
|