@kernel.chat/kbot 3.97.4 → 3.99.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/dist/agent.js +22 -1
- package/dist/cli.js +163 -0
- package/dist/skills-loader.d.ts +37 -5
- package/dist/skills-loader.js +342 -50
- package/dist/teacher-logger.d.ts +71 -0
- package/dist/teacher-logger.js +162 -0
- package/dist/tools/idempotency-check.d.ts +2 -0
- package/dist/tools/idempotency-check.js +31 -0
- package/dist/tools/schedule-persistence.d.ts +2 -0
- package/dist/tools/schedule-persistence.js +19 -0
- package/dist/train-agent-trace.d.ts +29 -0
- package/dist/train-agent-trace.js +141 -0
- package/dist/train-curate.d.ts +25 -0
- package/dist/train-curate.js +354 -0
- package/dist/train-cycle.d.ts +22 -0
- package/dist/train-cycle.js +230 -0
- package/dist/train-grpo.d.ts +68 -0
- package/dist/train-grpo.js +206 -0
- package/dist/train-merge.d.ts +26 -0
- package/dist/train-merge.js +148 -0
- package/dist/train-self.d.ts +38 -0
- package/dist/train-self.js +232 -0
- package/package.json +2 -1
- package/skills/deployment/daemon-deployment/SKILL.md +70 -0
- package/skills/deployment/ship-pipeline/SKILL.md +81 -0
- package/skills/emergent/forge-reflex/SKILL.md +53 -0
- package/skills/emergent/mimic-hybrid/SKILL.md +56 -0
- package/skills/memory/dream-to-commit/SKILL.md +52 -0
- package/skills/memory/memory-cascade/SKILL.md +59 -0
- package/skills/music-production/ableton-session-build/SKILL.md +61 -0
- package/skills/orchestration/cross-agent-blackboard/SKILL.md +58 -0
- package/skills/orchestration/specialist-routing/SKILL.md +57 -0
- package/skills/self-improvement/autopoiesis-loop/SKILL.md +47 -0
- package/skills/self-improvement/skill-self-authorship/SKILL.md +70 -0
- package/skills/self-improvement/teacher-trace-curation/SKILL.md +54 -0
- package/skills/software-development/systematic-debugging/SKILL.md +86 -0
- package/skills/software-development/test-driven-development/SKILL.md +74 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// kbot Idempotency Check Tool — Prevent duplicate actions
|
|
2
|
+
// This tool verifies if a task has already been executed to avoid unintended side effects.
|
|
3
|
+
import { registerTool } from './index.js';
|
|
4
|
+
export function registerIdempotencyTools() {
|
|
5
|
+
registerTool({
|
|
6
|
+
name: 'idempotency_check',
|
|
7
|
+
description: 'Checks if an action has already been performed based on a unique identifier. Returns true if the action has been done, false otherwise.',
|
|
8
|
+
parameters: {
|
|
9
|
+
identifier: { type: 'string', description: 'Unique identifier for the action', required: true },
|
|
10
|
+
},
|
|
11
|
+
tier: 'free',
|
|
12
|
+
async execute(args) {
|
|
13
|
+
const identifier = String(args.identifier);
|
|
14
|
+
// Simulate a database check (replace with actual DB query)
|
|
15
|
+
const hasRun = await simulateDatabaseCheck(identifier);
|
|
16
|
+
if (hasRun) {
|
|
17
|
+
return 'true';
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
return 'false';
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function simulateDatabaseCheck(identifier) {
|
|
26
|
+
// Replace with actual database query logic
|
|
27
|
+
// This is a placeholder for demonstration purposes
|
|
28
|
+
console.log(`Simulating database check for identifier: ${identifier}`);
|
|
29
|
+
return false; // Assume not run for now
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=idempotency-check.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// kbot Schedule Persistence Tool — Reliable task scheduling
|
|
2
|
+
import { registerTool } from './index.js';
|
|
3
|
+
export function registerSchedulePersistenceTools() {
|
|
4
|
+
registerTool({
|
|
5
|
+
name: 'schedule_persist',
|
|
6
|
+
description: 'Persistently stores and retrieves schedules, ensuring tasks are executed even after interruptions.',
|
|
7
|
+
parameters: {
|
|
8
|
+
schedule: { type: 'string', description: 'The schedule to persist (JSON format)', required: true },
|
|
9
|
+
},
|
|
10
|
+
tier: 'pro',
|
|
11
|
+
async execute(args) {
|
|
12
|
+
const schedule = JSON.parse(String(args.schedule));
|
|
13
|
+
// Placeholder for persistence logic (replace with actual implementation)
|
|
14
|
+
console.log(`Persisting schedule: ${JSON.stringify(schedule)}`);
|
|
15
|
+
return 'Schedule persisted (implementation placeholder)';
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=schedule-persistence.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const TOOL_TOKENS: {
|
|
2
|
+
readonly think_open: "<think>";
|
|
3
|
+
readonly think_close: "</think>";
|
|
4
|
+
readonly tool_open: "<tool>";
|
|
5
|
+
readonly tool_close: "</tool>";
|
|
6
|
+
readonly args_open: "<args>";
|
|
7
|
+
readonly args_close: "</args>";
|
|
8
|
+
readonly result_open: "<result>";
|
|
9
|
+
readonly result_close: "</result>";
|
|
10
|
+
readonly answer_open: "<answer>";
|
|
11
|
+
readonly answer_close: "</answer>";
|
|
12
|
+
};
|
|
13
|
+
export interface AgentTraceOptions {
|
|
14
|
+
input?: string;
|
|
15
|
+
output?: string;
|
|
16
|
+
minTools?: number;
|
|
17
|
+
maxResultLen?: number;
|
|
18
|
+
verifiedOnly?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface AgentTraceResult {
|
|
21
|
+
output: string;
|
|
22
|
+
trajectories: number;
|
|
23
|
+
examples: number;
|
|
24
|
+
skipped_no_tools: number;
|
|
25
|
+
skipped_errors: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function formatAgentTraces(opts?: AgentTraceOptions): AgentTraceResult;
|
|
28
|
+
export declare function formatAgentTraceReport(r: AgentTraceResult): string;
|
|
29
|
+
//# sourceMappingURL=train-agent-trace.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// train-agent-trace — reformat multi-turn tool-use sessions into training examples
|
|
2
|
+
// with explicit tool tokens. Used by train-self --mode agent-trace.
|
|
3
|
+
//
|
|
4
|
+
// Output format (one example per completed trajectory):
|
|
5
|
+
// {"messages": [
|
|
6
|
+
// {"role":"user","content":"..."},
|
|
7
|
+
// {"role":"assistant","content":"<think>...</think><tool>name</tool><args>{...}</args>"},
|
|
8
|
+
// {"role":"tool","content":"<result>...</result>"},
|
|
9
|
+
// {"role":"assistant","content":"<think>...</think><answer>...</answer>"}
|
|
10
|
+
// ]}
|
|
11
|
+
//
|
|
12
|
+
// Tool tokens we inject:
|
|
13
|
+
// <think>...</think> — reasoning
|
|
14
|
+
// <tool>name</tool> — tool selected
|
|
15
|
+
// <args>{...}</args> — JSON arguments
|
|
16
|
+
// <result>...</result> — tool output (truncated)
|
|
17
|
+
// <answer>...</answer> — final assistant turn
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
export const TOOL_TOKENS = {
|
|
22
|
+
think_open: '<think>', think_close: '</think>',
|
|
23
|
+
tool_open: '<tool>', tool_close: '</tool>',
|
|
24
|
+
args_open: '<args>', args_close: '</args>',
|
|
25
|
+
result_open: '<result>', result_close: '</result>',
|
|
26
|
+
answer_open: '<answer>', answer_close: '</answer>',
|
|
27
|
+
};
|
|
28
|
+
function truncate(s, n) {
|
|
29
|
+
if (s.length <= n)
|
|
30
|
+
return s;
|
|
31
|
+
return s.slice(0, n) + ` …[+${s.length - n} chars]`;
|
|
32
|
+
}
|
|
33
|
+
/** Build an assistant turn with think + tool call tokens. */
|
|
34
|
+
function assistantToolTurn(thinking, toolName, args) {
|
|
35
|
+
const thinkPart = thinking
|
|
36
|
+
? `${TOOL_TOKENS.think_open}${thinking.trim()}${TOOL_TOKENS.think_close}\n`
|
|
37
|
+
: '';
|
|
38
|
+
const argsJson = JSON.stringify(args);
|
|
39
|
+
return `${thinkPart}${TOOL_TOKENS.tool_open}${toolName}${TOOL_TOKENS.tool_close}\n${TOOL_TOKENS.args_open}${argsJson}${TOOL_TOKENS.args_close}`;
|
|
40
|
+
}
|
|
41
|
+
/** Build a tool-result turn. */
|
|
42
|
+
function toolResultTurn(result, maxLen) {
|
|
43
|
+
return `${TOOL_TOKENS.result_open}${truncate(result, maxLen)}${TOOL_TOKENS.result_close}`;
|
|
44
|
+
}
|
|
45
|
+
/** Build the final assistant answer turn. */
|
|
46
|
+
function answerTurn(thinking, content) {
|
|
47
|
+
const thinkPart = thinking
|
|
48
|
+
? `${TOOL_TOKENS.think_open}${thinking.trim()}${TOOL_TOKENS.think_close}\n`
|
|
49
|
+
: '';
|
|
50
|
+
return `${thinkPart}${TOOL_TOKENS.answer_open}${content.trim()}${TOOL_TOKENS.answer_close}`;
|
|
51
|
+
}
|
|
52
|
+
/** Convert one teacher trace into an agent-trace example. */
|
|
53
|
+
function formatTrace(t, maxResult) {
|
|
54
|
+
const toolCalls = t.response.tool_calls || [];
|
|
55
|
+
if (toolCalls.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const messages = [];
|
|
58
|
+
// Seed with conversation context (excluding the final assistant turn)
|
|
59
|
+
for (const m of t.messages) {
|
|
60
|
+
if (m.role === 'user' || m.role === 'system') {
|
|
61
|
+
messages.push({ role: m.role, content: m.content });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Inject assistant tool-call turn
|
|
65
|
+
const firstCall = toolCalls[0];
|
|
66
|
+
messages.push({
|
|
67
|
+
role: 'assistant',
|
|
68
|
+
content: assistantToolTurn(t.response.thinking || '', firstCall.name, firstCall.arguments),
|
|
69
|
+
});
|
|
70
|
+
// We don't have the actual tool result in teacher traces — synthesize a placeholder
|
|
71
|
+
// that the model can learn to parse. In production, the tool executor would fill this
|
|
72
|
+
// when replaying recorded sessions.
|
|
73
|
+
messages.push({
|
|
74
|
+
role: 'tool',
|
|
75
|
+
content: toolResultTurn('[result from ' + firstCall.name + ']', maxResult),
|
|
76
|
+
});
|
|
77
|
+
// Final answer
|
|
78
|
+
messages.push({
|
|
79
|
+
role: 'assistant',
|
|
80
|
+
content: answerTurn('', t.response.content),
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
messages,
|
|
84
|
+
_tool_count: toolCalls.length,
|
|
85
|
+
_trace_id: t.id,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function formatAgentTraces(opts = {}) {
|
|
89
|
+
const input = opts.input ?? join(homedir(), '.kbot', 'teacher', 'traces.jsonl');
|
|
90
|
+
const output = opts.output ?? join(homedir(), '.kbot', 'teacher', 'dataset-agent-trace.jsonl');
|
|
91
|
+
const minTools = opts.minTools ?? 1;
|
|
92
|
+
const maxResult = opts.maxResultLen ?? 1200;
|
|
93
|
+
const result = {
|
|
94
|
+
output,
|
|
95
|
+
trajectories: 0,
|
|
96
|
+
examples: 0,
|
|
97
|
+
skipped_no_tools: 0,
|
|
98
|
+
skipped_errors: 0,
|
|
99
|
+
};
|
|
100
|
+
if (!existsSync(input))
|
|
101
|
+
return result;
|
|
102
|
+
// Truncate output
|
|
103
|
+
writeFileSync(output, '');
|
|
104
|
+
const lines = readFileSync(input, 'utf-8').split('\n').filter(l => l.trim());
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
try {
|
|
107
|
+
const t = JSON.parse(line);
|
|
108
|
+
result.trajectories++;
|
|
109
|
+
if (opts.verifiedOnly && !t.outcome?.verified) {
|
|
110
|
+
result.skipped_errors++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const toolCount = t.response.tool_calls?.length || 0;
|
|
114
|
+
if (toolCount < minTools) {
|
|
115
|
+
result.skipped_no_tools++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const ex = formatTrace(t, maxResult);
|
|
119
|
+
if (ex) {
|
|
120
|
+
appendFileSync(output, JSON.stringify(ex) + '\n');
|
|
121
|
+
result.examples++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
result.skipped_errors++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
export function formatAgentTraceReport(r) {
|
|
131
|
+
return [
|
|
132
|
+
'agent-trace formatter',
|
|
133
|
+
'─'.repeat(40),
|
|
134
|
+
` Output: ${r.output}`,
|
|
135
|
+
` Trajectories seen: ${r.trajectories}`,
|
|
136
|
+
` Examples emitted: ${r.examples}`,
|
|
137
|
+
` Skipped (no tools): ${r.skipped_no_tools}`,
|
|
138
|
+
` Skipped (errors): ${r.skipped_errors}`,
|
|
139
|
+
].join('\n');
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=train-agent-trace.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type CurateMode = 'default' | 'reasoning' | 'agent-trace' | 'code-only';
|
|
2
|
+
export interface CurateOptions {
|
|
3
|
+
sources?: string[];
|
|
4
|
+
output?: string;
|
|
5
|
+
mode?: CurateMode;
|
|
6
|
+
maxExamples?: number;
|
|
7
|
+
minScore?: number;
|
|
8
|
+
minResponseLen?: number;
|
|
9
|
+
maxResponseLen?: number;
|
|
10
|
+
dedupe?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface CurateResult {
|
|
13
|
+
output: string;
|
|
14
|
+
total_examined: number;
|
|
15
|
+
kept: number;
|
|
16
|
+
rejected: number;
|
|
17
|
+
duplicates: number;
|
|
18
|
+
mean_score: number;
|
|
19
|
+
by_source: Record<string, number>;
|
|
20
|
+
}
|
|
21
|
+
/** Run the curator end-to-end. Returns a report. */
|
|
22
|
+
export declare function curate(opts?: CurateOptions): CurateResult;
|
|
23
|
+
/** Format as a human-readable report */
|
|
24
|
+
export declare function formatCurateReport(r: CurateResult): string;
|
|
25
|
+
//# sourceMappingURL=train-curate.d.ts.map
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// Training data curator — reads teacher traces and ~/.kbot/observer/session.jsonl,
|
|
2
|
+
// scores + filters examples, emits a clean JSONL ready for fine-tuning.
|
|
3
|
+
//
|
|
4
|
+
// Scoring signals:
|
|
5
|
+
// + response length is reasonable (100–8000 tokens)
|
|
6
|
+
// + contained a thinking block (for reasoning distill)
|
|
7
|
+
// + tool calls had no error results
|
|
8
|
+
// + outcome.verified === true (if tagged)
|
|
9
|
+
// − user retried or gave negative feedback
|
|
10
|
+
// − response contained "I don't know" / "I can't help"
|
|
11
|
+
// − near-duplicate of another example (hash-based)
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, readdirSync, statSync } from 'node:fs';
|
|
13
|
+
import { join, resolve } from 'node:path';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
const NEG_PATTERNS = [
|
|
17
|
+
/\bi (don'?t|do not) (know|have that|have access)\b/i,
|
|
18
|
+
/\bi (can'?t|cannot) (help|assist|do)\b/i,
|
|
19
|
+
/\bas an ai\b/i,
|
|
20
|
+
/\bi'?m sorry,? but\b/i,
|
|
21
|
+
/\bi apologi[sz]e\b/i,
|
|
22
|
+
];
|
|
23
|
+
function hashText(text) {
|
|
24
|
+
return createHash('sha256').update(text.slice(0, 2000).toLowerCase().replace(/\s+/g, ' ').trim()).digest('hex').slice(0, 16);
|
|
25
|
+
}
|
|
26
|
+
function scoreExample(ex, mode) {
|
|
27
|
+
let score = 0.5;
|
|
28
|
+
const reasons = [];
|
|
29
|
+
const lastAssistant = [...ex.messages].reverse().find(m => m.role === 'assistant')?.content || '';
|
|
30
|
+
const lastUser = [...ex.messages].reverse().find(m => m.role === 'user')?.content || '';
|
|
31
|
+
const respLen = lastAssistant.length;
|
|
32
|
+
// Length signal
|
|
33
|
+
if (respLen >= 200 && respLen <= 8000) {
|
|
34
|
+
score += 0.15;
|
|
35
|
+
reasons.push('good_length');
|
|
36
|
+
}
|
|
37
|
+
if (respLen < 80) {
|
|
38
|
+
score -= 0.3;
|
|
39
|
+
reasons.push('too_short');
|
|
40
|
+
}
|
|
41
|
+
if (respLen > 16000) {
|
|
42
|
+
score -= 0.15;
|
|
43
|
+
reasons.push('too_long');
|
|
44
|
+
}
|
|
45
|
+
// Refusal / low-effort signal
|
|
46
|
+
for (const pat of NEG_PATTERNS) {
|
|
47
|
+
if (pat.test(lastAssistant)) {
|
|
48
|
+
score -= 0.25;
|
|
49
|
+
reasons.push('refusal_like');
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Mode-specific scoring
|
|
54
|
+
if (mode === 'reasoning') {
|
|
55
|
+
if (ex.thinking && ex.thinking.length > 100) {
|
|
56
|
+
score += 0.25;
|
|
57
|
+
reasons.push('has_thinking');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
score -= 0.2;
|
|
61
|
+
reasons.push('no_thinking');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (mode === 'agent-trace') {
|
|
65
|
+
if (ex.tool_calls && ex.tool_calls.length > 0) {
|
|
66
|
+
score += 0.25;
|
|
67
|
+
reasons.push('has_tool_calls');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
score -= 0.3;
|
|
71
|
+
reasons.push('no_tool_calls');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (mode === 'code-only') {
|
|
75
|
+
if (/```[a-zA-Z]+/.test(lastAssistant)) {
|
|
76
|
+
score += 0.15;
|
|
77
|
+
reasons.push('has_code_block');
|
|
78
|
+
}
|
|
79
|
+
if (/\b(function|class|import|const|def|fn )\b/.test(lastAssistant)) {
|
|
80
|
+
score += 0.1;
|
|
81
|
+
reasons.push('code_keywords');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Outcome-tagged verified example
|
|
85
|
+
if (ex.outcome?.verified) {
|
|
86
|
+
score += 0.3;
|
|
87
|
+
reasons.push('verified');
|
|
88
|
+
}
|
|
89
|
+
if (ex.outcome?.signal === 'user_retry') {
|
|
90
|
+
score -= 0.4;
|
|
91
|
+
reasons.push('user_retried');
|
|
92
|
+
}
|
|
93
|
+
if (ex.outcome?.signal === 'build_pass' || ex.outcome?.signal === 'test_pass') {
|
|
94
|
+
score += 0.35;
|
|
95
|
+
reasons.push(ex.outcome.signal);
|
|
96
|
+
}
|
|
97
|
+
// Interaction quality
|
|
98
|
+
if (lastUser.length > 20 && lastUser.length < 4000) {
|
|
99
|
+
score += 0.05;
|
|
100
|
+
reasons.push('good_prompt_len');
|
|
101
|
+
}
|
|
102
|
+
return { score: Math.max(0, Math.min(1, score)), reasons };
|
|
103
|
+
}
|
|
104
|
+
/** Parse a single line from teacher/traces.jsonl into a normalized Example. */
|
|
105
|
+
function parseTeacherLine(line) {
|
|
106
|
+
try {
|
|
107
|
+
const t = JSON.parse(line);
|
|
108
|
+
const messages = t.messages || [];
|
|
109
|
+
const response = t.response;
|
|
110
|
+
if (!response?.content || messages.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
messages: [...messages, { role: 'assistant', content: response.content }],
|
|
114
|
+
thinking: response.thinking,
|
|
115
|
+
tool_calls: response.tool_calls,
|
|
116
|
+
outcome: t.outcome,
|
|
117
|
+
meta: { source: 'teacher', provider: t.provider, model: t.model, ts: t.ts },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Parse ~/.kbot/observer/session.jsonl. Shape varies; be permissive. */
|
|
125
|
+
function parseObserverLine(line) {
|
|
126
|
+
try {
|
|
127
|
+
const o = JSON.parse(line);
|
|
128
|
+
// Expected fields: input/output or user/assistant or prompt/response
|
|
129
|
+
const user = (o.input || o.user || o.prompt || o.query);
|
|
130
|
+
const assistant = (o.output || o.assistant || o.response || o.answer);
|
|
131
|
+
if (!user || !assistant || typeof user !== 'string' || typeof assistant !== 'string')
|
|
132
|
+
return null;
|
|
133
|
+
return {
|
|
134
|
+
messages: [
|
|
135
|
+
{ role: 'user', content: user },
|
|
136
|
+
{ role: 'assistant', content: assistant },
|
|
137
|
+
],
|
|
138
|
+
meta: { source: 'observer', ts: o.ts },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function extractContent(raw) {
|
|
146
|
+
if (typeof raw === 'string')
|
|
147
|
+
return raw;
|
|
148
|
+
if (Array.isArray(raw)) {
|
|
149
|
+
return raw.map(b => (b && typeof b === 'object' && 'text' in b) ? String(b.text || '') : '').join('\n');
|
|
150
|
+
}
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
function parseClaudeCodeFile(filePath) {
|
|
154
|
+
let lines;
|
|
155
|
+
try {
|
|
156
|
+
lines = readFileSync(filePath, 'utf-8').split('\n').filter(l => l.trim());
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const out = [];
|
|
162
|
+
let pendingUser = null;
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
let turn;
|
|
165
|
+
try {
|
|
166
|
+
turn = JSON.parse(line);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (turn.type === 'user' && turn.message) {
|
|
172
|
+
const content = extractContent(turn.message.content);
|
|
173
|
+
if (content.length > 10 && !content.startsWith('<tool_result'))
|
|
174
|
+
pendingUser = content;
|
|
175
|
+
}
|
|
176
|
+
else if (turn.type === 'assistant' && turn.message && pendingUser) {
|
|
177
|
+
const content = extractContent(turn.message.content);
|
|
178
|
+
if (content.length > 40) {
|
|
179
|
+
out.push({
|
|
180
|
+
messages: [
|
|
181
|
+
{ role: 'user', content: pendingUser },
|
|
182
|
+
{ role: 'assistant', content },
|
|
183
|
+
],
|
|
184
|
+
meta: { source: 'claude-code', session: turn.sessionId, file: filePath },
|
|
185
|
+
});
|
|
186
|
+
pendingUser = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
function findClaudeCodeSessions(limitFiles = 40, maxDepth = 4) {
|
|
193
|
+
const base = join(homedir(), '.claude', 'projects');
|
|
194
|
+
if (!existsSync(base))
|
|
195
|
+
return [];
|
|
196
|
+
const files = [];
|
|
197
|
+
function walk(dir, depth) {
|
|
198
|
+
if (depth > maxDepth)
|
|
199
|
+
return;
|
|
200
|
+
let entries;
|
|
201
|
+
try {
|
|
202
|
+
entries = readdirSync(dir);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (entry.startsWith('.') || entry === 'node_modules')
|
|
209
|
+
continue;
|
|
210
|
+
const full = join(dir, entry);
|
|
211
|
+
let st;
|
|
212
|
+
try {
|
|
213
|
+
st = statSync(full);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (st.isDirectory()) {
|
|
219
|
+
walk(full, depth + 1);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (entry.endsWith('.jsonl') && st.size > 2048)
|
|
223
|
+
files.push({ path: full, size: st.size });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
walk(base, 0);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
files.sort((a, b) => b.size - a.size);
|
|
233
|
+
return files.slice(0, limitFiles).map(f => f.path);
|
|
234
|
+
}
|
|
235
|
+
function defaultSources(includeClaudeCode = true) {
|
|
236
|
+
const home = homedir();
|
|
237
|
+
const list = [
|
|
238
|
+
join(home, '.kbot', 'teacher', 'traces.jsonl'),
|
|
239
|
+
join(home, '.kbot', 'teacher', 'corrections.jsonl'),
|
|
240
|
+
join(home, '.kbot', 'observer', 'session.jsonl'),
|
|
241
|
+
].filter(p => existsSync(p));
|
|
242
|
+
if (includeClaudeCode)
|
|
243
|
+
list.push(...findClaudeCodeSessions());
|
|
244
|
+
return list;
|
|
245
|
+
}
|
|
246
|
+
function readLines(file) {
|
|
247
|
+
try {
|
|
248
|
+
return readFileSync(file, 'utf-8').split('\n').filter(l => l.trim().length > 0);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function parseSource(file) {
|
|
255
|
+
if (file.includes('/.claude/projects/'))
|
|
256
|
+
return parseClaudeCodeFile(file);
|
|
257
|
+
const lines = readLines(file);
|
|
258
|
+
if (file.includes('teacher') || file.includes('corrections'))
|
|
259
|
+
return lines.map(parseTeacherLine).filter((x) => x !== null);
|
|
260
|
+
if (file.includes('observer'))
|
|
261
|
+
return lines.map(parseObserverLine).filter((x) => x !== null);
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const line of lines) {
|
|
264
|
+
const t = parseTeacherLine(line) || parseObserverLine(line);
|
|
265
|
+
if (t)
|
|
266
|
+
out.push(t);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
/** Run the curator end-to-end. Returns a report. */
|
|
271
|
+
export function curate(opts = {}) {
|
|
272
|
+
const mode = opts.mode ?? 'default';
|
|
273
|
+
const sources = opts.sources ?? defaultSources();
|
|
274
|
+
const output = resolve(opts.output ?? join(homedir(), '.kbot', 'teacher', `dataset-${mode}.jsonl`));
|
|
275
|
+
const maxExamples = opts.maxExamples ?? 5000;
|
|
276
|
+
const minScore = opts.minScore ?? 0.5;
|
|
277
|
+
const minResp = opts.minResponseLen ?? 80;
|
|
278
|
+
const maxResp = opts.maxResponseLen ?? 32000;
|
|
279
|
+
const dedupe = opts.dedupe !== false;
|
|
280
|
+
const outDir = output.substring(0, output.lastIndexOf('/'));
|
|
281
|
+
if (outDir && !existsSync(outDir))
|
|
282
|
+
mkdirSync(outDir, { recursive: true });
|
|
283
|
+
const bySource = {};
|
|
284
|
+
const all = [];
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
let total = 0;
|
|
287
|
+
let duplicates = 0;
|
|
288
|
+
for (const src of sources) {
|
|
289
|
+
const examples = parseSource(src);
|
|
290
|
+
bySource[src] = examples.length;
|
|
291
|
+
for (const ex of examples) {
|
|
292
|
+
total++;
|
|
293
|
+
const lastAssistant = [...ex.messages].reverse().find(m => m.role === 'assistant')?.content || '';
|
|
294
|
+
if (lastAssistant.length < minResp || lastAssistant.length > maxResp)
|
|
295
|
+
continue;
|
|
296
|
+
const hash = hashText(ex.messages.map(m => m.content).join('|'));
|
|
297
|
+
if (dedupe && seen.has(hash)) {
|
|
298
|
+
duplicates++;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
seen.add(hash);
|
|
302
|
+
const { score, reasons } = scoreExample(ex, mode);
|
|
303
|
+
if (score < minScore)
|
|
304
|
+
continue;
|
|
305
|
+
all.push({ ...ex, score, hash, reasons });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Sort by score desc, cap
|
|
309
|
+
all.sort((a, b) => b.score - a.score);
|
|
310
|
+
const kept = all.slice(0, maxExamples);
|
|
311
|
+
// Emit as JSONL with OpenAI-style {messages: [...]} format that train_prepare understands
|
|
312
|
+
if (existsSync(output))
|
|
313
|
+
writeFileSync(output, ''); // truncate
|
|
314
|
+
for (const ex of kept) {
|
|
315
|
+
const record = { messages: ex.messages };
|
|
316
|
+
if (ex.thinking)
|
|
317
|
+
record.thinking = ex.thinking;
|
|
318
|
+
if (ex.tool_calls)
|
|
319
|
+
record.tool_calls = ex.tool_calls;
|
|
320
|
+
record._score = ex.score;
|
|
321
|
+
record._reasons = ex.reasons;
|
|
322
|
+
appendFileSync(output, JSON.stringify(record) + '\n');
|
|
323
|
+
}
|
|
324
|
+
const meanScore = kept.length > 0 ? kept.reduce((s, e) => s + e.score, 0) / kept.length : 0;
|
|
325
|
+
return {
|
|
326
|
+
output,
|
|
327
|
+
total_examined: total,
|
|
328
|
+
kept: kept.length,
|
|
329
|
+
rejected: total - kept.length - duplicates,
|
|
330
|
+
duplicates,
|
|
331
|
+
mean_score: Math.round(meanScore * 1000) / 1000,
|
|
332
|
+
by_source: bySource,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/** Format as a human-readable report */
|
|
336
|
+
export function formatCurateReport(r) {
|
|
337
|
+
const lines = [
|
|
338
|
+
`Curate Report`,
|
|
339
|
+
`${'─'.repeat(40)}`,
|
|
340
|
+
` Output: ${r.output}`,
|
|
341
|
+
` Examined: ${r.total_examined}`,
|
|
342
|
+
` Kept: ${r.kept}`,
|
|
343
|
+
` Rejected: ${r.rejected}`,
|
|
344
|
+
` Duplicates: ${r.duplicates}`,
|
|
345
|
+
` Mean score: ${r.mean_score.toFixed(3)}`,
|
|
346
|
+
'',
|
|
347
|
+
` Sources:`,
|
|
348
|
+
];
|
|
349
|
+
for (const [src, count] of Object.entries(r.by_source)) {
|
|
350
|
+
lines.push(` ${count.toString().padStart(6)} ${src}`);
|
|
351
|
+
}
|
|
352
|
+
return lines.join('\n');
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=train-curate.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface TrainCycleOptions {
|
|
2
|
+
studentModel?: string;
|
|
3
|
+
teacherProvider?: 'anthropic' | 'openai';
|
|
4
|
+
teacherModel?: string;
|
|
5
|
+
promptsFile?: string;
|
|
6
|
+
corrections?: string;
|
|
7
|
+
samples?: number;
|
|
8
|
+
passThreshold?: number;
|
|
9
|
+
retrain?: boolean;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface CycleResult {
|
|
13
|
+
sampled: number;
|
|
14
|
+
passed: number;
|
|
15
|
+
corrected: number;
|
|
16
|
+
skipped: number;
|
|
17
|
+
corrections_file: string;
|
|
18
|
+
retrain_summary?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function runCycle(opts?: TrainCycleOptions): Promise<CycleResult>;
|
|
21
|
+
export declare function formatCycleReport(r: CycleResult): string;
|
|
22
|
+
//# sourceMappingURL=train-cycle.d.ts.map
|