kc-beta 0.1.2 → 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/engine.js +202 -5
- 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 +22 -6
- 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 +78 -0
- package/src/agent/token-counter.js +62 -0
- package/src/agent/tools/document-parse.js +3 -3
- 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 +151 -57
- package/src/config.js +20 -7
- package/src/providers.js +370 -0
package/src/agent/llm-client.js
CHANGED
|
@@ -1,102 +1,268 @@
|
|
|
1
|
+
import { withRetry } from "./retry.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Supports OpenAI-compatible APIs (SiliconFlow, Aliyun, OpenAI, etc.)
|
|
4
|
+
* Multi-protocol LLM client using native fetch + SSE parsing.
|
|
5
|
+
* Supports OpenAI-compatible APIs and Anthropic Messages API.
|
|
5
6
|
*/
|
|
6
7
|
export class LLMClient {
|
|
7
8
|
/**
|
|
8
9
|
* @param {object} opts
|
|
9
10
|
* @param {string} opts.apiKey
|
|
10
|
-
* @param {string} opts.baseUrl - e.g. "https://api.siliconflow.cn/v1"
|
|
11
|
+
* @param {string} opts.baseUrl - e.g. "https://api.siliconflow.cn/v1" or "https://api.anthropic.com"
|
|
12
|
+
* @param {string} [opts.authType] - "bearer" (default) | "x-api-key" (Anthropic)
|
|
13
|
+
* @param {string} [opts.apiFormat] - "openai" (default) | "anthropic"
|
|
11
14
|
*/
|
|
12
|
-
constructor({ apiKey, baseUrl }) {
|
|
15
|
+
constructor({ apiKey, baseUrl, authType = "bearer", apiFormat = "openai" }) {
|
|
13
16
|
this.apiKey = apiKey;
|
|
14
17
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
18
|
+
this.authType = authType;
|
|
19
|
+
this.apiFormat = apiFormat;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
23
|
+
* Build auth headers based on provider type.
|
|
24
|
+
* @returns {object}
|
|
25
|
+
*/
|
|
26
|
+
_buildHeaders() {
|
|
27
|
+
const headers = { "Content-Type": "application/json" };
|
|
28
|
+
if (this.authType === "x-api-key") {
|
|
29
|
+
headers["x-api-key"] = this.apiKey;
|
|
30
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
31
|
+
} else if (this.authType === "aws-sigv4") {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"AWS Bedrock authentication (SigV4) is not yet supported. " +
|
|
34
|
+
"Please use a different provider or an OpenAI-compatible proxy."
|
|
35
|
+
);
|
|
36
|
+
} else {
|
|
37
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
38
|
+
}
|
|
39
|
+
return headers;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the chat endpoint for the configured API format.
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
_getEndpoint() {
|
|
47
|
+
if (this.apiFormat === "anthropic") {
|
|
48
|
+
return `${this.baseUrl}/v1/messages`;
|
|
49
|
+
}
|
|
50
|
+
return `${this.baseUrl}/chat/completions`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build request body for the configured API format.
|
|
19
55
|
* @param {object} opts
|
|
20
|
-
* @
|
|
21
|
-
* @param {Array} opts.messages
|
|
22
|
-
* @param {Array} [opts.tools]
|
|
23
|
-
* @param {number} [opts.maxTokens]
|
|
24
|
-
* @yields {object} Parsed chunk from the SSE stream
|
|
56
|
+
* @returns {object}
|
|
25
57
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
model,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
_buildStreamBody({ model, messages, tools, maxTokens }) {
|
|
59
|
+
if (this.apiFormat === "anthropic") {
|
|
60
|
+
return this._buildAnthropicBody({ model, messages, tools, maxTokens, stream: true });
|
|
61
|
+
}
|
|
62
|
+
return this._buildOpenaiBody({ model, messages, tools, maxTokens, stream: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_buildNonStreamBody({ model, messages, maxTokens }) {
|
|
66
|
+
if (this.apiFormat === "anthropic") {
|
|
67
|
+
return this._buildAnthropicBody({ model, messages, tools: null, maxTokens, stream: false });
|
|
68
|
+
}
|
|
69
|
+
return this._buildOpenaiBody({ model, messages, tools: null, maxTokens, stream: false });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_buildOpenaiBody({ model, messages, tools, maxTokens, stream }) {
|
|
73
|
+
const body = { model, messages, stream };
|
|
32
74
|
if (maxTokens) body.max_tokens = maxTokens;
|
|
33
75
|
if (tools && tools.length > 0) body.tools = tools;
|
|
76
|
+
return body;
|
|
77
|
+
}
|
|
34
78
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
_buildAnthropicBody({ model, messages, tools, maxTokens, stream }) {
|
|
80
|
+
// Anthropic: system message is a top-level field, not in messages array
|
|
81
|
+
let system = undefined;
|
|
82
|
+
const filteredMessages = [];
|
|
83
|
+
for (const msg of messages) {
|
|
84
|
+
if (msg.role === "system") {
|
|
85
|
+
system = (system ? system + "\n\n" : "") + msg.content;
|
|
86
|
+
} else if (msg.role === "tool") {
|
|
87
|
+
// Anthropic expects tool results as user messages with tool_result content blocks
|
|
88
|
+
filteredMessages.push({
|
|
89
|
+
role: "user",
|
|
90
|
+
content: [{
|
|
91
|
+
type: "tool_result",
|
|
92
|
+
tool_use_id: msg.tool_call_id,
|
|
93
|
+
content: msg.content,
|
|
94
|
+
}],
|
|
95
|
+
});
|
|
96
|
+
} else if (msg.role === "assistant" && msg.tool_calls) {
|
|
97
|
+
// Convert OpenAI tool_calls to Anthropic content blocks
|
|
98
|
+
const content = [];
|
|
99
|
+
if (msg.content) content.push({ type: "text", text: msg.content });
|
|
100
|
+
for (const tc of msg.tool_calls) {
|
|
101
|
+
let input = {};
|
|
102
|
+
try { input = JSON.parse(tc.function.arguments || "{}"); } catch { /* ignore */ }
|
|
103
|
+
content.push({
|
|
104
|
+
type: "tool_use",
|
|
105
|
+
id: tc.id,
|
|
106
|
+
name: tc.function.name,
|
|
107
|
+
input,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
filteredMessages.push({ role: "assistant", content });
|
|
111
|
+
} else {
|
|
112
|
+
filteredMessages.push(msg);
|
|
113
|
+
}
|
|
47
114
|
}
|
|
48
115
|
|
|
49
|
-
|
|
50
|
-
|
|
116
|
+
const body = {
|
|
117
|
+
model,
|
|
118
|
+
messages: filteredMessages,
|
|
119
|
+
max_tokens: maxTokens || 8192,
|
|
120
|
+
stream,
|
|
121
|
+
};
|
|
122
|
+
if (system) body.system = system;
|
|
123
|
+
if (tools && tools.length > 0) {
|
|
124
|
+
// Convert OpenAI tool schema to Anthropic tool schema
|
|
125
|
+
body.tools = tools.map((t) => ({
|
|
126
|
+
name: t.function.name,
|
|
127
|
+
description: t.function.description || "",
|
|
128
|
+
input_schema: t.function.parameters || { type: "object", properties: {} },
|
|
129
|
+
}));
|
|
51
130
|
}
|
|
131
|
+
return body;
|
|
52
132
|
}
|
|
53
133
|
|
|
54
134
|
/**
|
|
55
|
-
*
|
|
135
|
+
* Streaming chat completion. Yields parsed SSE chunk objects
|
|
136
|
+
* normalized to OpenAI shape: { choices: [{ delta: { content?, tool_calls? } }] }
|
|
56
137
|
* @param {object} opts
|
|
57
138
|
* @param {string} opts.model
|
|
58
139
|
* @param {Array} opts.messages
|
|
140
|
+
* @param {Array} [opts.tools]
|
|
59
141
|
* @param {number} [opts.maxTokens]
|
|
60
|
-
* @
|
|
142
|
+
* @yields {object} Normalized chunk
|
|
143
|
+
*/
|
|
144
|
+
async *streamChat({ model, messages, tools, maxTokens }) {
|
|
145
|
+
const body = this._buildStreamBody({ model, messages, tools, maxTokens });
|
|
146
|
+
|
|
147
|
+
const resp = await withRetry(async () => {
|
|
148
|
+
const r = await fetch(this._getEndpoint(), {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: this._buildHeaders(),
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
if (!r.ok) {
|
|
154
|
+
const text = await r.text();
|
|
155
|
+
const err = new Error(`LLM API error ${r.status}: ${text}`);
|
|
156
|
+
err.status = r.status;
|
|
157
|
+
err.retryAfter = r.headers.get("retry-after");
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
return r;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (this.apiFormat === "anthropic") {
|
|
164
|
+
yield* this._parseAnthropicSSE(resp.body);
|
|
165
|
+
} else {
|
|
166
|
+
yield* this._parseOpenaiSSE(resp.body);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Non-streaming chat completion. Returns the full response
|
|
172
|
+
* normalized to OpenAI shape.
|
|
173
|
+
* @param {object} opts
|
|
174
|
+
* @returns {object}
|
|
61
175
|
*/
|
|
62
176
|
async chat({ model, messages, maxTokens }) {
|
|
63
|
-
const body = {
|
|
64
|
-
model,
|
|
65
|
-
messages,
|
|
66
|
-
};
|
|
67
|
-
if (maxTokens) body.max_tokens = maxTokens;
|
|
177
|
+
const body = this._buildNonStreamBody({ model, messages, maxTokens });
|
|
68
178
|
|
|
69
|
-
const resp = await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
179
|
+
const resp = await withRetry(async () => {
|
|
180
|
+
const r = await fetch(this._getEndpoint(), {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: this._buildHeaders(),
|
|
183
|
+
body: JSON.stringify(body),
|
|
184
|
+
});
|
|
185
|
+
if (!r.ok) {
|
|
186
|
+
const text = await r.text();
|
|
187
|
+
const err = new Error(`LLM API error ${r.status}: ${text}`);
|
|
188
|
+
err.status = r.status;
|
|
189
|
+
err.retryAfter = r.headers.get("retry-after");
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
return r;
|
|
76
193
|
});
|
|
77
194
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
195
|
+
const data = await resp.json();
|
|
196
|
+
|
|
197
|
+
if (this.apiFormat === "anthropic") {
|
|
198
|
+
// Normalize Anthropic response to OpenAI shape
|
|
199
|
+
const textParts = [];
|
|
200
|
+
for (const block of data.content || []) {
|
|
201
|
+
if (block.type === "text") textParts.push(block.text);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
choices: [{
|
|
205
|
+
message: {
|
|
206
|
+
role: "assistant",
|
|
207
|
+
content: textParts.join(""),
|
|
208
|
+
},
|
|
209
|
+
}],
|
|
210
|
+
usage: data.usage ? {
|
|
211
|
+
prompt_tokens: data.usage.input_tokens || 0,
|
|
212
|
+
completion_tokens: data.usage.output_tokens || 0,
|
|
213
|
+
} : undefined,
|
|
214
|
+
};
|
|
81
215
|
}
|
|
82
216
|
|
|
83
|
-
return
|
|
217
|
+
return data;
|
|
84
218
|
}
|
|
85
219
|
|
|
86
220
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
221
|
+
* List available models from the provider.
|
|
222
|
+
* @returns {Promise<Array<{id: string, name: string, ownedBy: string}>>}
|
|
223
|
+
*/
|
|
224
|
+
async listModels() {
|
|
225
|
+
let endpoint;
|
|
226
|
+
if (this.apiFormat === "anthropic") {
|
|
227
|
+
endpoint = `${this.baseUrl}/v1/models`;
|
|
228
|
+
} else {
|
|
229
|
+
endpoint = `${this.baseUrl}/models`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const resp = await fetch(endpoint, {
|
|
234
|
+
method: "GET",
|
|
235
|
+
headers: this._buildHeaders(),
|
|
236
|
+
signal: AbortSignal.timeout(5000),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!resp.ok) return [];
|
|
240
|
+
const data = await resp.json();
|
|
241
|
+
return (data.data || []).map((m) => ({
|
|
242
|
+
id: m.id,
|
|
243
|
+
name: m.id,
|
|
244
|
+
ownedBy: m.owned_by || "",
|
|
245
|
+
}));
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- OpenAI SSE parsing ---
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse SSE stream from OpenAI-compatible API.
|
|
89
255
|
* @param {ReadableStream} body
|
|
90
|
-
* @yields {object} Parsed
|
|
256
|
+
* @yields {object} Parsed chunk
|
|
91
257
|
*/
|
|
92
|
-
async *
|
|
258
|
+
async *_parseOpenaiSSE(body) {
|
|
93
259
|
const decoder = new TextDecoder();
|
|
94
260
|
let buffer = "";
|
|
95
261
|
|
|
96
262
|
for await (const chunk of body) {
|
|
97
263
|
buffer += decoder.decode(chunk, { stream: true });
|
|
98
264
|
const lines = buffer.split("\n");
|
|
99
|
-
buffer = lines.pop();
|
|
265
|
+
buffer = lines.pop();
|
|
100
266
|
|
|
101
267
|
for (const line of lines) {
|
|
102
268
|
const trimmed = line.trim();
|
|
@@ -113,19 +279,146 @@ export class LLMClient {
|
|
|
113
279
|
}
|
|
114
280
|
}
|
|
115
281
|
|
|
116
|
-
// Process any remaining buffer
|
|
117
282
|
if (buffer.trim()) {
|
|
118
283
|
const trimmed = buffer.trim();
|
|
119
284
|
if (trimmed.startsWith("data: ")) {
|
|
120
285
|
const data = trimmed.slice(6).trim();
|
|
121
286
|
if (data !== "[DONE]") {
|
|
122
|
-
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
287
|
+
try { yield JSON.parse(data); } catch { /* skip */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// --- Anthropic SSE parsing + normalization ---
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse Anthropic SSE stream and normalize to OpenAI chunk shape.
|
|
297
|
+
* Anthropic SSE uses event types: message_start, content_block_start,
|
|
298
|
+
* content_block_delta, content_block_stop, message_delta, message_stop.
|
|
299
|
+
*
|
|
300
|
+
* Normalizes everything to: { choices: [{ delta: { content?, tool_calls? } }] }
|
|
301
|
+
* so engine.js needs no changes.
|
|
302
|
+
*
|
|
303
|
+
* @param {ReadableStream} body
|
|
304
|
+
* @yields {object} Normalized OpenAI-shaped chunk
|
|
305
|
+
*/
|
|
306
|
+
async *_parseAnthropicSSE(body) {
|
|
307
|
+
const decoder = new TextDecoder();
|
|
308
|
+
let buffer = "";
|
|
309
|
+
let currentEventType = "";
|
|
310
|
+
|
|
311
|
+
// State for accumulating tool call content blocks
|
|
312
|
+
let toolCallIndex = -1;
|
|
313
|
+
|
|
314
|
+
for await (const rawChunk of body) {
|
|
315
|
+
buffer += decoder.decode(rawChunk, { stream: true });
|
|
316
|
+
const lines = buffer.split("\n");
|
|
317
|
+
buffer = lines.pop();
|
|
318
|
+
|
|
319
|
+
for (const line of lines) {
|
|
320
|
+
const trimmed = line.trim();
|
|
321
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
322
|
+
|
|
323
|
+
if (trimmed.startsWith("event: ")) {
|
|
324
|
+
currentEventType = trimmed.slice(7).trim();
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (trimmed.startsWith("data: ")) {
|
|
329
|
+
const dataStr = trimmed.slice(6).trim();
|
|
330
|
+
let data;
|
|
331
|
+
try { data = JSON.parse(dataStr); } catch { continue; }
|
|
332
|
+
|
|
333
|
+
const normalized = this._normalizeAnthropicEvent(currentEventType, data, { toolCallIndex });
|
|
334
|
+
if (normalized) {
|
|
335
|
+
// Update tool call index tracking
|
|
336
|
+
if (normalized._newToolCallIndex !== undefined) {
|
|
337
|
+
toolCallIndex = normalized._newToolCallIndex;
|
|
338
|
+
delete normalized._newToolCallIndex;
|
|
339
|
+
}
|
|
340
|
+
yield normalized;
|
|
126
341
|
}
|
|
127
342
|
}
|
|
128
343
|
}
|
|
129
344
|
}
|
|
130
345
|
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Normalize a single Anthropic SSE event into OpenAI chunk shape.
|
|
349
|
+
* @param {string} eventType
|
|
350
|
+
* @param {object} data
|
|
351
|
+
* @param {object} state - Mutable state for tracking across events
|
|
352
|
+
* @returns {object|null} Normalized chunk or null if no output needed
|
|
353
|
+
*/
|
|
354
|
+
_normalizeAnthropicEvent(eventType, data, state) {
|
|
355
|
+
switch (eventType) {
|
|
356
|
+
case "content_block_start": {
|
|
357
|
+
const block = data.content_block;
|
|
358
|
+
if (block?.type === "text") {
|
|
359
|
+
// Text block starting — no content yet
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
if (block?.type === "tool_use") {
|
|
363
|
+
state.toolCallIndex++;
|
|
364
|
+
const chunk = {
|
|
365
|
+
choices: [{
|
|
366
|
+
delta: {
|
|
367
|
+
tool_calls: [{
|
|
368
|
+
index: state.toolCallIndex,
|
|
369
|
+
id: block.id,
|
|
370
|
+
type: "function",
|
|
371
|
+
function: { name: block.name, arguments: "" },
|
|
372
|
+
}],
|
|
373
|
+
},
|
|
374
|
+
}],
|
|
375
|
+
_newToolCallIndex: state.toolCallIndex,
|
|
376
|
+
};
|
|
377
|
+
return chunk;
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
case "content_block_delta": {
|
|
383
|
+
const delta = data.delta;
|
|
384
|
+
if (delta?.type === "text_delta") {
|
|
385
|
+
return {
|
|
386
|
+
choices: [{ delta: { content: delta.text } }],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (delta?.type === "input_json_delta") {
|
|
390
|
+
return {
|
|
391
|
+
choices: [{
|
|
392
|
+
delta: {
|
|
393
|
+
tool_calls: [{
|
|
394
|
+
index: state.toolCallIndex,
|
|
395
|
+
function: { arguments: delta.partial_json },
|
|
396
|
+
}],
|
|
397
|
+
},
|
|
398
|
+
}],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
case "message_delta": {
|
|
405
|
+
// End of message — contains stop_reason and usage
|
|
406
|
+
return {
|
|
407
|
+
choices: [{
|
|
408
|
+
delta: {},
|
|
409
|
+
finish_reason: data.delta?.stop_reason === "end_turn" ? "stop" : (data.delta?.stop_reason || null),
|
|
410
|
+
}],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
case "message_start":
|
|
415
|
+
case "content_block_stop":
|
|
416
|
+
case "message_stop":
|
|
417
|
+
case "ping":
|
|
418
|
+
return null;
|
|
419
|
+
|
|
420
|
+
default:
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
131
424
|
}
|
|
@@ -11,4 +11,10 @@ export class Pipeline {
|
|
|
11
11
|
|
|
12
12
|
/** Whether all requirements for leaving this phase are satisfied. */
|
|
13
13
|
exitCriteriaMet() { throw new Error("Not implemented"); }
|
|
14
|
+
|
|
15
|
+
/** Serialize milestone state for persistence. Override in subclasses. */
|
|
16
|
+
exportState() { return {}; }
|
|
17
|
+
|
|
18
|
+
/** Restore milestone state from persisted data. Override in subclasses. */
|
|
19
|
+
importState(_data) { /* no-op by default */ }
|
|
14
20
|
}
|
|
@@ -106,4 +106,22 @@ export class DistillationEngine extends Pipeline {
|
|
|
106
106
|
if (!total) return false;
|
|
107
107
|
return Object.keys(this.workflowsCreated).length >= total && this.workflowsPassing.length >= total;
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
exportState() {
|
|
111
|
+
return {
|
|
112
|
+
skillsToDistill: this.skillsToDistill,
|
|
113
|
+
workflowsCreated: this.workflowsCreated,
|
|
114
|
+
workflowsTested: this.workflowsTested,
|
|
115
|
+
workflowsPassing: this.workflowsPassing,
|
|
116
|
+
tierAssignments: this.tierAssignments,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
importState(data) {
|
|
121
|
+
if (Array.isArray(data.skillsToDistill) && data.skillsToDistill.length > this.skillsToDistill.length) this.skillsToDistill = data.skillsToDistill;
|
|
122
|
+
if (Array.isArray(data.workflowsPassing) && data.workflowsPassing.length > this.workflowsPassing.length) this.workflowsPassing = data.workflowsPassing;
|
|
123
|
+
if (data.workflowsCreated && typeof data.workflowsCreated === "object") Object.assign(this.workflowsCreated, data.workflowsCreated);
|
|
124
|
+
if (data.workflowsTested && typeof data.workflowsTested === "object") Object.assign(this.workflowsTested, data.workflowsTested);
|
|
125
|
+
if (data.tierAssignments && typeof data.tierAssignments === "object") Object.assign(this.tierAssignments, data.tierAssignments);
|
|
126
|
+
}
|
|
109
127
|
}
|
|
@@ -87,4 +87,25 @@ export class RuleExtractionPipeline extends Pipeline {
|
|
|
87
87
|
return this.regulationsScanned && this.rulesExtracted.length > 0 &&
|
|
88
88
|
this.rulesWithTests.length >= Math.max(this.rulesExtracted.length * 0.8, 1) && this.coverageAudited;
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
exportState() {
|
|
92
|
+
return {
|
|
93
|
+
regulationsScanned: this.regulationsScanned,
|
|
94
|
+
rulesExtracted: this.rulesExtracted,
|
|
95
|
+
rulesWithTests: this.rulesWithTests,
|
|
96
|
+
coverageAudited: this.coverageAudited,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
importState(data) {
|
|
101
|
+
if (data.regulationsScanned) this.regulationsScanned = true;
|
|
102
|
+
if (data.coverageAudited) this.coverageAudited = true;
|
|
103
|
+
// Arrays: use imported as floor, then re-scan will reconcile
|
|
104
|
+
if (Array.isArray(data.rulesExtracted) && data.rulesExtracted.length > this.rulesExtracted.length) {
|
|
105
|
+
this.rulesExtracted = data.rulesExtracted;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(data.rulesWithTests) && data.rulesWithTests.length > this.rulesWithTests.length) {
|
|
108
|
+
this.rulesWithTests = data.rulesWithTests;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
90
111
|
}
|
|
@@ -11,9 +11,9 @@ const DEFAULT_ENV = `# === KC Agent Project Configuration ===
|
|
|
11
11
|
# Language: en | zh
|
|
12
12
|
LANGUAGE=en
|
|
13
13
|
|
|
14
|
-
# ===
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
# === LLM API ===
|
|
15
|
+
LLM_API_KEY=
|
|
16
|
+
LLM_BASE_URL=https://api.siliconflow.cn/v1
|
|
17
17
|
|
|
18
18
|
# === Worker LLM Tiers (highest capability to lowest) ===
|
|
19
19
|
TIER1=Pro/zai-org/GLM-5, Pro/moonshotai/Kimi-K2.5
|
|
@@ -53,8 +53,8 @@ export class ProjectInitializer extends Pipeline {
|
|
|
53
53
|
if (!fs.existsSync(envPath)) {
|
|
54
54
|
let envContent = DEFAULT_ENV;
|
|
55
55
|
const gc = this._loadGlobalConfig();
|
|
56
|
-
if (gc.api_key) envContent = envContent.replace("
|
|
57
|
-
if (gc.base_url) envContent = envContent.replace("
|
|
56
|
+
if (gc.api_key) envContent = envContent.replace("LLM_API_KEY=", `LLM_API_KEY=${gc.api_key}`);
|
|
57
|
+
if (gc.base_url) envContent = envContent.replace("LLM_BASE_URL=https://api.siliconflow.cn/v1", `LLM_BASE_URL=${gc.base_url}`);
|
|
58
58
|
if (gc.accuracy_threshold) {
|
|
59
59
|
envContent = envContent.replace("SKILL_ACCURACY=0.9", `SKILL_ACCURACY=${gc.accuracy_threshold}`);
|
|
60
60
|
envContent = envContent.replace("WORKFLOW_ACCURACY=0.9", `WORKFLOW_ACCURACY=${gc.accuracy_threshold}`);
|
|
@@ -96,7 +96,7 @@ export class ProjectInitializer extends Pipeline {
|
|
|
96
96
|
const envPath = path.join(this._workspace.cwd, ".env");
|
|
97
97
|
if (fs.existsSync(envPath)) {
|
|
98
98
|
for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
|
|
99
|
-
if (line.startsWith("SILICONFLOW_API_KEY=") && line.split("=")[1].trim()) {
|
|
99
|
+
if ((line.startsWith("LLM_API_KEY=") || line.startsWith("SILICONFLOW_API_KEY=")) && line.split("=")[1].trim()) {
|
|
100
100
|
this.configReady = true; return;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -154,4 +154,20 @@ export class ProjectInitializer extends Pipeline {
|
|
|
154
154
|
exitCriteriaMet() {
|
|
155
155
|
return this.workspaceCreated && this.configReady && this.hasRegulations && this.hasSamples;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
exportState() {
|
|
159
|
+
return {
|
|
160
|
+
workspaceCreated: this.workspaceCreated,
|
|
161
|
+
configReady: this.configReady,
|
|
162
|
+
hasRegulations: this.hasRegulations,
|
|
163
|
+
hasSamples: this.hasSamples,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
importState(data) {
|
|
168
|
+
if (data.workspaceCreated) this.workspaceCreated = true;
|
|
169
|
+
if (data.configReady) this.configReady = true;
|
|
170
|
+
if (data.hasRegulations) this.hasRegulations = true;
|
|
171
|
+
if (data.hasSamples) this.hasSamples = true;
|
|
172
|
+
}
|
|
157
173
|
}
|
|
@@ -94,4 +94,23 @@ export class ProductionQCPipeline extends Pipeline {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
exitCriteriaMet() { return this.monitoringPhase === "stable"; }
|
|
97
|
+
|
|
98
|
+
exportState() {
|
|
99
|
+
return {
|
|
100
|
+
batchesProcessed: this.batchesProcessed,
|
|
101
|
+
totalDocuments: this.totalDocuments,
|
|
102
|
+
documentsReviewed: this.documentsReviewed,
|
|
103
|
+
monitoringPhase: this.monitoringPhase,
|
|
104
|
+
accuracyByRule: this.accuracyByRule,
|
|
105
|
+
issuesCount: this.issuesFound.length,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
importState(data) {
|
|
110
|
+
if (typeof data.batchesProcessed === "number" && data.batchesProcessed > this.batchesProcessed) this.batchesProcessed = data.batchesProcessed;
|
|
111
|
+
if (typeof data.totalDocuments === "number" && data.totalDocuments > this.totalDocuments) this.totalDocuments = data.totalDocuments;
|
|
112
|
+
if (typeof data.documentsReviewed === "number" && data.documentsReviewed > this.documentsReviewed) this.documentsReviewed = data.documentsReviewed;
|
|
113
|
+
if (data.monitoringPhase) this.monitoringPhase = data.monitoringPhase;
|
|
114
|
+
if (data.accuracyByRule && typeof data.accuracyByRule === "object") Object.assign(this.accuracyByRule, data.accuracyByRule);
|
|
115
|
+
}
|
|
97
116
|
}
|
|
@@ -77,4 +77,18 @@ export class SkillAuthoringPipeline extends Pipeline {
|
|
|
77
77
|
if (!this.totalRules.length) return false;
|
|
78
78
|
return this.skillsAuthored.length >= this.totalRules.length && this.skillsWithScripts.length >= this.skillsAuthored.length * 0.5;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
exportState() {
|
|
82
|
+
return {
|
|
83
|
+
totalRules: this.totalRules,
|
|
84
|
+
skillsAuthored: this.skillsAuthored,
|
|
85
|
+
skillsWithScripts: this.skillsWithScripts,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
importState(data) {
|
|
90
|
+
if (Array.isArray(data.totalRules) && data.totalRules.length > this.totalRules.length) this.totalRules = data.totalRules;
|
|
91
|
+
if (Array.isArray(data.skillsAuthored) && data.skillsAuthored.length > this.skillsAuthored.length) this.skillsAuthored = data.skillsAuthored;
|
|
92
|
+
if (Array.isArray(data.skillsWithScripts) && data.skillsWithScripts.length > this.skillsWithScripts.length) this.skillsWithScripts = data.skillsWithScripts;
|
|
93
|
+
}
|
|
80
94
|
}
|
|
@@ -106,4 +106,24 @@ export class SkillTestingPipeline extends Pipeline {
|
|
|
106
106
|
if (!total) return false;
|
|
107
107
|
return Object.keys(this.skillsTested).length >= total && this.skillsPassing.length >= total * this._accuracyThreshold;
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
exportState() {
|
|
111
|
+
return {
|
|
112
|
+
skillsToTest: this.skillsToTest,
|
|
113
|
+
skillsTested: this.skillsTested,
|
|
114
|
+
skillsPassing: this.skillsPassing,
|
|
115
|
+
iterationCount: this.iterationCount,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
importState(data) {
|
|
120
|
+
if (typeof data.iterationCount === "number" && data.iterationCount > this.iterationCount) this.iterationCount = data.iterationCount;
|
|
121
|
+
if (Array.isArray(data.skillsToTest) && data.skillsToTest.length > this.skillsToTest.length) this.skillsToTest = data.skillsToTest;
|
|
122
|
+
if (Array.isArray(data.skillsPassing) && data.skillsPassing.length > this.skillsPassing.length) this.skillsPassing = data.skillsPassing;
|
|
123
|
+
if (data.skillsTested && typeof data.skillsTested === "object") {
|
|
124
|
+
for (const [k, v] of Object.entries(data.skillsTested)) {
|
|
125
|
+
if (!this.skillsTested[k] || v > this.skillsTested[k]) this.skillsTested[k] = v;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
109
129
|
}
|