@kernel.chat/kbot 3.51.0 → 3.54.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.
Files changed (121) hide show
  1. package/README.md +43 -9
  2. package/dist/agent-protocol.test.d.ts +2 -0
  3. package/dist/agent-protocol.test.d.ts.map +1 -0
  4. package/dist/agent-protocol.test.js +730 -0
  5. package/dist/agent-protocol.test.js.map +1 -0
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +34 -10
  8. package/dist/agent.js.map +1 -1
  9. package/dist/agents/replit.js +1 -1
  10. package/dist/auth.js +3 -3
  11. package/dist/auth.js.map +1 -1
  12. package/dist/behaviour.d.ts +30 -0
  13. package/dist/behaviour.d.ts.map +1 -0
  14. package/dist/behaviour.js +191 -0
  15. package/dist/behaviour.js.map +1 -0
  16. package/dist/bench.d.ts +64 -0
  17. package/dist/bench.d.ts.map +1 -0
  18. package/dist/bench.js +973 -0
  19. package/dist/bench.js.map +1 -0
  20. package/dist/bootstrap.js +1 -1
  21. package/dist/bootstrap.js.map +1 -1
  22. package/dist/cli.js +144 -29
  23. package/dist/cli.js.map +1 -1
  24. package/dist/cloud-agent.d.ts +77 -0
  25. package/dist/cloud-agent.d.ts.map +1 -0
  26. package/dist/cloud-agent.js +743 -0
  27. package/dist/cloud-agent.js.map +1 -0
  28. package/dist/context.test.d.ts +2 -0
  29. package/dist/context.test.d.ts.map +1 -0
  30. package/dist/context.test.js +561 -0
  31. package/dist/context.test.js.map +1 -0
  32. package/dist/evolution.d.ts.map +1 -1
  33. package/dist/evolution.js +4 -1
  34. package/dist/evolution.js.map +1 -1
  35. package/dist/github-release.d.ts +61 -0
  36. package/dist/github-release.d.ts.map +1 -0
  37. package/dist/github-release.js +451 -0
  38. package/dist/github-release.js.map +1 -0
  39. package/dist/graph-memory.test.d.ts +2 -0
  40. package/dist/graph-memory.test.d.ts.map +1 -0
  41. package/dist/graph-memory.test.js +946 -0
  42. package/dist/graph-memory.test.js.map +1 -0
  43. package/dist/init-science.d.ts +43 -0
  44. package/dist/init-science.d.ts.map +1 -0
  45. package/dist/init-science.js +477 -0
  46. package/dist/init-science.js.map +1 -0
  47. package/dist/integrations/ableton-m4l.d.ts +124 -0
  48. package/dist/integrations/ableton-m4l.d.ts.map +1 -0
  49. package/dist/integrations/ableton-m4l.js +338 -0
  50. package/dist/integrations/ableton-m4l.js.map +1 -0
  51. package/dist/integrations/ableton-osc.d.ts.map +1 -1
  52. package/dist/integrations/ableton-osc.js +6 -2
  53. package/dist/integrations/ableton-osc.js.map +1 -1
  54. package/dist/lab.d.ts +45 -0
  55. package/dist/lab.d.ts.map +1 -0
  56. package/dist/lab.js +1020 -0
  57. package/dist/lab.js.map +1 -0
  58. package/dist/lsp-deep.d.ts +101 -0
  59. package/dist/lsp-deep.d.ts.map +1 -0
  60. package/dist/lsp-deep.js +689 -0
  61. package/dist/lsp-deep.js.map +1 -0
  62. package/dist/memory.test.d.ts +2 -0
  63. package/dist/memory.test.d.ts.map +1 -0
  64. package/dist/memory.test.js +369 -0
  65. package/dist/memory.test.js.map +1 -0
  66. package/dist/multi-session.d.ts +164 -0
  67. package/dist/multi-session.d.ts.map +1 -0
  68. package/dist/multi-session.js +885 -0
  69. package/dist/multi-session.js.map +1 -0
  70. package/dist/music-learning.d.ts +181 -0
  71. package/dist/music-learning.d.ts.map +1 -0
  72. package/dist/music-learning.js +340 -0
  73. package/dist/music-learning.js.map +1 -0
  74. package/dist/self-eval.d.ts.map +1 -1
  75. package/dist/self-eval.js +5 -2
  76. package/dist/self-eval.js.map +1 -1
  77. package/dist/skill-system.d.ts +68 -0
  78. package/dist/skill-system.d.ts.map +1 -0
  79. package/dist/skill-system.js +386 -0
  80. package/dist/skill-system.js.map +1 -0
  81. package/dist/streaming.d.ts.map +1 -1
  82. package/dist/streaming.js +0 -1
  83. package/dist/streaming.js.map +1 -1
  84. package/dist/teach.d.ts +136 -0
  85. package/dist/teach.d.ts.map +1 -0
  86. package/dist/teach.js +915 -0
  87. package/dist/teach.js.map +1 -0
  88. package/dist/telemetry.d.ts +1 -1
  89. package/dist/telemetry.d.ts.map +1 -1
  90. package/dist/telemetry.js.map +1 -1
  91. package/dist/tools/ableton.d.ts.map +1 -1
  92. package/dist/tools/ableton.js +24 -8
  93. package/dist/tools/ableton.js.map +1 -1
  94. package/dist/tools/arrangement-engine.d.ts +2 -0
  95. package/dist/tools/arrangement-engine.d.ts.map +1 -0
  96. package/dist/tools/arrangement-engine.js +644 -0
  97. package/dist/tools/arrangement-engine.js.map +1 -0
  98. package/dist/tools/browser-agent.js +2 -2
  99. package/dist/tools/browser-agent.js.map +1 -1
  100. package/dist/tools/forge.d.ts.map +1 -1
  101. package/dist/tools/forge.js +15 -26
  102. package/dist/tools/forge.js.map +1 -1
  103. package/dist/tools/git.d.ts.map +1 -1
  104. package/dist/tools/git.js +10 -7
  105. package/dist/tools/git.js.map +1 -1
  106. package/dist/tools/index.d.ts.map +1 -1
  107. package/dist/tools/index.js +5 -0
  108. package/dist/tools/index.js.map +1 -1
  109. package/dist/tools/producer-engine.d.ts +71 -0
  110. package/dist/tools/producer-engine.d.ts.map +1 -0
  111. package/dist/tools/producer-engine.js +1859 -0
  112. package/dist/tools/producer-engine.js.map +1 -0
  113. package/dist/tools/sound-designer.d.ts +2 -0
  114. package/dist/tools/sound-designer.d.ts.map +1 -0
  115. package/dist/tools/sound-designer.js +896 -0
  116. package/dist/tools/sound-designer.js.map +1 -0
  117. package/dist/voice-realtime.d.ts +54 -0
  118. package/dist/voice-realtime.d.ts.map +1 -0
  119. package/dist/voice-realtime.js +805 -0
  120. package/dist/voice-realtime.js.map +1 -0
  121. package/package.json +11 -4
