@kernel.chat/kbot 3.97.4 → 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/dist/agent.js +21 -0
- package/dist/cli.js +119 -0
- 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 +1 -1
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
|
|
@@ -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
|