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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { estimateTokens } from "./token-counter.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Append-only JSONL event log for KC agent sessions.
|
|
7
|
+
* Each line is a JSON object: { seq, ts, type, data }
|
|
8
|
+
*
|
|
9
|
+
* This is the source of truth for session history. ConversationHistory
|
|
10
|
+
* and display logs become views over this log.
|
|
11
|
+
*/
|
|
12
|
+
export class EventLog {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} workspacePath - Session workspace directory
|
|
15
|
+
*/
|
|
16
|
+
constructor(workspacePath) {
|
|
17
|
+
this._dir = path.join(workspacePath, "logs");
|
|
18
|
+
this._logPath = path.join(this._dir, "events.jsonl");
|
|
19
|
+
this._seq = 0;
|
|
20
|
+
this._estimatedTokens = 0;
|
|
21
|
+
this._initFromExisting();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Current sequence number */
|
|
25
|
+
get currentSeq() { return this._seq; }
|
|
26
|
+
|
|
27
|
+
/** Estimated total tokens across all events */
|
|
28
|
+
get estimatedTokens() { return this._estimatedTokens; }
|
|
29
|
+
|
|
30
|
+
/** Path to the log file */
|
|
31
|
+
get logPath() { return this._logPath; }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize sequence counter and token estimate from existing log file.
|
|
35
|
+
*/
|
|
36
|
+
_initFromExisting() {
|
|
37
|
+
if (!fs.existsSync(this._logPath)) return;
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(this._logPath, "utf-8");
|
|
40
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
try {
|
|
43
|
+
const event = JSON.parse(line);
|
|
44
|
+
if (event.seq > this._seq) this._seq = event.seq;
|
|
45
|
+
this._estimatedTokens += this._eventTokens(event);
|
|
46
|
+
} catch { /* skip malformed lines */ }
|
|
47
|
+
}
|
|
48
|
+
} catch { /* file read error, start fresh */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Append an event to the log.
|
|
53
|
+
* @param {string} type - Event type
|
|
54
|
+
* @param {object} [data] - Event payload
|
|
55
|
+
* @returns {number} The sequence number of the appended event
|
|
56
|
+
*/
|
|
57
|
+
append(type, data = {}) {
|
|
58
|
+
this._seq++;
|
|
59
|
+
const event = {
|
|
60
|
+
seq: this._seq,
|
|
61
|
+
ts: new Date().toISOString(),
|
|
62
|
+
type,
|
|
63
|
+
data,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
fs.mkdirSync(this._dir, { recursive: true });
|
|
67
|
+
fs.appendFileSync(this._logPath, JSON.stringify(event) + "\n", "utf-8");
|
|
68
|
+
|
|
69
|
+
this._estimatedTokens += this._eventTokens(event);
|
|
70
|
+
return this._seq;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read events from the log with optional filtering.
|
|
75
|
+
* @param {object} [opts]
|
|
76
|
+
* @param {number} [opts.fromSeq] - Start reading from this sequence (inclusive)
|
|
77
|
+
* @param {number} [opts.toSeq] - Stop reading at this sequence (inclusive)
|
|
78
|
+
* @param {string[]} [opts.types] - Only return events of these types
|
|
79
|
+
* @returns {Array<object>}
|
|
80
|
+
*/
|
|
81
|
+
read({ fromSeq = 0, toSeq = Infinity, types } = {}) {
|
|
82
|
+
if (!fs.existsSync(this._logPath)) return [];
|
|
83
|
+
|
|
84
|
+
const events = [];
|
|
85
|
+
const content = fs.readFileSync(this._logPath, "utf-8");
|
|
86
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
87
|
+
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
try {
|
|
90
|
+
const event = JSON.parse(line);
|
|
91
|
+
if (event.seq < fromSeq || event.seq > toSeq) continue;
|
|
92
|
+
if (types && !types.includes(event.type)) continue;
|
|
93
|
+
events.push(event);
|
|
94
|
+
} catch { /* skip */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return events;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Estimate tokens for a single event (for running total).
|
|
102
|
+
* @param {object} event
|
|
103
|
+
* @returns {number}
|
|
104
|
+
*/
|
|
105
|
+
_eventTokens(event) {
|
|
106
|
+
const dataStr = typeof event.data === "string"
|
|
107
|
+
? event.data
|
|
108
|
+
: JSON.stringify(event.data || {});
|
|
109
|
+
return estimateTokens(dataStr);
|
|
110
|
+
}
|
|
111
|
+
}
|
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
|
}
|