@@ -0,0 +1,743 @@
1
+ // kbot Cloud Agent — Persistent background agent execution over HTTP
2
+ //
3
+ // Extends `kbot serve` with long-running agent orchestration:
4
+ // kbot serve --cloud
5
+ //
6
+ // Endpoints:
7
+ // POST /agents — Create a cloud agent
8
+ // GET /agents — List all agents
9
+ // GET /agents/:id — Get agent status + results
10
+ // POST /agents/:id/pause — Pause agent
11
+ // POST /agents/:id/resume — Resume agent
12
+ // DELETE /agents/:id — Kill agent
13
+ // POST /agents/:id/message — Send a message to running agent
14
+ // GET /agents/:id/stream — SSE stream of agent events
15
+ import { randomUUID } from 'node:crypto';
16
+ import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
17
+ import { readFile, writeFile } from 'node:fs/promises';
18
+ import { join } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+ import { runAgent } from './agent.js';
21
+ import { ResponseStream } from './streaming.js';
22
+ // ── Constants ──
23
+ const MAX_CONCURRENT_AGENTS = 4;
24
+ const PERSIST_DIR = join(homedir(), '.kbot', 'cloud-agents');
25
+ const CRON_CHECK_INTERVAL_MS = 60_000; // check cron schedules every 60s
26
+ // ── State ──
27
+ const agents = new Map();
28
+ const runningAbortControllers = new Map();
29
+ const sseListeners = new Map();
30
+ let cronIntervalId = null;
31
+ // ── Persistence ──
32
+ function ensurePersistDir() {
33
+ if (!existsSync(PERSIST_DIR)) {
34
+ mkdirSync(PERSIST_DIR, { recursive: true });
35
+ }
36
+ }
37
+ async function persistAgent(agent) {
38
+ ensurePersistDir();
39
+ const filePath = join(PERSIST_DIR, `${agent.id}.json`);
40
+ await writeFile(filePath, JSON.stringify(agent, null, 2), 'utf-8');
41
+ }
42
+ async function loadPersistedAgents() {
43
+ ensurePersistDir();
44
+ const files = readdirSync(PERSIST_DIR).filter(f => f.endsWith('.json'));
45
+ for (const file of files) {
46
+ try {
47
+ const raw = await readFile(join(PERSIST_DIR, file), 'utf-8');
48
+ const agent = JSON.parse(raw);
49
+ // Normalize older persisted agents that lack pendingMessages
50
+ if (!agent.pendingMessages)
51
+ agent.pendingMessages = [];
52
+ agents.set(agent.id, agent);
53
+ }
54
+ catch {
55
+ // Skip corrupted files
56
+ }
57
+ }
58
+ }
59
+ function removePersistedAgent(id) {
60
+ const filePath = join(PERSIST_DIR, `${id}.json`);
61
+ try {
62
+ unlinkSync(filePath);
63
+ }
64
+ catch { /* already gone */ }
65
+ }
66
+ // ── SSE Helpers ──
67
+ function emitSSE(agentId, event, data) {
68
+ const listeners = sseListeners.get(agentId);
69
+ if (!listeners || listeners.size === 0)
70
+ return;
71
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
72
+ for (const res of listeners) {
73
+ try {
74
+ res.write(payload);
75
+ }
76
+ catch {
77
+ listeners.delete(res);
78
+ }
79
+ }
80
+ }
81
+ function addSSEListener(agentId, res) {
82
+ if (!sseListeners.has(agentId))
83
+ sseListeners.set(agentId, new Set());
84
+ sseListeners.get(agentId).add(res);
85
+ }
86
+ function removeSSEListener(agentId, res) {
87
+ sseListeners.get(agentId)?.delete(res);
88
+ }
89
+ // ── Webhook ──
90
+ async function postWebhook(url, payload) {
91
+ try {
92
+ await fetch(url, {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify(payload),
96
+ signal: AbortSignal.timeout(15_000),
97
+ });
98
+ }
99
+ catch {
100
+ // Webhook delivery is best-effort; don't crash the agent
101
+ }
102
+ }
103
+ /**
104
+ * Parse a standard 5-field cron expression into expanded arrays.
105
+ *
106
+ * Supports:
107
+ * - Wildcards: *
108
+ * - Ranges: 1-5
109
+ * - Steps: *\/5, 1-10/2
110
+ * - Lists: 1,3,5
111
+ * - Combinations: 1-5,10,15-20/2
112
+ *
113
+ * Fields: minute (0-59), hour (0-23), day-of-month (1-31), month (1-12), day-of-week (0-6, Sun=0)
114
+ */
115
+ export function parseCron(expr) {
116
+ const parts = expr.trim().split(/\s+/);
117
+ if (parts.length !== 5) {
118
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);
119
+ }
120
+ const ranges = [
121
+ [0, 59], // minute
122
+ [0, 23], // hour
123
+ [1, 31], // dayOfMonth
124
+ [1, 12], // month
125
+ [0, 6], // dayOfWeek
126
+ ];
127
+ function expandField(field, min, max) {
128
+ const values = new Set();
129
+ for (const segment of field.split(',')) {
130
+ // Handle step: */N or range/N
131
+ const stepMatch = segment.match(/^(.+)\/(\d+)$/);
132
+ const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
133
+ const base = stepMatch ? stepMatch[1] : segment;
134
+ let rangeStart = min;
135
+ let rangeEnd = max;
136
+ if (base === '*') {
137
+ // full range
138
+ }
139
+ else if (base.includes('-')) {
140
+ const [lo, hi] = base.split('-').map(Number);
141
+ if (isNaN(lo) || isNaN(hi) || lo < min || hi > max || lo > hi) {
142
+ throw new Error(`Invalid cron range: ${base} (valid: ${min}-${max})`);
143
+ }
144
+ rangeStart = lo;
145
+ rangeEnd = hi;
146
+ }
147
+ else {
148
+ const val = parseInt(base, 10);
149
+ if (isNaN(val) || val < min || val > max) {
150
+ throw new Error(`Invalid cron value: ${base} (valid: ${min}-${max})`);
151
+ }
152
+ if (step === 1) {
153
+ values.add(val);
154
+ continue;
155
+ }
156
+ rangeStart = val;
157
+ rangeEnd = max;
158
+ }
159
+ for (let i = rangeStart; i <= rangeEnd; i += step) {
160
+ values.add(i);
161
+ }
162
+ }
163
+ return Array.from(values).sort((a, b) => a - b);
164
+ }
165
+ return {
166
+ minute: expandField(parts[0], ranges[0][0], ranges[0][1]),
167
+ hour: expandField(parts[1], ranges[1][0], ranges[1][1]),
168
+ dayOfMonth: expandField(parts[2], ranges[2][0], ranges[2][1]),
169
+ month: expandField(parts[3], ranges[3][0], ranges[3][1]),
170
+ dayOfWeek: expandField(parts[4], ranges[4][0], ranges[4][1]),
171
+ };
172
+ }
173
+ export function shouldRunAt(cron, date) {
174
+ return (cron.minute.includes(date.getMinutes()) &&
175
+ cron.hour.includes(date.getHours()) &&
176
+ cron.month.includes(date.getMonth() + 1) &&
177
+ // Standard cron: if both day-of-month and day-of-week are restricted, match either
178
+ (cron.dayOfMonth.includes(date.getDate()) || cron.dayOfWeek.includes(date.getDay())));
179
+ }
180
+ // ── Agent Execution ──
181
+ function countRunningAgents() {
182
+ let count = 0;
183
+ for (const a of agents.values()) {
184
+ if (a.status === 'running')
185
+ count++;
186
+ }
187
+ return count;
188
+ }
189
+ function promoteQueuedAgent() {
190
+ for (const agent of agents.values()) {
191
+ if (agent.status === 'queued') {
192
+ executeAgent(agent);
193
+ return;
194
+ }
195
+ }
196
+ }
197
+ async function executeAgent(agent) {
198
+ agent.status = 'running';
199
+ agent.lastActiveAt = new Date().toISOString();
200
+ emitSSE(agent.id, 'status', { status: 'running', iteration: agent.currentIteration });
201
+ await persistAgent(agent);
202
+ const ac = new AbortController();
203
+ runningAbortControllers.set(agent.id, ac);
204
+ const iterations = agent.maxIterations ?? 1;
205
+ try {
206
+ while (agent.currentIteration < iterations && agent.status === 'running') {
207
+ const iterationIndex = agent.currentIteration;
208
+ const startTime = Date.now();
209
+ // Build the task message, appending pending messages if any
210
+ let taskMessage = agent.task;
211
+ if (agent.pendingMessages.length > 0) {
212
+ taskMessage += '\n\n--- Additional context ---\n' + agent.pendingMessages.join('\n');
213
+ agent.pendingMessages = [];
214
+ }
215
+ // Iteration context for multi-iteration agents
216
+ if (iterations > 1) {
217
+ taskMessage = `[Iteration ${iterationIndex + 1}/${iterations}] ${taskMessage}`;
218
+ }
219
+ emitSSE(agent.id, 'iteration_start', { iteration: iterationIndex + 1, total: iterations });
220
+ // Create a ResponseStream to capture tool calls and content
221
+ const stream = new ResponseStream();
222
+ const toolCalls = [];
223
+ stream.on((event) => {
224
+ if (event.type === 'tool_call_start') {
225
+ toolCalls.push(event.name);
226
+ emitSSE(agent.id, 'tool_call', { tool: event.name, id: event.id });
227
+ }
228
+ if (event.type === 'tool_call_end') {
229
+ emitSSE(agent.id, 'tool_call_end', { tool: event.name, id: event.id });
230
+ }
231
+ if (event.type === 'content_delta') {
232
+ emitSSE(agent.id, 'content_delta', { delta: event.text });
233
+ }
234
+ if (event.type === 'error') {
235
+ emitSSE(agent.id, 'error', { error: event.message });
236
+ }
237
+ });
238
+ const agentOpts = {
239
+ agent: agent.agent,
240
+ stream: true,
241
+ responseStream: stream,
242
+ };
243
+ let response;
244
+ try {
245
+ response = await runAgent(taskMessage, agentOpts);
246
+ }
247
+ catch (err) {
248
+ // If this is an abort (paused or killed), stop cleanly
249
+ if (ac.signal.aborted)
250
+ return;
251
+ const errorMsg = err instanceof Error ? err.message : String(err);
252
+ agent.error = errorMsg;
253
+ agent.status = 'failed';
254
+ agent.lastActiveAt = new Date().toISOString();
255
+ emitSSE(agent.id, 'error', { error: errorMsg, iteration: iterationIndex + 1 });
256
+ emitSSE(agent.id, 'status', { status: 'failed' });
257
+ await persistAgent(agent);
258
+ runningAbortControllers.delete(agent.id);
259
+ promoteQueuedAgent();
260
+ return;
261
+ }
262
+ const duration = Date.now() - startTime;
263
+ const result = {
264
+ iteration: iterationIndex + 1,
265
+ content: response.content,
266
+ toolCalls,
267
+ tokens: {
268
+ input: response.usage?.input_tokens ?? 0,
269
+ output: response.usage?.output_tokens ?? 0,
270
+ },
271
+ duration,
272
+ timestamp: new Date().toISOString(),
273
+ };
274
+ agent.results.push(result);
275
+ agent.currentIteration = iterationIndex + 1;
276
+ agent.lastActiveAt = new Date().toISOString();
277
+ emitSSE(agent.id, 'iteration_complete', {
278
+ iteration: iterationIndex + 1,
279
+ content: response.content.slice(0, 500), // preview
280
+ toolCalls: toolCalls.length,
281
+ duration,
282
+ });
283
+ // Webhook delivery
284
+ if (agent.webhook) {
285
+ await postWebhook(agent.webhook, {
286
+ agentId: agent.id,
287
+ agentName: agent.name,
288
+ event: 'iteration_complete',
289
+ result,
290
+ });
291
+ }
292
+ await persistAgent(agent);
293
+ // Check if paused during iteration
294
+ if (agent.status !== 'running')
295
+ return;
296
+ }
297
+ // All iterations completed
298
+ if (agent.status === 'running') {
299
+ agent.status = 'completed';
300
+ agent.lastActiveAt = new Date().toISOString();
301
+ emitSSE(agent.id, 'status', { status: 'completed' });
302
+ if (agent.webhook) {
303
+ await postWebhook(agent.webhook, {
304
+ agentId: agent.id,
305
+ agentName: agent.name,
306
+ event: 'completed',
307
+ totalIterations: agent.currentIteration,
308
+ results: agent.results,
309
+ });
310
+ }
311
+ await persistAgent(agent);
312
+ }
313
+ }
314
+ finally {
315
+ runningAbortControllers.delete(agent.id);
316
+ promoteQueuedAgent();
317
+ }
318
+ }
319
+ // ── CRUD Operations ──
320
+ export async function createCloudAgent(req) {
321
+ if (!req.task || req.task.trim().length === 0) {
322
+ throw new Error('Missing required field: task');
323
+ }
324
+ // Validate cron expression if provided
325
+ if (req.schedule) {
326
+ parseCron(req.schedule); // throws on invalid
327
+ }
328
+ const agent = {
329
+ id: randomUUID(),
330
+ name: req.name || `agent-${Date.now().toString(36)}`,
331
+ status: 'queued',
332
+ task: req.task,
333
+ agent: req.agent,
334
+ schedule: req.schedule,
335
+ webhook: req.webhook,
336
+ maxIterations: req.maxIterations ?? 1,
337
+ currentIteration: 0,
338
+ results: [],
339
+ createdAt: new Date().toISOString(),
340
+ lastActiveAt: new Date().toISOString(),
341
+ pendingMessages: [],
342
+ };
343
+ agents.set(agent.id, agent);
344
+ await persistAgent(agent);
345
+ // If scheduled, don't start immediately — the cron scheduler will trigger it
346
+ if (agent.schedule) {
347
+ agent.status = 'paused';
348
+ emitSSE(agent.id, 'status', { status: 'paused', reason: 'scheduled' });
349
+ await persistAgent(agent);
350
+ return agent;
351
+ }
352
+ // Start immediately if under concurrency limit
353
+ if (countRunningAgents() < MAX_CONCURRENT_AGENTS) {
354
+ // Fire and forget — executeAgent manages its own lifecycle
355
+ executeAgent(agent).catch(() => { });
356
+ }
357
+ else {
358
+ emitSSE(agent.id, 'status', { status: 'queued', position: countQueuePosition(agent.id) });
359
+ }
360
+ return agent;
361
+ }
362
+ export function listCloudAgents() {
363
+ return Array.from(agents.values()).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
364
+ }
365
+ export function getCloudAgent(id) {
366
+ return agents.get(id);
367
+ }
368
+ export async function pauseCloudAgent(id) {
369
+ const agent = agents.get(id);
370
+ if (!agent)
371
+ throw new Error(`Agent not found: ${id}`);
372
+ if (agent.status !== 'running' && agent.status !== 'queued') {
373
+ throw new Error(`Cannot pause agent in status: ${agent.status}`);
374
+ }
375
+ agent.status = 'paused';
376
+ agent.lastActiveAt = new Date().toISOString();
377
+ // Abort the running execution
378
+ const ac = runningAbortControllers.get(id);
379
+ if (ac) {
380
+ ac.abort();
381
+ runningAbortControllers.delete(id);
382
+ }
383
+ emitSSE(id, 'status', { status: 'paused' });
384
+ await persistAgent(agent);
385
+ return agent;
386
+ }
387
+ export async function resumeCloudAgent(id) {
388
+ const agent = agents.get(id);
389
+ if (!agent)
390
+ throw new Error(`Agent not found: ${id}`);
391
+ if (agent.status !== 'paused') {
392
+ throw new Error(`Cannot resume agent in status: ${agent.status}`);
393
+ }
394
+ if (countRunningAgents() < MAX_CONCURRENT_AGENTS) {
395
+ executeAgent(agent).catch(() => { });
396
+ }
397
+ else {
398
+ agent.status = 'queued';
399
+ emitSSE(id, 'status', { status: 'queued', position: countQueuePosition(id) });
400
+ await persistAgent(agent);
401
+ }
402
+ return agent;
403
+ }
404
+ export async function killCloudAgent(id) {
405
+ const agent = agents.get(id);
406
+ if (!agent)
407
+ throw new Error(`Agent not found: ${id}`);
408
+ // Abort if running
409
+ const ac = runningAbortControllers.get(id);
410
+ if (ac) {
411
+ ac.abort();
412
+ runningAbortControllers.delete(id);
413
+ }
414
+ // Close SSE connections
415
+ const listeners = sseListeners.get(id);
416
+ if (listeners) {
417
+ for (const res of listeners) {
418
+ try {
419
+ res.end();
420
+ }
421
+ catch { /* ignore */ }
422
+ }
423
+ sseListeners.delete(id);
424
+ }
425
+ agents.delete(id);
426
+ removePersistedAgent(id);
427
+ }
428
+ export function sendMessageToAgent(id, message) {
429
+ const agent = agents.get(id);
430
+ if (!agent)
431
+ throw new Error(`Agent not found: ${id}`);
432
+ if (agent.status !== 'running' && agent.status !== 'paused') {
433
+ throw new Error(`Cannot send message to agent in status: ${agent.status}`);
434
+ }
435
+ agent.pendingMessages.push(message);
436
+ emitSSE(id, 'message_queued', { message: message.slice(0, 200) });
437
+ return agent;
438
+ }
439
+ // ── Queue Helpers ──
440
+ function countQueuePosition(id) {
441
+ let pos = 0;
442
+ for (const agent of agents.values()) {
443
+ if (agent.status === 'queued') {
444
+ pos++;
445
+ if (agent.id === id)
446
+ return pos;
447
+ }
448
+ }
449
+ return pos;
450
+ }
451
+ // ── Cron Scheduler ──
452
+ function startCronScheduler() {
453
+ if (cronIntervalId)
454
+ return;
455
+ cronIntervalId = setInterval(() => {
456
+ const now = new Date();
457
+ for (const agent of agents.values()) {
458
+ if (!agent.schedule || agent.status !== 'paused')
459
+ continue;
460
+ try {
461
+ const cron = parseCron(agent.schedule);
462
+ if (shouldRunAt(cron, now)) {
463
+ // Reset iteration for a new scheduled run
464
+ agent.currentIteration = 0;
465
+ agent.results = [];
466
+ agent.error = undefined;
467
+ if (countRunningAgents() < MAX_CONCURRENT_AGENTS) {
468
+ executeAgent(agent).catch(() => { });
469
+ }
470
+ else {
471
+ agent.status = 'queued';
472
+ emitSSE(agent.id, 'status', { status: 'queued', reason: 'cron_triggered' });
473
+ persistAgent(agent).catch(() => { });
474
+ }
475
+ }
476
+ }
477
+ catch {
478
+ // Invalid cron — skip silently, it was validated on creation
479
+ }
480
+ }
481
+ }, CRON_CHECK_INTERVAL_MS);
482
+ // Don't let the interval keep the process alive
483
+ if (cronIntervalId && typeof cronIntervalId === 'object' && 'unref' in cronIntervalId) {
484
+ cronIntervalId.unref();
485
+ }
486
+ }
487
+ function stopCronScheduler() {
488
+ if (cronIntervalId) {
489
+ clearInterval(cronIntervalId);
490
+ cronIntervalId = null;
491
+ }
492
+ }
493
+ // ── HTTP Route Handler ──
494
+ function cors(res) {
495
+ res.setHeader('Access-Control-Allow-Origin', '*');
496
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
497
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
498
+ }
499
+ function json(res, status, data) {
500
+ cors(res);
501
+ res.writeHead(status, { 'Content-Type': 'application/json' });
502
+ res.end(JSON.stringify(data));
503
+ }
504
+ function readBody(req) {
505
+ return new Promise((resolve, reject) => {
506
+ const chunks = [];
507
+ req.on('data', (c) => chunks.push(c));
508
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
509
+ req.on('error', reject);
510
+ });
511
+ }
512
+ function stripSensitiveFields(agent) {
513
+ // Return a plain object without internal pendingMessages when listing
514
+ const { pendingMessages: _pm, ...rest } = agent;
515
+ return {
516
+ ...rest,
517
+ pendingMessageCount: agent.pendingMessages.length,
518
+ };
519
+ }
520
+ /**
521
+ * Returns an async route handler function that can be mounted in the kbot serve HTTP server.
522
+ *
523
+ * Usage in serve.ts:
524
+ * const cloudRoutes = getCloudAgentRoutes()
525
+ * // Inside the request handler:
526
+ * if (await cloudRoutes(req, res)) return // handled
527
+ */
528
+ export function getCloudAgentRoutes() {
529
+ // Initialize: load persisted agents and start cron scheduler
530
+ let initialized = false;
531
+ let initPromise = null;
532
+ async function ensureInit() {
533
+ if (initialized)
534
+ return;
535
+ if (!initPromise) {
536
+ initPromise = (async () => {
537
+ await loadPersistedAgents();
538
+ startCronScheduler();
539
+ // Resume any agents that were running when server last stopped
540
+ for (const agent of agents.values()) {
541
+ if (agent.status === 'running') {
542
+ // They weren't gracefully stopped — mark as paused so user can resume
543
+ agent.status = 'paused';
544
+ agent.error = 'Server restarted — agent paused. Resume with POST /agents/:id/resume';
545
+ await persistAgent(agent);
546
+ }
547
+ }
548
+ initialized = true;
549
+ })();
550
+ }
551
+ await initPromise;
552
+ }
553
+ return async (req, res) => {
554
+ const url = new URL(req.url || '/', 'http://localhost');
555
+ const path = url.pathname;
556
+ // Only handle /agents routes
557
+ if (!path.startsWith('/agents'))
558
+ return false;
559
+ // CORS preflight
560
+ if (req.method === 'OPTIONS') {
561
+ cors(res);
562
+ res.writeHead(204);
563
+ res.end();
564
+ return true;
565
+ }
566
+ await ensureInit();
567
+ try {
568
+ // POST /agents — create a cloud agent
569
+ if (path === '/agents' && req.method === 'POST') {
570
+ const body = JSON.parse(await readBody(req));
571
+ const agent = await createCloudAgent(body);
572
+ json(res, 201, stripSensitiveFields(agent));
573
+ return true;
574
+ }
575
+ // GET /agents — list all agents
576
+ if (path === '/agents' && req.method === 'GET') {
577
+ const status = url.searchParams.get('status') || undefined;
578
+ let list = listCloudAgents();
579
+ if (status) {
580
+ list = list.filter(a => a.status === status);
581
+ }
582
+ json(res, 200, {
583
+ agents: list.map(stripSensitiveFields),
584
+ count: list.length,
585
+ running: countRunningAgents(),
586
+ maxConcurrent: MAX_CONCURRENT_AGENTS,
587
+ });
588
+ return true;
589
+ }
590
+ // Match /agents/:id or /agents/:id/action
591
+ const idMatch = path.match(/^\/agents\/([a-f0-9-]{36})(?:\/(\w+))?$/);
592
+ if (!idMatch) {
593
+ json(res, 404, { error: 'Not found' });
594
+ return true;
595
+ }
596
+ const agentId = idMatch[1];
597
+ const action = idMatch[2]; // pause, resume, message, stream — or undefined for GET/DELETE
598
+ // GET /agents/:id — get agent details
599
+ if (!action && req.method === 'GET') {
600
+ const agent = getCloudAgent(agentId);
601
+ if (!agent) {
602
+ json(res, 404, { error: `Agent not found: ${agentId}` });
603
+ return true;
604
+ }
605
+ json(res, 200, stripSensitiveFields(agent));
606
+ return true;
607
+ }
608
+ // DELETE /agents/:id — kill agent
609
+ if (!action && req.method === 'DELETE') {
610
+ try {
611
+ await killCloudAgent(agentId);
612
+ json(res, 200, { ok: true, deleted: agentId });
613
+ }
614
+ catch (err) {
615
+ json(res, 404, { error: err instanceof Error ? err.message : String(err) });
616
+ }
617
+ return true;
618
+ }
619
+ // POST /agents/:id/pause
620
+ if (action === 'pause' && req.method === 'POST') {
621
+ try {
622
+ const agent = await pauseCloudAgent(agentId);
623
+ json(res, 200, stripSensitiveFields(agent));
624
+ }
625
+ catch (err) {
626
+ json(res, 400, { error: err instanceof Error ? err.message : String(err) });
627
+ }
628
+ return true;
629
+ }
630
+ // POST /agents/:id/resume
631
+ if (action === 'resume' && req.method === 'POST') {
632
+ try {
633
+ const agent = await resumeCloudAgent(agentId);
634
+ json(res, 200, stripSensitiveFields(agent));
635
+ }
636
+ catch (err) {
637
+ json(res, 400, { error: err instanceof Error ? err.message : String(err) });
638
+ }
639
+ return true;
640
+ }
641
+ // POST /agents/:id/message — inject a message
642
+ if (action === 'message' && req.method === 'POST') {
643
+ const body = JSON.parse(await readBody(req));
644
+ const message = body.message;
645
+ if (!message) {
646
+ json(res, 400, { error: 'Missing "message" field' });
647
+ return true;
648
+ }
649
+ try {
650
+ const agent = sendMessageToAgent(agentId, message);
651
+ json(res, 200, {
652
+ ok: true,
653
+ pendingMessageCount: agent.pendingMessages.length,
654
+ });
655
+ }
656
+ catch (err) {
657
+ json(res, 400, { error: err instanceof Error ? err.message : String(err) });
658
+ }
659
+ return true;
660
+ }
661
+ // GET /agents/:id/stream — SSE event stream
662
+ if (action === 'stream' && req.method === 'GET') {
663
+ const agent = getCloudAgent(agentId);
664
+ if (!agent) {
665
+ json(res, 404, { error: `Agent not found: ${agentId}` });
666
+ return true;
667
+ }
668
+ cors(res);
669
+ res.writeHead(200, {
670
+ 'Content-Type': 'text/event-stream',
671
+ 'Cache-Control': 'no-cache',
672
+ 'Connection': 'keep-alive',
673
+ });
674
+ // Send current state as initial event
675
+ res.write(`event: connected\ndata: ${JSON.stringify({
676
+ agentId: agent.id,
677
+ name: agent.name,
678
+ status: agent.status,
679
+ currentIteration: agent.currentIteration,
680
+ totalIterations: agent.maxIterations ?? 1,
681
+ })}\n\n`);
682
+ addSSEListener(agentId, res);
683
+ // Keep-alive ping every 30s
684
+ const pingInterval = setInterval(() => {
685
+ try {
686
+ res.write(': ping\n\n');
687
+ }
688
+ catch {
689
+ clearInterval(pingInterval);
690
+ removeSSEListener(agentId, res);
691
+ }
692
+ }, 30_000);
693
+ req.on('close', () => {
694
+ clearInterval(pingInterval);
695
+ removeSSEListener(agentId, res);
696
+ });
697
+ return true;
698
+ }
699
+ json(res, 404, { error: `Unknown action: ${action}` });
700
+ return true;
701
+ }
702
+ catch (err) {
703
+ json(res, 500, { error: err instanceof Error ? err.message : String(err) });
704
+ return true;
705
+ }
706
+ };
707
+ }
708
+ // ── Cleanup ──
709
+ export function shutdownCloudAgents() {
710
+ stopCronScheduler();
711
+ // Abort all running agents
712
+ for (const [id, ac] of runningAbortControllers) {
713
+ ac.abort();
714
+ const agent = agents.get(id);
715
+ if (agent) {
716
+ agent.status = 'paused';
717
+ agent.error = 'Server shutting down';
718
+ // Synchronous write on shutdown — best effort
719
+ try {
720
+ writeFileSync(join(PERSIST_DIR, `${id}.json`), JSON.stringify(agent, null, 2), 'utf-8');
721
+ }
722
+ catch { /* best effort */ }
723
+ }
724
+ }
725
+ runningAbortControllers.clear();
726
+ // Close all SSE connections
727
+ for (const listeners of sseListeners.values()) {
728
+ for (const res of listeners) {
729
+ try {
730
+ res.end();
731
+ }
732
+ catch { /* ignore */ }
733
+ }
734
+ }
735
+ sseListeners.clear();
736
+ }
737
+ // ── Re-exports for convenience ──
738
+ // parseCron, shouldRunAt, ParsedCron are exported at their declaration sites above
739
+ // CloudAgent, AgentResult are exported at their interface declarations above
740
+ // createCloudAgent, listCloudAgents, getCloudAgent, pauseCloudAgent,
741
+ // resumeCloudAgent, killCloudAgent, getCloudAgentRoutes, shutdownCloudAgents
742
+ // are exported at their function declarations above
743
+ //# sourceMappingURL=cloud-agent.js.map