@kernel.chat/kbot 3.99.20 → 3.99.22
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 +11 -0
- package/dist/agent.js +23 -0
- package/dist/agents/producer.js +65 -23
- package/dist/auth.d.ts +2 -0
- package/dist/cli.js +7 -4
- package/dist/critic-gate.d.ts +29 -0
- package/dist/critic-gate.js +223 -0
- package/dist/critic-retrospect.d.ts +64 -0
- package/dist/critic-retrospect.js +279 -0
- package/dist/critic-taxonomy.d.ts +40 -0
- package/dist/critic-taxonomy.js +146 -0
- package/dist/growth.d.ts +37 -0
- package/dist/growth.js +272 -0
- package/dist/integrations/ableton.d.ts +30 -0
- package/dist/integrations/ableton.js +66 -0
- package/dist/integrations/kbot-control-client.d.ts +66 -0
- package/dist/integrations/kbot-control-client.js +224 -0
- package/dist/observer.d.ts +13 -0
- package/dist/observer.js +5 -1
- package/dist/planner/hierarchical/dag.d.ts +71 -0
- package/dist/planner/hierarchical/dag.js +97 -0
- package/dist/planner/hierarchical/persistence.d.ts +26 -0
- package/dist/planner/hierarchical/persistence.js +113 -0
- package/dist/planner/hierarchical/session-planner.d.ts +68 -0
- package/dist/planner/hierarchical/session-planner.js +141 -0
- package/dist/planner/hierarchical/types.d.ts +116 -0
- package/dist/planner/hierarchical/types.js +18 -0
- package/dist/tool-pipeline.d.ts +39 -1
- package/dist/tool-pipeline.js +109 -1
- package/dist/tools/ableton-listen.d.ts +2 -0
- package/dist/tools/ableton-listen.js +126 -0
- package/dist/tools/ableton.js +477 -12
- package/dist/tools/index.js +2 -0
- package/dist/tools/kbot-control.d.ts +2 -0
- package/dist/tools/kbot-control.js +63 -0
- package/package.json +1 -1
package/dist/growth.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// kbot growth — Make kbot's learning visible. Reads local learning artifacts
|
|
2
|
+
// (~/.kbot/skill-profile.json, confidence.json, evolution-state.json,
|
|
3
|
+
// observer/session.jsonl) and shows a week-over-week improvement report.
|
|
4
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
function pathsFor(dataDir) {
|
|
9
|
+
return {
|
|
10
|
+
skill: join(dataDir, 'skill-profile.json'),
|
|
11
|
+
confidence: join(dataDir, 'confidence.json'),
|
|
12
|
+
evolution: join(dataDir, 'evolution-state.json'),
|
|
13
|
+
observer: join(dataDir, 'observer', 'session.jsonl'),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function readJsonSafe(path) {
|
|
17
|
+
try {
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return null;
|
|
20
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function readObserver(observerPath) {
|
|
27
|
+
if (!existsSync(observerPath))
|
|
28
|
+
return [];
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(observerPath, 'utf8');
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const line of raw.split('\n')) {
|
|
33
|
+
if (!line.trim())
|
|
34
|
+
continue;
|
|
35
|
+
try {
|
|
36
|
+
out.push(JSON.parse(line));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// skip malformed line
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function inWindow(ts, start, end) {
|
|
49
|
+
const t = Date.parse(ts);
|
|
50
|
+
if (Number.isNaN(t))
|
|
51
|
+
return false;
|
|
52
|
+
return t >= start && t < end;
|
|
53
|
+
}
|
|
54
|
+
function computeWindow(events, confidence, start, end) {
|
|
55
|
+
const sessions = new Set();
|
|
56
|
+
const toolCounts = {};
|
|
57
|
+
const agentCounts = {};
|
|
58
|
+
let toolCalls = 0;
|
|
59
|
+
let errors = 0;
|
|
60
|
+
for (const e of events) {
|
|
61
|
+
if (!inWindow(e.ts, start, end))
|
|
62
|
+
continue;
|
|
63
|
+
toolCalls++;
|
|
64
|
+
if (e.error === true)
|
|
65
|
+
errors++;
|
|
66
|
+
if (e.session)
|
|
67
|
+
sessions.add(e.session);
|
|
68
|
+
toolCounts[e.tool] = (toolCounts[e.tool] ?? 0) + 1;
|
|
69
|
+
const agent = typeof e.args?.['agent'] === 'string' ? e.args['agent'] : null;
|
|
70
|
+
if (agent)
|
|
71
|
+
agentCounts[agent] = (agentCounts[agent] ?? 0) + 1;
|
|
72
|
+
}
|
|
73
|
+
const successRate = toolCalls > 0 ? (toolCalls - errors) / toolCalls : 0;
|
|
74
|
+
// Routing accuracy: |predicted - actual| <= 0.2 counts as accurate
|
|
75
|
+
let routingSamples = 0;
|
|
76
|
+
let routingHits = 0;
|
|
77
|
+
for (const c of confidence) {
|
|
78
|
+
if (!inWindow(c.timestamp, start, end))
|
|
79
|
+
continue;
|
|
80
|
+
routingSamples++;
|
|
81
|
+
if (Math.abs(c.predicted - c.actual) <= 0.2)
|
|
82
|
+
routingHits++;
|
|
83
|
+
}
|
|
84
|
+
const routingAccuracy = routingSamples > 0 ? routingHits / routingSamples : 0;
|
|
85
|
+
return {
|
|
86
|
+
sessions: sessions.size,
|
|
87
|
+
toolCalls,
|
|
88
|
+
errors,
|
|
89
|
+
successRate,
|
|
90
|
+
routingAccuracy,
|
|
91
|
+
toolCounts,
|
|
92
|
+
agentCounts,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function topToolDeltas(current, prior, limit = 5) {
|
|
96
|
+
const tools = new Set([...Object.keys(current), ...Object.keys(prior)]);
|
|
97
|
+
const rows = [];
|
|
98
|
+
for (const t of tools) {
|
|
99
|
+
const c = current[t] ?? 0;
|
|
100
|
+
const p = prior[t] ?? 0;
|
|
101
|
+
if (c + p === 0)
|
|
102
|
+
continue;
|
|
103
|
+
rows.push({ tool: t, current: c, prior: p, delta: c - p });
|
|
104
|
+
}
|
|
105
|
+
rows.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
106
|
+
return rows.slice(0, limit);
|
|
107
|
+
}
|
|
108
|
+
function perAgentRouting(confidence, start, end) {
|
|
109
|
+
const agg = {};
|
|
110
|
+
for (const c of confidence) {
|
|
111
|
+
if (!inWindow(c.timestamp, start, end))
|
|
112
|
+
continue;
|
|
113
|
+
const domain = c.domain || 'general';
|
|
114
|
+
const bucket = (agg[domain] ??= { hits: 0, total: 0 });
|
|
115
|
+
bucket.total++;
|
|
116
|
+
if (Math.abs(c.predicted - c.actual) <= 0.2)
|
|
117
|
+
bucket.hits++;
|
|
118
|
+
}
|
|
119
|
+
return Object.entries(agg)
|
|
120
|
+
.map(([agent, v]) => ({ agent, accuracy: v.total > 0 ? v.hits / v.total : 0, samples: v.total }))
|
|
121
|
+
.sort((a, b) => b.samples - a.samples)
|
|
122
|
+
.slice(0, 8);
|
|
123
|
+
}
|
|
124
|
+
function blendScore(successRate, routingAccuracy) {
|
|
125
|
+
// 60% tool success, 40% routing. Fall back to either if one is missing.
|
|
126
|
+
if (successRate === 0 && routingAccuracy === 0)
|
|
127
|
+
return 0;
|
|
128
|
+
if (routingAccuracy === 0)
|
|
129
|
+
return successRate;
|
|
130
|
+
if (successRate === 0)
|
|
131
|
+
return routingAccuracy;
|
|
132
|
+
return successRate * 0.6 + routingAccuracy * 0.4;
|
|
133
|
+
}
|
|
134
|
+
const pct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
135
|
+
function bar(n, width = 20) {
|
|
136
|
+
const filled = Math.max(0, Math.min(width, Math.round(n * width)));
|
|
137
|
+
return chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(width - filled));
|
|
138
|
+
}
|
|
139
|
+
function renderNotEnoughData() {
|
|
140
|
+
return [
|
|
141
|
+
'',
|
|
142
|
+
` ${chalk.bold('kbot growth')}`,
|
|
143
|
+
` ${chalk.dim('─'.repeat(40))}`,
|
|
144
|
+
'',
|
|
145
|
+
` ${chalk.yellow('Not enough data yet.')}`,
|
|
146
|
+
'',
|
|
147
|
+
` kbot learns from your sessions. To seed it:`,
|
|
148
|
+
` • Run ${chalk.bold('kbot')} on real work for a few days`,
|
|
149
|
+
` • Let the observer log tool calls to ${chalk.dim('~/.kbot/observer/session.jsonl')}`,
|
|
150
|
+
` • Re-run ${chalk.bold('kbot growth')} after ~3 sessions`,
|
|
151
|
+
'',
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
|
154
|
+
function renderPretty(result) {
|
|
155
|
+
const s = result.summary;
|
|
156
|
+
const lines = [];
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push(` ${chalk.bold('kbot growth')} ${chalk.dim(`— last ${s.days} days vs prior ${s.days}`)}`);
|
|
159
|
+
lines.push(` ${chalk.dim('─'.repeat(60))}`);
|
|
160
|
+
lines.push('');
|
|
161
|
+
const headlineColor = s.betterPct >= 0 ? chalk.green : chalk.red;
|
|
162
|
+
const sign = s.betterPct >= 0 ? '+' : '';
|
|
163
|
+
lines.push(` ${chalk.bold('kbot is')} ${headlineColor.bold(`${sign}${s.betterPct.toFixed(1)}%`)} ${chalk.bold('better at your tasks this week')}`);
|
|
164
|
+
lines.push('');
|
|
165
|
+
// Core metrics table
|
|
166
|
+
lines.push(` ${chalk.bold('Metrics')}`);
|
|
167
|
+
lines.push(` ${chalk.dim('─'.repeat(60))}`);
|
|
168
|
+
for (const m of result.metrics) {
|
|
169
|
+
const arrow = m.delta > 0 ? chalk.green('▲') : m.delta < 0 ? chalk.red('▼') : chalk.dim('·');
|
|
170
|
+
const isRate = m.label.includes('rate') || m.label.includes('accuracy');
|
|
171
|
+
const cur = isRate ? pct(m.current) : String(Math.round(m.current));
|
|
172
|
+
const prev = isRate ? pct(m.prior) : String(Math.round(m.prior));
|
|
173
|
+
const deltaStr = isRate
|
|
174
|
+
? `${m.delta >= 0 ? '+' : ''}${(m.delta * 100).toFixed(1)}pp`
|
|
175
|
+
: `${m.delta >= 0 ? '+' : ''}${Math.round(m.delta)}`;
|
|
176
|
+
lines.push(` ${arrow} ${m.label.padEnd(24)} ${cur.padStart(8)} ${chalk.dim(`prev ${prev}`)} ${chalk.bold(deltaStr)}`);
|
|
177
|
+
}
|
|
178
|
+
lines.push('');
|
|
179
|
+
// Tool deltas
|
|
180
|
+
if (result.deltas.length > 0) {
|
|
181
|
+
lines.push(` ${chalk.bold('Top tools by usage delta')}`);
|
|
182
|
+
lines.push(` ${chalk.dim('─'.repeat(60))}`);
|
|
183
|
+
for (const d of result.deltas) {
|
|
184
|
+
const arrow = d.delta > 0 ? chalk.green('▲') : d.delta < 0 ? chalk.red('▼') : chalk.dim('·');
|
|
185
|
+
const deltaStr = `${d.delta >= 0 ? '+' : ''}${d.delta}`;
|
|
186
|
+
lines.push(` ${arrow} ${d.tool.padEnd(30)} ${String(d.current).padStart(5)} ${chalk.dim(`prev ${d.prior}`)} ${chalk.bold(deltaStr)}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push('');
|
|
189
|
+
}
|
|
190
|
+
// Per-agent routing
|
|
191
|
+
if (result.agents.length > 0) {
|
|
192
|
+
lines.push(` ${chalk.bold('Per-domain routing accuracy')}`);
|
|
193
|
+
lines.push(` ${chalk.dim('─'.repeat(60))}`);
|
|
194
|
+
for (const a of result.agents) {
|
|
195
|
+
lines.push(` ${a.agent.padEnd(16)} ${bar(a.accuracy)} ${pct(a.accuracy).padStart(6)} ${chalk.dim(`n=${a.samples}`)}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
}
|
|
199
|
+
lines.push(` ${chalk.dim(`New patterns learned: ${s.newPatterns}`)}`);
|
|
200
|
+
lines.push('');
|
|
201
|
+
return lines.join('\n');
|
|
202
|
+
}
|
|
203
|
+
export function runGrowth(opts = {}) {
|
|
204
|
+
const days = Math.max(1, Math.floor(opts.days ?? 7));
|
|
205
|
+
const now = opts.now ?? Date.now();
|
|
206
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
207
|
+
const currentStart = now - days * dayMs;
|
|
208
|
+
const priorStart = now - 2 * days * dayMs;
|
|
209
|
+
const paths = pathsFor(opts.dataDir ?? join(homedir(), '.kbot'));
|
|
210
|
+
const events = readObserver(paths.observer);
|
|
211
|
+
const confidenceRaw = readJsonSafe(paths.confidence);
|
|
212
|
+
const confidence = confidenceRaw?.entries ?? [];
|
|
213
|
+
const skillRaw = readJsonSafe(paths.skill);
|
|
214
|
+
const skills = skillRaw?.skills ?? {};
|
|
215
|
+
// evolution state is read to surface future signals; currently used only as a
|
|
216
|
+
// signal that the file exists and kbot has evolved behaviours.
|
|
217
|
+
const evolution = readJsonSafe(paths.evolution);
|
|
218
|
+
if (events.length === 0 && confidence.length === 0 && Object.keys(skills).length === 0) {
|
|
219
|
+
if (opts.json) {
|
|
220
|
+
process.stdout.write(JSON.stringify({ summary: null, metrics: [], deltas: [] }, null, 2) + '\n');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
process.stdout.write(renderNotEnoughData() + '\n');
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const cur = computeWindow(events, confidence, currentStart, now);
|
|
228
|
+
const prior = computeWindow(events, confidence, priorStart, currentStart);
|
|
229
|
+
// "Better by N%": compare blended score now vs prior, as a relative lift.
|
|
230
|
+
const curBlend = blendScore(cur.successRate, cur.routingAccuracy);
|
|
231
|
+
const priBlend = blendScore(prior.successRate, prior.routingAccuracy);
|
|
232
|
+
const betterPct = priBlend > 0 ? ((curBlend - priBlend) / priBlend) * 100 : curBlend > 0 ? 100 : 0;
|
|
233
|
+
// New patterns learned: skills whose lastAttempt is in the current window.
|
|
234
|
+
let newPatterns = 0;
|
|
235
|
+
for (const entry of Object.values(skills)) {
|
|
236
|
+
if (entry.lastAttempt && inWindow(entry.lastAttempt, currentStart, now))
|
|
237
|
+
newPatterns++;
|
|
238
|
+
}
|
|
239
|
+
// Plus unique new domains appearing in confidence in current window but not prior.
|
|
240
|
+
const priorDomains = new Set(confidence.filter((c) => inWindow(c.timestamp, priorStart, currentStart)).map((c) => c.domain));
|
|
241
|
+
const newDomains = new Set(confidence
|
|
242
|
+
.filter((c) => inWindow(c.timestamp, currentStart, now))
|
|
243
|
+
.map((c) => c.domain)
|
|
244
|
+
.filter((d) => !priorDomains.has(d)));
|
|
245
|
+
newPatterns += newDomains.size;
|
|
246
|
+
void evolution; // evolution data reserved for future deltas
|
|
247
|
+
const summary = {
|
|
248
|
+
betterPct,
|
|
249
|
+
days,
|
|
250
|
+
sessions: cur.sessions,
|
|
251
|
+
toolCalls: cur.toolCalls,
|
|
252
|
+
successRate: cur.successRate,
|
|
253
|
+
routingAccuracy: cur.routingAccuracy,
|
|
254
|
+
newPatterns,
|
|
255
|
+
};
|
|
256
|
+
const metrics = [
|
|
257
|
+
{ label: 'sessions', current: cur.sessions, prior: prior.sessions, delta: cur.sessions - prior.sessions },
|
|
258
|
+
{ label: 'tool calls', current: cur.toolCalls, prior: prior.toolCalls, delta: cur.toolCalls - prior.toolCalls },
|
|
259
|
+
{ label: 'tool success rate', current: cur.successRate, prior: prior.successRate, delta: cur.successRate - prior.successRate },
|
|
260
|
+
{ label: 'routing accuracy', current: cur.routingAccuracy, prior: prior.routingAccuracy, delta: cur.routingAccuracy - prior.routingAccuracy },
|
|
261
|
+
];
|
|
262
|
+
const deltas = topToolDeltas(cur.toolCounts, prior.toolCounts, 5);
|
|
263
|
+
const agents = perAgentRouting(confidence, currentStart, now);
|
|
264
|
+
const result = { summary, metrics, deltas, agents };
|
|
265
|
+
if (opts.json) {
|
|
266
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
process.stdout.write(renderPretty(result) + '\n');
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=growth.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ableton.ts — unified Ableton client helper.
|
|
3
|
+
*
|
|
4
|
+
* Single entry point for all kbot Ableton tools. Tries kbot-control.amxd
|
|
5
|
+
* (TCP:9000) first; falls back to AbletonOSC (UDP:11000) if the device
|
|
6
|
+
* isn't loaded. Over time, as kbot-control's dispatcher covers the full
|
|
7
|
+
* OSC surface, the OSC fallback goes away.
|
|
8
|
+
*
|
|
9
|
+
* Tool code should import from here, not from kbot-control-client.ts
|
|
10
|
+
* or ableton-osc.ts directly.
|
|
11
|
+
*/
|
|
12
|
+
import { ensureAbleton, type OscArg } from './ableton-osc.js';
|
|
13
|
+
/**
|
|
14
|
+
* Call a kbot-control method if the device is loaded. Returns undefined
|
|
15
|
+
* if unavailable — caller should fall back to OSC.
|
|
16
|
+
*/
|
|
17
|
+
export declare function tryKc<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Route an OSC operation through kbot-control if possible, else AbletonOSC.
|
|
20
|
+
* The two functions are called with the same args — whichever resolves wins.
|
|
21
|
+
*
|
|
22
|
+
* Use when you have parallel implementations. Example:
|
|
23
|
+
* await routed(
|
|
24
|
+
* () => tryKc('song.tempo', { value: 120 }),
|
|
25
|
+
* async () => { (await ensureAbleton()).send('/live/song/set/tempo', 120); return 120 },
|
|
26
|
+
* )
|
|
27
|
+
*/
|
|
28
|
+
export declare function routed<T>(kc: () => Promise<T | undefined>, osc: () => Promise<T>): Promise<T>;
|
|
29
|
+
export { ensureAbleton, type OscArg };
|
|
30
|
+
//# sourceMappingURL=ableton.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ableton.ts — unified Ableton client helper.
|
|
3
|
+
*
|
|
4
|
+
* Single entry point for all kbot Ableton tools. Tries kbot-control.amxd
|
|
5
|
+
* (TCP:9000) first; falls back to AbletonOSC (UDP:11000) if the device
|
|
6
|
+
* isn't loaded. Over time, as kbot-control's dispatcher covers the full
|
|
7
|
+
* OSC surface, the OSC fallback goes away.
|
|
8
|
+
*
|
|
9
|
+
* Tool code should import from here, not from kbot-control-client.ts
|
|
10
|
+
* or ableton-osc.ts directly.
|
|
11
|
+
*/
|
|
12
|
+
import { KbotControlClient } from './kbot-control-client.js';
|
|
13
|
+
import { ensureAbleton } from './ableton-osc.js';
|
|
14
|
+
let kbotControlAvailable = null;
|
|
15
|
+
let lastProbeAt = 0;
|
|
16
|
+
const PROBE_CACHE_MS = 5_000;
|
|
17
|
+
async function probeKbotControl() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (kbotControlAvailable !== null && now - lastProbeAt < PROBE_CACHE_MS) {
|
|
20
|
+
return kbotControlAvailable;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await KbotControlClient.get().connect();
|
|
24
|
+
kbotControlAvailable = KbotControlClient.get().isConnected;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
kbotControlAvailable = false;
|
|
28
|
+
}
|
|
29
|
+
lastProbeAt = now;
|
|
30
|
+
return kbotControlAvailable;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Call a kbot-control method if the device is loaded. Returns undefined
|
|
34
|
+
* if unavailable — caller should fall back to OSC.
|
|
35
|
+
*/
|
|
36
|
+
export async function tryKc(method, params) {
|
|
37
|
+
if (!(await probeKbotControl()))
|
|
38
|
+
return undefined;
|
|
39
|
+
try {
|
|
40
|
+
return await KbotControlClient.get().call(method, params);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Method might not be implemented yet in the dispatcher;
|
|
44
|
+
// let the caller fall through to OSC.
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Route an OSC operation through kbot-control if possible, else AbletonOSC.
|
|
50
|
+
* The two functions are called with the same args — whichever resolves wins.
|
|
51
|
+
*
|
|
52
|
+
* Use when you have parallel implementations. Example:
|
|
53
|
+
* await routed(
|
|
54
|
+
* () => tryKc('song.tempo', { value: 120 }),
|
|
55
|
+
* async () => { (await ensureAbleton()).send('/live/song/set/tempo', 120); return 120 },
|
|
56
|
+
* )
|
|
57
|
+
*/
|
|
58
|
+
export async function routed(kc, osc) {
|
|
59
|
+
const v = await kc();
|
|
60
|
+
if (v !== undefined)
|
|
61
|
+
return v;
|
|
62
|
+
return osc();
|
|
63
|
+
}
|
|
64
|
+
// Re-export the legacy OSC escape hatch for tools that haven't migrated yet.
|
|
65
|
+
export { ensureAbleton };
|
|
66
|
+
//# sourceMappingURL=ableton.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kbot-control-client.ts — TCP client for kbot-control.amxd
|
|
3
|
+
*
|
|
4
|
+
* Singleton client that connects to the kbot-control Max for Live device
|
|
5
|
+
* at 127.0.0.1:9000. Newline-delimited JSON-RPC 2.0 over plain TCP.
|
|
6
|
+
* Zero npm dependencies — uses node:net only.
|
|
7
|
+
*
|
|
8
|
+
* Supersedes:
|
|
9
|
+
* - ableton-osc.ts (UDP 11000, AbletonOSC Remote Script)
|
|
10
|
+
* - ableton-bridge.ts (TCP 9001, AbletonBridge)
|
|
11
|
+
* - ableton-m4l.ts (TCP 9999, kbot-bridge.amxd)
|
|
12
|
+
*
|
|
13
|
+
* Fallback order when this client can't connect:
|
|
14
|
+
* 1. kbot-control.amxd (this)
|
|
15
|
+
* 2. ableton-osc.ts (legacy)
|
|
16
|
+
* 3. Claude computer-use MCP (last resort)
|
|
17
|
+
*/
|
|
18
|
+
export interface RpcRequest {
|
|
19
|
+
jsonrpc: '2.0';
|
|
20
|
+
id: number;
|
|
21
|
+
method: string;
|
|
22
|
+
params?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface RpcResponse<T = unknown> {
|
|
25
|
+
jsonrpc: '2.0';
|
|
26
|
+
id: number | null;
|
|
27
|
+
result?: T;
|
|
28
|
+
error?: {
|
|
29
|
+
code: number;
|
|
30
|
+
message: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export type Listener = (value: unknown) => void;
|
|
34
|
+
export declare class KbotControlClient {
|
|
35
|
+
private static instance;
|
|
36
|
+
private socket;
|
|
37
|
+
private connected;
|
|
38
|
+
private buffer;
|
|
39
|
+
private pending;
|
|
40
|
+
private listeners;
|
|
41
|
+
private nextId;
|
|
42
|
+
private connectAttempt;
|
|
43
|
+
static HOST: string;
|
|
44
|
+
static PORT: number;
|
|
45
|
+
static TIMEOUT: number;
|
|
46
|
+
static CONNECT_TIMEOUT: number;
|
|
47
|
+
private constructor();
|
|
48
|
+
static get(): KbotControlClient;
|
|
49
|
+
connect(): Promise<void>;
|
|
50
|
+
private handleData;
|
|
51
|
+
private handleMessage;
|
|
52
|
+
call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
53
|
+
subscribe(path: string, fn: Listener): Promise<void>;
|
|
54
|
+
unsubscribe(path: string, fn: Listener): Promise<void>;
|
|
55
|
+
private pollers;
|
|
56
|
+
private startPolling;
|
|
57
|
+
private stopPolling;
|
|
58
|
+
disconnect(): void;
|
|
59
|
+
get isConnected(): boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Convenience: connect + call + return result.
|
|
63
|
+
* Throws if kbot-control.amxd isn't loaded in Ableton.
|
|
64
|
+
*/
|
|
65
|
+
export declare function kc<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
66
|
+
//# sourceMappingURL=kbot-control-client.d.ts.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kbot-control-client.ts — TCP client for kbot-control.amxd
|
|
3
|
+
*
|
|
4
|
+
* Singleton client that connects to the kbot-control Max for Live device
|
|
5
|
+
* at 127.0.0.1:9000. Newline-delimited JSON-RPC 2.0 over plain TCP.
|
|
6
|
+
* Zero npm dependencies — uses node:net only.
|
|
7
|
+
*
|
|
8
|
+
* Supersedes:
|
|
9
|
+
* - ableton-osc.ts (UDP 11000, AbletonOSC Remote Script)
|
|
10
|
+
* - ableton-bridge.ts (TCP 9001, AbletonBridge)
|
|
11
|
+
* - ableton-m4l.ts (TCP 9999, kbot-bridge.amxd)
|
|
12
|
+
*
|
|
13
|
+
* Fallback order when this client can't connect:
|
|
14
|
+
* 1. kbot-control.amxd (this)
|
|
15
|
+
* 2. ableton-osc.ts (legacy)
|
|
16
|
+
* 3. Claude computer-use MCP (last resort)
|
|
17
|
+
*/
|
|
18
|
+
import * as net from 'node:net';
|
|
19
|
+
export class KbotControlClient {
|
|
20
|
+
static instance = null;
|
|
21
|
+
socket = null;
|
|
22
|
+
connected = false;
|
|
23
|
+
buffer = '';
|
|
24
|
+
pending = new Map();
|
|
25
|
+
listeners = new Map();
|
|
26
|
+
nextId = 1;
|
|
27
|
+
connectAttempt = null;
|
|
28
|
+
static HOST = '127.0.0.1';
|
|
29
|
+
static PORT = 9000;
|
|
30
|
+
static TIMEOUT = 15_000;
|
|
31
|
+
static CONNECT_TIMEOUT = 3_000;
|
|
32
|
+
constructor() { }
|
|
33
|
+
static get() {
|
|
34
|
+
if (!this.instance)
|
|
35
|
+
this.instance = new KbotControlClient();
|
|
36
|
+
return this.instance;
|
|
37
|
+
}
|
|
38
|
+
async connect() {
|
|
39
|
+
if (this.connected)
|
|
40
|
+
return;
|
|
41
|
+
if (this.connectAttempt)
|
|
42
|
+
return this.connectAttempt;
|
|
43
|
+
this.connectAttempt = new Promise((resolve, reject) => {
|
|
44
|
+
const sock = new net.Socket();
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
sock.destroy();
|
|
47
|
+
reject(new Error(`kbot-control: connect timeout (${KbotControlClient.CONNECT_TIMEOUT}ms)`));
|
|
48
|
+
}, KbotControlClient.CONNECT_TIMEOUT);
|
|
49
|
+
sock.connect(KbotControlClient.PORT, KbotControlClient.HOST, () => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
this.socket = sock;
|
|
52
|
+
this.connected = true;
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
sock.on('data', (chunk) => this.handleData(chunk.toString()));
|
|
56
|
+
sock.on('close', () => {
|
|
57
|
+
this.connected = false;
|
|
58
|
+
this.socket = null;
|
|
59
|
+
for (const [, p] of this.pending) {
|
|
60
|
+
clearTimeout(p.timer);
|
|
61
|
+
p.reject(new Error('kbot-control: connection closed'));
|
|
62
|
+
}
|
|
63
|
+
this.pending.clear();
|
|
64
|
+
});
|
|
65
|
+
sock.on('error', (e) => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
this.connected = false;
|
|
68
|
+
reject(new Error(`kbot-control: ${e.message} — is kbot-control.amxd loaded in Ableton on a track?`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
await this.connectAttempt;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
this.connectAttempt = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
handleData(chunk) {
|
|
79
|
+
this.buffer += chunk;
|
|
80
|
+
const lines = this.buffer.split('\n');
|
|
81
|
+
this.buffer = lines.pop() || '';
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed)
|
|
85
|
+
continue;
|
|
86
|
+
this.handleMessage(trimmed);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
handleMessage(raw) {
|
|
90
|
+
let msg;
|
|
91
|
+
try {
|
|
92
|
+
msg = JSON.parse(raw);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Notifications (listener events) — no id
|
|
98
|
+
if ('method' in msg && msg.method === 'notify') {
|
|
99
|
+
const { path, value } = msg.params || {};
|
|
100
|
+
if (path) {
|
|
101
|
+
const set = this.listeners.get(path);
|
|
102
|
+
if (set)
|
|
103
|
+
for (const fn of set)
|
|
104
|
+
fn(value);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Server hello greeting
|
|
109
|
+
if ('method' in msg && msg.method === 'hello')
|
|
110
|
+
return;
|
|
111
|
+
const response = msg;
|
|
112
|
+
if (response.id == null)
|
|
113
|
+
return;
|
|
114
|
+
const p = this.pending.get(response.id);
|
|
115
|
+
if (!p)
|
|
116
|
+
return;
|
|
117
|
+
this.pending.delete(response.id);
|
|
118
|
+
clearTimeout(p.timer);
|
|
119
|
+
if (response.error)
|
|
120
|
+
p.reject(new Error(`[${response.error.code}] ${response.error.message}`));
|
|
121
|
+
else
|
|
122
|
+
p.resolve(response.result);
|
|
123
|
+
}
|
|
124
|
+
async call(method, params) {
|
|
125
|
+
await this.connect();
|
|
126
|
+
if (!this.socket)
|
|
127
|
+
throw new Error('kbot-control: not connected');
|
|
128
|
+
const id = this.nextId++;
|
|
129
|
+
const req = { jsonrpc: '2.0', id, method, params };
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const timer = setTimeout(() => {
|
|
132
|
+
this.pending.delete(id);
|
|
133
|
+
reject(new Error(`kbot-control: timeout on ${method} (${KbotControlClient.TIMEOUT}ms)`));
|
|
134
|
+
}, KbotControlClient.TIMEOUT);
|
|
135
|
+
this.pending.set(id, {
|
|
136
|
+
resolve: (r) => resolve(r),
|
|
137
|
+
reject,
|
|
138
|
+
timer,
|
|
139
|
+
});
|
|
140
|
+
this.socket.write(JSON.stringify(req) + '\n');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async subscribe(path, fn) {
|
|
144
|
+
let set = this.listeners.get(path);
|
|
145
|
+
if (!set) {
|
|
146
|
+
set = new Set();
|
|
147
|
+
this.listeners.set(path, set);
|
|
148
|
+
await this.call('listen.subscribe', { path });
|
|
149
|
+
// Start a polling loop as a fallback until the outlet-based push works.
|
|
150
|
+
// This also catches events even when the push path breaks.
|
|
151
|
+
this.startPolling(path);
|
|
152
|
+
}
|
|
153
|
+
set.add(fn);
|
|
154
|
+
}
|
|
155
|
+
async unsubscribe(path, fn) {
|
|
156
|
+
const set = this.listeners.get(path);
|
|
157
|
+
if (!set)
|
|
158
|
+
return;
|
|
159
|
+
set.delete(fn);
|
|
160
|
+
if (set.size === 0) {
|
|
161
|
+
this.listeners.delete(path);
|
|
162
|
+
this.stopPolling(path);
|
|
163
|
+
try {
|
|
164
|
+
await this.call('listen.unsubscribe', { path });
|
|
165
|
+
}
|
|
166
|
+
catch { /* ignore */ }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
pollers = new Map();
|
|
170
|
+
startPolling(path, intervalMs = 150) {
|
|
171
|
+
if (this.pollers.has(path))
|
|
172
|
+
return;
|
|
173
|
+
const state = { timer: null, since: 0 };
|
|
174
|
+
state.timer = setInterval(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const r = await this.call('listen.poll', { path, since: state.since });
|
|
177
|
+
if (r && r.events && r.events.length > 0) {
|
|
178
|
+
state.since = r.latest_seq;
|
|
179
|
+
const set = this.listeners.get(path);
|
|
180
|
+
if (set) {
|
|
181
|
+
for (const ev of r.events) {
|
|
182
|
+
// LiveAPI often reports values as [propertyName, value]; unwrap.
|
|
183
|
+
let v = ev.value;
|
|
184
|
+
if (Array.isArray(v) && v.length === 2 && typeof v[0] === 'string')
|
|
185
|
+
v = v[1];
|
|
186
|
+
for (const fn of set)
|
|
187
|
+
fn(v);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (r && typeof r.latest_seq === 'number') {
|
|
192
|
+
state.since = r.latest_seq;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch { /* ignore transient errors */ }
|
|
196
|
+
}, intervalMs);
|
|
197
|
+
this.pollers.set(path, state);
|
|
198
|
+
}
|
|
199
|
+
stopPolling(path) {
|
|
200
|
+
const s = this.pollers.get(path);
|
|
201
|
+
if (s) {
|
|
202
|
+
clearInterval(s.timer);
|
|
203
|
+
this.pollers.delete(path);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
disconnect() {
|
|
207
|
+
for (const [, s] of this.pollers)
|
|
208
|
+
clearInterval(s.timer);
|
|
209
|
+
this.pollers.clear();
|
|
210
|
+
if (this.socket)
|
|
211
|
+
this.socket.destroy();
|
|
212
|
+
this.socket = null;
|
|
213
|
+
this.connected = false;
|
|
214
|
+
}
|
|
215
|
+
get isConnected() { return this.connected; }
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Convenience: connect + call + return result.
|
|
219
|
+
* Throws if kbot-control.amxd isn't loaded in Ableton.
|
|
220
|
+
*/
|
|
221
|
+
export async function kc(method, params) {
|
|
222
|
+
return KbotControlClient.get().call(method, params);
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=kbot-control-client.js.map
|
package/dist/observer.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome classification for tool calls.
|
|
3
|
+
* Required for training an action-token model — cannot be backfilled from logs.
|
|
4
|
+
*/
|
|
5
|
+
export type ToolOutcome = 'success' | 'error' | 'timeout' | 'empty' | 'large';
|
|
1
6
|
export interface ObservedToolCall {
|
|
2
7
|
ts: string;
|
|
3
8
|
tool: string;
|
|
@@ -5,6 +10,14 @@ export interface ObservedToolCall {
|
|
|
5
10
|
result_length?: number;
|
|
6
11
|
session?: string;
|
|
7
12
|
error?: boolean;
|
|
13
|
+
/** Schema version. Absent = v1 (legacy). 2 = includes durationMs/outcome/resultSize. */
|
|
14
|
+
schema?: number;
|
|
15
|
+
/** Wall-clock duration of tool execution in milliseconds. (schema v2+) */
|
|
16
|
+
durationMs?: number;
|
|
17
|
+
/** Outcome classification for training. (schema v2+) */
|
|
18
|
+
outcome?: ToolOutcome;
|
|
19
|
+
/** Bytes of serialized result (Buffer.byteLength of result string). (schema v2+) */
|
|
20
|
+
resultSize?: number;
|
|
8
21
|
}
|
|
9
22
|
export interface ObserverStats {
|
|
10
23
|
totalObserved: number;
|