@kernel.chat/kbot 3.97.1 → 3.98.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <img src="https://raw.githubusercontent.com/isaacsight/kernel/main/tools/video-assets/demo.gif" alt="kbot demo" width="700">
9
+ <img src="https://raw.githubusercontent.com/isaacsight/kernel/main/tools/video-assets/demo-quick.gif" alt="kbot demo" width="700">
10
10
  </p>
11
11
 
12
12
  <p align="center">
package/dist/agent.js CHANGED
@@ -654,6 +654,19 @@ function isComplexTask(message) {
654
654
  async function callProvider(provider, apiKey, model, systemContext, messages, tools, options) {
655
655
  const p = getProvider(provider);
656
656
  const startTime = Date.now();
657
+ // Teacher logger — captures (prompt, response) pairs for later distillation.
658
+ // Disabled for local providers (already free) and when KBOT_TEACHER_LOG=0.
659
+ const { getTeacherLogger } = await import('./teacher-logger.js');
660
+ const teacher = getTeacherLogger();
661
+ const teacherId = (teacher.isEnabled() && !isLocalProvider(provider))
662
+ ? teacher.begin({
663
+ sessionId: options?.sessionId,
664
+ provider,
665
+ model,
666
+ system: systemContext,
667
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
668
+ })
669
+ : '';
657
670
  try {
658
671
  let result;
659
672
  // Embedded inference — runs in-process via node-llama-cpp, no HTTP
@@ -692,6 +705,14 @@ async function callProvider(provider, apiKey, model, systemContext, messages, to
692
705
  }
693
706
  }
694
707
  recordSuccess(provider, Date.now() - startTime);
708
+ if (teacherId) {
709
+ teacher.end(teacherId, {
710
+ content: result.content,
711
+ thinking: result.thinking,
712
+ tool_calls: result.tool_calls,
713
+ stop_reason: result.stop_reason,
714
+ }, result.usage);
715
+ }
695
716
  return result;
696
717
  }
697
718
  catch (err) {
package/dist/cli.js CHANGED
@@ -3875,6 +3875,125 @@ async function main() {
3875
3875
  pikaCmd.action(() => {
3876
3876
  pikaCmd.commands.find(c => c.name() === 'status')?.parse(['', '', 'status']);
3877
3877
  });
3878
+ // ── train-self / train-cycle / train-merge / train-grpo ──
3879
+ // Fine-tune a local model on your own agent sessions.
3880
+ program
3881
+ .command('train-self')
3882
+ .description('Fine-tune a local model on your own kbot sessions (MLX LoRA)')
3883
+ .option('--mode <mode>', 'default | reasoning | agent-trace | code-only', 'default')
3884
+ .option('--base-model <model>', 'Override base model (HF path or mlx-community/*)')
3885
+ .option('--output-name <name>', 'Ollama model name to register')
3886
+ .option('--backend <backend>', 'mlx | unsloth | llama-cpp | together', 'mlx')
3887
+ .option('--iters <n>', 'Training iterations', (v) => parseInt(v, 10))
3888
+ .option('--batch-size <n>', 'Batch size', (v) => parseInt(v, 10))
3889
+ .option('--num-layers <n>', 'LoRA layers', (v) => parseInt(v, 10))
3890
+ .option('--learning-rate <lr>', 'Learning rate', parseFloat)
3891
+ .option('--max-examples <n>', 'Cap curated examples', (v) => parseInt(v, 10))
3892
+ .option('--dry-run', 'Curate only, do not train')
3893
+ .option('--skip-curate', 'Skip curation (use existing dataset)')
3894
+ .option('--skip-train', 'Skip training (prepare + deploy only)')
3895
+ .option('--skip-deploy', 'Skip Ollama deploy')
3896
+ .option('--no-grad-checkpoint', 'Disable gradient checkpointing')
3897
+ .action(async (opts) => {
3898
+ const { trainSelf, formatTrainSelfReport } = await import('./train-self.js');
3899
+ const r = await trainSelf({
3900
+ mode: opts.mode,
3901
+ baseModel: opts.baseModel,
3902
+ outputName: opts.outputName,
3903
+ backend: opts.backend,
3904
+ iters: opts.iters,
3905
+ batchSize: opts.batchSize,
3906
+ numLayers: opts.numLayers,
3907
+ learningRate: opts.learningRate,
3908
+ maxExamples: opts.maxExamples,
3909
+ dryRun: Boolean(opts.dryRun),
3910
+ skipCurate: Boolean(opts.skipCurate),
3911
+ skipTrain: Boolean(opts.skipTrain),
3912
+ skipDeploy: Boolean(opts.skipDeploy),
3913
+ gradCheckpoint: opts.gradCheckpoint !== false,
3914
+ });
3915
+ console.log(formatTrainSelfReport(r));
3916
+ });
3917
+ program
3918
+ .command('train-cycle')
3919
+ .description('On-policy distillation: student generates → Claude grades/corrects → retrain')
3920
+ .option('--student <model>', 'Local student model (Ollama)', 'kernel-coder:latest')
3921
+ .option('--teacher <model>', 'Teacher model', 'claude-opus-4-6')
3922
+ .option('--samples <n>', 'Prompts to sample per cycle', (v) => parseInt(v, 10), 50)
3923
+ .option('--threshold <score>', 'Pass threshold 0..1', parseFloat, 0.6)
3924
+ .option('--retrain', 'Trigger train-self after collecting corrections')
3925
+ .option('--dry-run', 'Skip teacher grading, just test student generation')
3926
+ .action(async (opts) => {
3927
+ const { runCycle, formatCycleReport } = await import('./train-cycle.js');
3928
+ const r = await runCycle({
3929
+ studentModel: opts.student,
3930
+ teacherModel: opts.teacher,
3931
+ samples: opts.samples,
3932
+ passThreshold: opts.threshold,
3933
+ retrain: Boolean(opts.retrain),
3934
+ dryRun: Boolean(opts.dryRun),
3935
+ });
3936
+ console.log(formatCycleReport(r));
3937
+ });
3938
+ program
3939
+ .command('train-merge')
3940
+ .description('Merge models via MergeKit (TIES/SLERP/DARE)')
3941
+ .option('--method <method>', 'ties | slerp | dare_ties | linear', 'ties')
3942
+ .option('--base <model>', 'Base model (HF path)', 'Qwen/Qwen2.5-Coder-7B-Instruct')
3943
+ .option('--output <name>', 'Output name')
3944
+ .option('--default', 'Use kbot triad defaults (qwen-coder + deepseek-r1 + self)')
3945
+ .option('--deploy', 'Register with Ollama after merge')
3946
+ .action(async (opts) => {
3947
+ const { mergeKbotDefault, mergeModels, formatMergeReport } = await import('./train-merge.js');
3948
+ if (opts.default) {
3949
+ const r = await mergeKbotDefault();
3950
+ console.log(formatMergeReport(r));
3951
+ return;
3952
+ }
3953
+ const r = await mergeModels({
3954
+ method: opts.method,
3955
+ baseModel: opts.base,
3956
+ models: [
3957
+ { model: opts.base, weight: 1, density: 0.5 },
3958
+ ],
3959
+ outputName: opts.output,
3960
+ deploy: Boolean(opts.deploy),
3961
+ });
3962
+ console.log(formatMergeReport(r));
3963
+ });
3964
+ program
3965
+ .command('train-grpo')
3966
+ .description('GRPO on verifiable tasks (build-pass, test-pass, regex-match, json-valid)')
3967
+ .option('--student <model>', 'Student model', 'kernel-coder:latest')
3968
+ .option('--group-size <n>', 'Rollouts per prompt', (v) => parseInt(v, 10), 8)
3969
+ .option('--iters <n>', 'Outer iterations', (v) => parseInt(v, 10), 100)
3970
+ .option('--dry-run', 'Collect rollouts only, do not update weights')
3971
+ .option('--runner-cmd <cmd>', 'External GRPO runner command')
3972
+ .action(async (opts) => {
3973
+ const { runGrpoRollouts, DEFAULT_VERIFIER_SUITE, formatGrpoReport } = await import('./train-grpo.js');
3974
+ const r = await runGrpoRollouts({
3975
+ studentModel: opts.student,
3976
+ prompts: DEFAULT_VERIFIER_SUITE,
3977
+ groupSize: opts.groupSize,
3978
+ iters: opts.iters,
3979
+ dryRun: Boolean(opts.dryRun),
3980
+ runnerCmd: opts.runnerCmd,
3981
+ });
3982
+ console.log(formatGrpoReport(r));
3983
+ });
3984
+ program
3985
+ .command('train-agent-trace')
3986
+ .description('Reformat tool-use traces as agent training examples')
3987
+ .option('--min-tools <n>', 'Minimum tool calls per trajectory', (v) => parseInt(v, 10), 1)
3988
+ .option('--verified-only', 'Only use trajectories tagged verified')
3989
+ .action(async (opts) => {
3990
+ const { formatAgentTraces, formatAgentTraceReport } = await import('./train-agent-trace.js');
3991
+ const r = formatAgentTraces({
3992
+ minTools: opts.minTools,
3993
+ verifiedOnly: Boolean(opts.verifiedOnly),
3994
+ });
3995
+ console.log(formatAgentTraceReport(r));
3996
+ });
3878
3997
  program.parse(process.argv);
3879
3998
  const opts = program.opts();
3880
3999
  const promptArgs = program.args;
@@ -0,0 +1,71 @@
1
+ export interface TeacherTrace {
2
+ id: string;
3
+ ts: number;
4
+ session_id?: string;
5
+ provider: string;
6
+ model: string;
7
+ system: string;
8
+ messages: Array<{
9
+ role: string;
10
+ content: string;
11
+ }>;
12
+ response: {
13
+ content: string;
14
+ thinking?: string;
15
+ tool_calls?: Array<{
16
+ id: string;
17
+ name: string;
18
+ arguments: Record<string, unknown>;
19
+ }>;
20
+ stop_reason?: string;
21
+ };
22
+ usage?: {
23
+ input_tokens: number;
24
+ output_tokens: number;
25
+ };
26
+ latency_ms?: number;
27
+ outcome?: {
28
+ verified: boolean;
29
+ signal: 'user_retry' | 'build_pass' | 'test_pass' | 'tool_error' | 'self_eval' | 'none';
30
+ score?: number;
31
+ };
32
+ tags?: string[];
33
+ }
34
+ export interface TeacherLoggerOptions {
35
+ enabled?: boolean;
36
+ dir?: string;
37
+ maxBytes?: number;
38
+ scrub?: boolean;
39
+ }
40
+ declare class TeacherLogger {
41
+ private enabled;
42
+ private dir;
43
+ private traceFile;
44
+ private maxBytes;
45
+ private scrub;
46
+ private pending;
47
+ constructor(opts?: TeacherLoggerOptions);
48
+ isEnabled(): boolean;
49
+ setEnabled(v: boolean): void;
50
+ /** Begin a trace — returns an ID to finalize later */
51
+ begin(input: {
52
+ sessionId?: string;
53
+ provider: string;
54
+ model: string;
55
+ system: string;
56
+ messages: Array<{
57
+ role: string;
58
+ content: string;
59
+ }>;
60
+ }): string;
61
+ /** Finalize a trace with the model response. No-op if id is empty/unknown. */
62
+ end(id: string, response: TeacherTrace['response'], usage?: TeacherTrace['usage'], outcome?: TeacherTrace['outcome']): void;
63
+ /** Tag an already-persisted trace with outcome later (e.g. after verifier runs). */
64
+ tagOutcome(traceId: string, outcome: TeacherTrace['outcome']): void;
65
+ private persist;
66
+ path(): string;
67
+ }
68
+ export declare function getTeacherLogger(): TeacherLogger;
69
+ export declare function setTeacherLogger(logger: TeacherLogger): void;
70
+ export {};
71
+ //# sourceMappingURL=teacher-logger.d.ts.map
@@ -0,0 +1,162 @@
1
+ // Teacher Logger — captures provider calls as (prompt, response, tools, outcome) pairs
2
+ // for later distillation / fine-tuning. Writes to ~/.kbot/teacher/traces.jsonl.
3
+ //
4
+ // One logger per process. Append-only, JSONL, crash-safe (flush per record).
5
+ // PII scrubber runs before persist.
6
+ import { appendFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { randomUUID } from 'node:crypto';
10
+ // ── PII / secret scrubber ────────────────────────────────────────────
11
+ // Patterns for API keys, tokens, common secrets
12
+ const SCRUB_PATTERNS = [
13
+ [/sk-ant-[A-Za-z0-9\-_]{20,}/g, 'sk-ant-<REDACTED>'],
14
+ [/sk-[A-Za-z0-9]{20,}/g, 'sk-<REDACTED>'],
15
+ [/ghp_[A-Za-z0-9]{30,}/g, 'ghp_<REDACTED>'],
16
+ [/github_pat_[A-Za-z0-9_]{30,}/g, 'github_pat_<REDACTED>'],
17
+ [/AIza[A-Za-z0-9\-_]{30,}/g, 'AIza<REDACTED>'],
18
+ [/xoxb-[A-Za-z0-9\-]{20,}/g, 'xoxb-<REDACTED>'],
19
+ [/eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/g, '<JWT_REDACTED>'],
20
+ [/[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g, '<EMAIL>'],
21
+ [/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '<IP>'],
22
+ [/\b[A-Fa-f0-9]{64}\b/g, '<HEX64>'],
23
+ ];
24
+ function scrubString(s) {
25
+ let out = s;
26
+ for (const [pat, repl] of SCRUB_PATTERNS)
27
+ out = out.replace(pat, repl);
28
+ // Home path scrub — keep structure but redact username
29
+ const home = homedir();
30
+ if (home && out.includes(home)) {
31
+ out = out.split(home).join('~');
32
+ }
33
+ return out;
34
+ }
35
+ function scrubTrace(t) {
36
+ return {
37
+ ...t,
38
+ system: scrubString(t.system),
39
+ messages: t.messages.map(m => ({ role: m.role, content: scrubString(m.content) })),
40
+ response: {
41
+ ...t.response,
42
+ content: scrubString(t.response.content),
43
+ thinking: t.response.thinking ? scrubString(t.response.thinking) : undefined,
44
+ tool_calls: t.response.tool_calls?.map(tc => ({
45
+ id: tc.id,
46
+ name: tc.name,
47
+ arguments: JSON.parse(scrubString(JSON.stringify(tc.arguments))),
48
+ })),
49
+ },
50
+ };
51
+ }
52
+ // ── Logger ───────────────────────────────────────────────────────────
53
+ class TeacherLogger {
54
+ enabled;
55
+ dir;
56
+ traceFile;
57
+ maxBytes;
58
+ scrub;
59
+ pending = new Map();
60
+ constructor(opts = {}) {
61
+ this.enabled = opts.enabled ?? envEnabled();
62
+ this.dir = opts.dir ?? join(homedir(), '.kbot', 'teacher');
63
+ this.traceFile = join(this.dir, 'traces.jsonl');
64
+ this.maxBytes = opts.maxBytes ?? 500 * 1024 * 1024; // 500MB cap; rotate beyond
65
+ this.scrub = opts.scrub ?? true;
66
+ if (this.enabled && !existsSync(this.dir)) {
67
+ try {
68
+ mkdirSync(this.dir, { recursive: true });
69
+ }
70
+ catch { /* ignore */ }
71
+ }
72
+ }
73
+ isEnabled() { return this.enabled; }
74
+ setEnabled(v) { this.enabled = v; }
75
+ /** Begin a trace — returns an ID to finalize later */
76
+ begin(input) {
77
+ if (!this.enabled)
78
+ return '';
79
+ const id = randomUUID();
80
+ this.pending.set(id, {
81
+ id,
82
+ session_id: input.sessionId,
83
+ provider: input.provider,
84
+ model: input.model,
85
+ system: input.system,
86
+ messages: input.messages,
87
+ started_at: Date.now(),
88
+ });
89
+ return id;
90
+ }
91
+ /** Finalize a trace with the model response. No-op if id is empty/unknown. */
92
+ end(id, response, usage, outcome) {
93
+ if (!this.enabled || !id)
94
+ return;
95
+ const p = this.pending.get(id);
96
+ if (!p)
97
+ return;
98
+ this.pending.delete(id);
99
+ const trace = {
100
+ id: p.id,
101
+ ts: Date.now(),
102
+ session_id: p.session_id,
103
+ provider: p.provider,
104
+ model: p.model,
105
+ system: p.system,
106
+ messages: p.messages,
107
+ response,
108
+ usage,
109
+ latency_ms: Date.now() - p.started_at,
110
+ outcome,
111
+ };
112
+ this.persist(trace);
113
+ }
114
+ /** Tag an already-persisted trace with outcome later (e.g. after verifier runs). */
115
+ tagOutcome(traceId, outcome) {
116
+ if (!this.enabled || !traceId)
117
+ return;
118
+ const outcomeFile = join(this.dir, 'outcomes.jsonl');
119
+ try {
120
+ appendFileSync(outcomeFile, JSON.stringify({ id: traceId, outcome, ts: Date.now() }) + '\n');
121
+ }
122
+ catch { /* swallow */ }
123
+ }
124
+ persist(trace) {
125
+ try {
126
+ // Size-based rotation
127
+ if (existsSync(this.traceFile)) {
128
+ const sz = statSync(this.traceFile).size;
129
+ if (sz > this.maxBytes) {
130
+ const rotated = join(this.dir, `traces.${Date.now()}.jsonl`);
131
+ try {
132
+ require('node:fs').renameSync(this.traceFile, rotated);
133
+ }
134
+ catch { /* ignore */ }
135
+ }
136
+ }
137
+ const t = this.scrub ? scrubTrace(trace) : trace;
138
+ appendFileSync(this.traceFile, JSON.stringify(t) + '\n');
139
+ }
140
+ catch {
141
+ // Never throw from logger — swallow and continue
142
+ }
143
+ }
144
+ path() { return this.traceFile; }
145
+ }
146
+ function envEnabled() {
147
+ const v = process.env.KBOT_TEACHER_LOG;
148
+ if (v == null)
149
+ return true; // default on — cost-free data collection
150
+ return v !== '0' && v.toLowerCase() !== 'false' && v !== '';
151
+ }
152
+ // ── Singleton ────────────────────────────────────────────────────────
153
+ let singleton = null;
154
+ export function getTeacherLogger() {
155
+ if (!singleton)
156
+ singleton = new TeacherLogger();
157
+ return singleton;
158
+ }
159
+ export function setTeacherLogger(logger) {
160
+ singleton = logger;
161
+ }
162
+ //# sourceMappingURL=teacher-logger.js.map