@actwith-ai/pods 0.3.1

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/index.js ADDED
@@ -0,0 +1,2021 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // src/docker-runtime.ts
34
+ var docker_runtime_exports = {};
35
+ __export(docker_runtime_exports, {
36
+ isDockerAvailable: () => isDockerAvailable,
37
+ runDockerQuery: () => runDockerQuery
38
+ });
39
+ function isDockerAvailable() {
40
+ try {
41
+ const { execSync } = require("child_process");
42
+ execSync("docker --version", { stdio: "ignore" });
43
+ execSync(`docker image inspect ${DOCKER_IMAGE}`, { stdio: "ignore" });
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ async function runDockerQuery(options) {
50
+ const {
51
+ prompt,
52
+ identity,
53
+ config,
54
+ model,
55
+ maxTurns,
56
+ podIndex,
57
+ abortController,
58
+ onEvent
59
+ } = options;
60
+ return new Promise((resolve) => {
61
+ let stdout = "";
62
+ let killed = false;
63
+ const containerEnv = {
64
+ ANTHROPIC_API_KEY: config.anthropicApiKey,
65
+ ACTWITH_API_KEY: identity.apiKey,
66
+ ACTWITH_API_URL: config.apiUrl,
67
+ ACTWITH_SPACE_ID: config.spaceId
68
+ };
69
+ const dockerArgs = [
70
+ "run",
71
+ "--rm",
72
+ "-v",
73
+ "/dev/null:/workspace/.gitkeep",
74
+ // Minimal mount
75
+ "--network=host",
76
+ "--security-opt",
77
+ "no-new-privileges:true",
78
+ "--tmpfs",
79
+ "/tmp:size=500M"
80
+ ];
81
+ for (const [key, value] of Object.entries(containerEnv)) {
82
+ if (value) {
83
+ dockerArgs.push("-e", `${key}=${value}`);
84
+ }
85
+ }
86
+ dockerArgs.push(
87
+ DOCKER_IMAGE,
88
+ "-p",
89
+ prompt,
90
+ "--output-format",
91
+ "stream-json",
92
+ "--max-turns",
93
+ String(maxTurns),
94
+ "--model",
95
+ model,
96
+ "--dangerously-skip-permissions"
97
+ );
98
+ const child = (0, import_child_process.spawn)("docker", dockerArgs);
99
+ const onAbort = () => {
100
+ killed = true;
101
+ child.kill("SIGTERM");
102
+ setTimeout(() => {
103
+ try {
104
+ child.kill("SIGKILL");
105
+ } catch {
106
+ }
107
+ }, 5e3);
108
+ };
109
+ abortController.signal.addEventListener("abort", onAbort, { once: true });
110
+ let buffer = "";
111
+ child.stdout.on("data", (data) => {
112
+ const chunk = data.toString();
113
+ stdout += chunk;
114
+ buffer += chunk;
115
+ const lines = buffer.split("\n");
116
+ buffer = lines.pop() || "";
117
+ for (const line of lines) {
118
+ if (!line.trim()) continue;
119
+ processStreamLine(line, podIndex, onEvent);
120
+ }
121
+ });
122
+ child.stderr.on("data", (data) => {
123
+ const text = data.toString().trim();
124
+ if (text && onEvent) {
125
+ onEvent({
126
+ type: "text_output",
127
+ podIndex,
128
+ text: `[docker] ${text.slice(0, 150)}`,
129
+ timestamp: Date.now()
130
+ });
131
+ }
132
+ });
133
+ child.on("error", (err) => {
134
+ abortController.signal.removeEventListener("abort", onAbort);
135
+ resolve({
136
+ success: false,
137
+ resultText: "",
138
+ costUsd: 0,
139
+ numTurns: 0,
140
+ sessionId: null,
141
+ errors: [err.message]
142
+ });
143
+ });
144
+ child.on("close", () => {
145
+ abortController.signal.removeEventListener("abort", onAbort);
146
+ if (killed) {
147
+ resolve({
148
+ success: false,
149
+ resultText: "",
150
+ costUsd: 0,
151
+ numTurns: 0,
152
+ sessionId: null,
153
+ errors: ["Aborted"]
154
+ });
155
+ return;
156
+ }
157
+ if (buffer.trim()) {
158
+ processStreamLine(buffer, podIndex, onEvent);
159
+ }
160
+ const result = parseStreamResult(stdout);
161
+ resolve(result);
162
+ });
163
+ });
164
+ }
165
+ function processStreamLine(line, podIndex, onEvent) {
166
+ if (!onEvent) return;
167
+ try {
168
+ const msg = JSON.parse(line);
169
+ if (msg.type === "assistant" && msg.message?.content) {
170
+ for (const block of msg.message.content) {
171
+ if (block.type === "tool_use" && block.name) {
172
+ onEvent({
173
+ type: "tool_use",
174
+ podIndex,
175
+ toolName: block.name,
176
+ timestamp: Date.now()
177
+ });
178
+ } else if (block.type === "text" && block.text?.trim()) {
179
+ const text = block.text.trim();
180
+ onEvent({
181
+ type: "text_output",
182
+ podIndex,
183
+ text: text.length > 200 ? text.slice(0, 200) + "..." : text,
184
+ timestamp: Date.now()
185
+ });
186
+ }
187
+ }
188
+ }
189
+ if (msg.type === "result" && msg.total_cost_usd !== void 0) {
190
+ onEvent({
191
+ type: "cost_update",
192
+ podIndex,
193
+ costUsd: msg.total_cost_usd,
194
+ timestamp: Date.now()
195
+ });
196
+ }
197
+ } catch {
198
+ }
199
+ }
200
+ function parseStreamResult(output) {
201
+ const lines = output.split("\n").filter((l) => l.trim());
202
+ let resultText = "";
203
+ let sessionId = null;
204
+ let numTurns = 0;
205
+ let costUsd = 0;
206
+ let errors = [];
207
+ let foundResult = false;
208
+ for (let i = lines.length - 1; i >= 0; i--) {
209
+ try {
210
+ const msg = JSON.parse(lines[i]);
211
+ if (msg.type === "result") {
212
+ foundResult = true;
213
+ sessionId = msg.session_id ?? null;
214
+ numTurns = msg.num_turns ?? 0;
215
+ costUsd = msg.total_cost_usd ?? 0;
216
+ if (msg.subtype === "success" && msg.result) {
217
+ resultText = msg.result;
218
+ } else if (msg.errors) {
219
+ errors = msg.errors;
220
+ }
221
+ break;
222
+ }
223
+ } catch {
224
+ }
225
+ }
226
+ if (!foundResult) {
227
+ try {
228
+ const parsed = JSON.parse(output);
229
+ return {
230
+ success: !parsed.is_error,
231
+ resultText: parsed.result || "",
232
+ costUsd: parsed.total_cost_usd || 0,
233
+ numTurns: parsed.num_turns || 0,
234
+ sessionId: parsed.session_id || null,
235
+ errors: parsed.is_error ? ["Docker execution failed"] : []
236
+ };
237
+ } catch {
238
+ errors = ["No result message in Docker output"];
239
+ }
240
+ }
241
+ return {
242
+ success: errors.length === 0,
243
+ resultText,
244
+ costUsd,
245
+ numTurns,
246
+ sessionId,
247
+ errors
248
+ };
249
+ }
250
+ var import_child_process, DOCKER_IMAGE;
251
+ var init_docker_runtime = __esm({
252
+ "src/docker-runtime.ts"() {
253
+ "use strict";
254
+ import_child_process = require("child_process");
255
+ DOCKER_IMAGE = "actwith/claude-test:latest";
256
+ }
257
+ });
258
+
259
+ // src/ui/Header.tsx
260
+ function Header({ state, budgetGlobal, dockerMode }) {
261
+ const uptime = Math.round((Date.now() - state.startedAt) / 1e3);
262
+ const mins = Math.floor(uptime / 60);
263
+ const secs = uptime % 60;
264
+ const uptimeStr = `${mins}m ${secs}s`;
265
+ const activePods = state.pods.filter((p) => p.phase === "ACTIVE").length;
266
+ const budgetPct = budgetGlobal > 0 ? Math.round(state.globalCostUsd / budgetGlobal * 100) : 0;
267
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 1, children: [
268
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink.Text, { bold: true, color: "cyan", children: "actwith pods" }),
269
+ dockerMode && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink.Text, { color: "blue", children: "[docker]" }),
270
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Text, { children: [
271
+ state.pods.length,
272
+ " pods (",
273
+ activePods,
274
+ " active)"
275
+ ] }),
276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Text, { children: [
277
+ "$",
278
+ state.globalCostUsd.toFixed(4),
279
+ " / $",
280
+ budgetGlobal.toFixed(2),
281
+ " (",
282
+ budgetPct,
283
+ "%)"
284
+ ] }),
285
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Text, { dimColor: true, children: [
286
+ "up ",
287
+ uptimeStr
288
+ ] }),
289
+ state.shuttingDown && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink.Text, { color: "yellow", children: " SHUTTING DOWN" })
290
+ ] });
291
+ }
292
+ var import_ink, import_jsx_runtime;
293
+ var init_Header = __esm({
294
+ "src/ui/Header.tsx"() {
295
+ "use strict";
296
+ import_ink = require("ink");
297
+ import_jsx_runtime = require("react/jsx-runtime");
298
+ }
299
+ });
300
+
301
+ // src/ui/PodRow.tsx
302
+ function PodRow({ pod, selected }) {
303
+ const color = PHASE_COLORS[pod.phase] || "white";
304
+ const name = pod.identity?.agentName ?? `pod-${pod.index}`;
305
+ const isActive = pod.phase === "ACTIVE" || pod.phase === "INITIALIZING";
306
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Box, { flexDirection: "row", paddingX: 1, children: [
307
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { color: selected ? "cyan" : "white", children: [
308
+ selected ? ">" : " ",
309
+ " "
310
+ ] }),
311
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { bold: true, children: name }) }),
312
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Box, { width: 14, children: isActive ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { color, children: [
313
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink_spinner.default, { type: "dots" }),
314
+ " ",
315
+ pod.phase
316
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { color, children: pod.phase }) }),
317
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Box, { width: 30, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Text, { dimColor: true, children: pod.currentTaskDescription ? pod.currentTaskDescription.slice(0, 28) : pod.lastActivity }) }),
318
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Box, { width: 12, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { children: [
319
+ "$",
320
+ pod.totalCostUsd.toFixed(4)
321
+ ] }) }),
322
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink2.Box, { width: 8, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { dimColor: true, children: [
323
+ pod.workCount,
324
+ "t"
325
+ ] }) }),
326
+ pod.error && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ink2.Text, { color: "red", children: [
327
+ " ",
328
+ pod.error.slice(0, 30)
329
+ ] })
330
+ ] });
331
+ }
332
+ var import_ink2, import_ink_spinner, import_jsx_runtime2, PHASE_COLORS;
333
+ var init_PodRow = __esm({
334
+ "src/ui/PodRow.tsx"() {
335
+ "use strict";
336
+ import_ink2 = require("ink");
337
+ import_ink_spinner = __toESM(require("ink-spinner"));
338
+ import_jsx_runtime2 = require("react/jsx-runtime");
339
+ PHASE_COLORS = {
340
+ INITIALIZING: "gray",
341
+ IDLE: "blue",
342
+ ACTIVE: "green",
343
+ COOLDOWN: "yellow",
344
+ SHUTTING_DOWN: "magenta",
345
+ STOPPED: "gray"
346
+ };
347
+ }
348
+ });
349
+
350
+ // src/ui/LogPane.tsx
351
+ function LogPane({
352
+ logs,
353
+ maxLines,
354
+ filterByPod,
355
+ selectedPod
356
+ }) {
357
+ const visible = logs.slice(-maxLines);
358
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
359
+ import_ink3.Box,
360
+ {
361
+ flexDirection: "column",
362
+ borderStyle: "single",
363
+ borderColor: "gray",
364
+ paddingX: 1,
365
+ children: [
366
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Box, { flexDirection: "row", justifyContent: "space-between", children: [
367
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, dimColor: true, children: "Log" }),
368
+ filterByPod && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Text, { color: "cyan", children: [
369
+ "filtered: pod-",
370
+ selectedPod
371
+ ] })
372
+ ] }),
373
+ visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { dimColor: true, children: "No events yet" }) : visible.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Text, { wrap: "truncate", children: [
374
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { dimColor: true, children: entry.time }),
375
+ " ",
376
+ entry.text
377
+ ] }, i))
378
+ ]
379
+ }
380
+ );
381
+ }
382
+ var import_ink3, import_jsx_runtime3;
383
+ var init_LogPane = __esm({
384
+ "src/ui/LogPane.tsx"() {
385
+ "use strict";
386
+ import_ink3 = require("ink");
387
+ import_jsx_runtime3 = require("react/jsx-runtime");
388
+ }
389
+ });
390
+
391
+ // src/ui/StatusBar.tsx
392
+ function StatusBar({ shuttingDown, filterByPod }) {
393
+ if (shuttingDown) {
394
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink4.Box, { paddingX: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink4.Text, { color: "yellow", children: "Shutting down... (Ctrl+C again to force)" }) });
395
+ }
396
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ink4.Box, { paddingX: 1, gap: 2, children: [
397
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink4.Text, { dimColor: true, children: "j/k: select pod" }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ink4.Text, { dimColor: true, children: [
399
+ "f: ",
400
+ filterByPod ? "show all" : "filter by pod"
401
+ ] }),
402
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink4.Text, { dimColor: true, children: "a: show all" }),
403
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink4.Text, { dimColor: true, children: "Ctrl+C: shutdown" })
404
+ ] });
405
+ }
406
+ var import_ink4, import_jsx_runtime4;
407
+ var init_StatusBar = __esm({
408
+ "src/ui/StatusBar.tsx"() {
409
+ "use strict";
410
+ import_ink4 = require("ink");
411
+ import_jsx_runtime4 = require("react/jsx-runtime");
412
+ }
413
+ });
414
+
415
+ // src/ui/Dashboard.tsx
416
+ function Dashboard({
417
+ state,
418
+ logs,
419
+ selectedPod,
420
+ budgetGlobal,
421
+ filterByPod,
422
+ shuttingDown,
423
+ dockerMode
424
+ }) {
425
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ink5.Box, { flexDirection: "column", children: [
426
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
427
+ Header,
428
+ {
429
+ state,
430
+ budgetGlobal,
431
+ dockerMode
432
+ }
433
+ ),
434
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ink5.Box, { flexDirection: "column", paddingY: 1, children: state.pods.map((pod) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
435
+ PodRow,
436
+ {
437
+ pod,
438
+ selected: pod.index === selectedPod
439
+ },
440
+ pod.index
441
+ )) }),
442
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
443
+ LogPane,
444
+ {
445
+ logs,
446
+ maxLines: 10,
447
+ filterByPod,
448
+ selectedPod
449
+ }
450
+ ),
451
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StatusBar, { shuttingDown, filterByPod })
452
+ ] });
453
+ }
454
+ var import_ink5, import_jsx_runtime5;
455
+ var init_Dashboard = __esm({
456
+ "src/ui/Dashboard.tsx"() {
457
+ "use strict";
458
+ import_ink5 = require("ink");
459
+ init_Header();
460
+ init_PodRow();
461
+ init_LogPane();
462
+ init_StatusBar();
463
+ import_jsx_runtime5 = require("react/jsx-runtime");
464
+ }
465
+ });
466
+
467
+ // src/ui/App.tsx
468
+ var App_exports = {};
469
+ __export(App_exports, {
470
+ renderDashboard: () => renderDashboard
471
+ });
472
+ function App({
473
+ orchestrator,
474
+ config
475
+ }) {
476
+ const [state, setState] = (0, import_react.useState)(
477
+ orchestrator.getState()
478
+ );
479
+ const [logs, setLogs] = (0, import_react.useState)([]);
480
+ const [selectedPod, setSelectedPod] = (0, import_react.useState)(0);
481
+ const [filterByPod, setFilterByPod] = (0, import_react.useState)(false);
482
+ (0, import_ink6.useInput)(
483
+ (0, import_react.useCallback)(
484
+ (input, key) => {
485
+ const podCount = state.pods.length;
486
+ if (podCount === 0) return;
487
+ if (key.upArrow || input === "k") {
488
+ setSelectedPod((prev) => (prev - 1 + podCount) % podCount);
489
+ } else if (key.downArrow || input === "j") {
490
+ setSelectedPod((prev) => (prev + 1) % podCount);
491
+ } else if (input === "f") {
492
+ setFilterByPod((prev) => !prev);
493
+ } else if (input === "a") {
494
+ setFilterByPod(false);
495
+ }
496
+ },
497
+ [state.pods.length]
498
+ )
499
+ );
500
+ (0, import_react.useEffect)(() => {
501
+ const refreshInterval = setInterval(() => {
502
+ setState(orchestrator.getState());
503
+ }, 500);
504
+ const handleLog = (msg) => {
505
+ const time = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
506
+ setLogs((prev) => [
507
+ ...prev.slice(-(MAX_LOGS - 1)),
508
+ { time, text: msg, podIndex: null }
509
+ ]);
510
+ };
511
+ const handleEvent = (event) => {
512
+ const time = new Date(event.timestamp).toISOString().slice(11, 19);
513
+ const prefix = `[pod-${event.podIndex}]`;
514
+ let text = null;
515
+ switch (event.type) {
516
+ case "phase_change":
517
+ text = `${prefix} ${event.from} -> ${event.to}`;
518
+ break;
519
+ case "poll_result":
520
+ text = event.foundWork ? `${prefix} Found: ${event.taskId}` : `${prefix} No work`;
521
+ break;
522
+ case "work_start":
523
+ text = `${prefix} Working: ${event.taskDescription?.slice(0, 40)}`;
524
+ break;
525
+ case "work_complete":
526
+ text = `${prefix} Done: ${event.taskId} ($${event.costUsd.toFixed(4)})`;
527
+ break;
528
+ case "work_error":
529
+ text = `${prefix} Error: ${event.error}`;
530
+ break;
531
+ case "tool_use":
532
+ text = `${prefix} -> ${event.toolName}`;
533
+ break;
534
+ case "error":
535
+ text = `${prefix} ERROR: ${event.error}`;
536
+ break;
537
+ case "budget_exceeded":
538
+ text = `${prefix} Budget exceeded!`;
539
+ break;
540
+ }
541
+ if (text) {
542
+ setLogs((prev) => [
543
+ ...prev.slice(-(MAX_LOGS - 1)),
544
+ { time, text, podIndex: event.podIndex }
545
+ ]);
546
+ }
547
+ setState(orchestrator.getState());
548
+ };
549
+ orchestrator.on("log", handleLog);
550
+ orchestrator.on("pod_event", handleEvent);
551
+ return () => {
552
+ clearInterval(refreshInterval);
553
+ orchestrator.off("log", handleLog);
554
+ orchestrator.off("pod_event", handleEvent);
555
+ };
556
+ }, [orchestrator]);
557
+ const visibleLogs = filterByPod ? logs.filter((l) => l.podIndex === null || l.podIndex === selectedPod) : logs;
558
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
559
+ Dashboard,
560
+ {
561
+ state,
562
+ logs: visibleLogs,
563
+ selectedPod,
564
+ budgetGlobal: config.maxBudgetGlobal,
565
+ filterByPod,
566
+ shuttingDown: state.shuttingDown,
567
+ dockerMode: config.docker
568
+ }
569
+ );
570
+ }
571
+ function renderDashboard(orchestrator, config) {
572
+ (0, import_ink6.render)(/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(App, { orchestrator, config }));
573
+ }
574
+ var import_react, import_ink6, import_jsx_runtime6, MAX_LOGS;
575
+ var init_App = __esm({
576
+ "src/ui/App.tsx"() {
577
+ "use strict";
578
+ import_react = require("react");
579
+ import_ink6 = require("ink");
580
+ init_Dashboard();
581
+ import_jsx_runtime6 = require("react/jsx-runtime");
582
+ MAX_LOGS = 200;
583
+ }
584
+ });
585
+
586
+ // src/config.ts
587
+ var import_arg = __toESM(require("arg"));
588
+ var DEFAULTS = {
589
+ agentCount: 2,
590
+ apiUrl: "https://api.actwith.ai",
591
+ maxBudgetPerPod: 2,
592
+ maxBudgetGlobal: 5,
593
+ pollIntervalSec: 30,
594
+ cooldownSec: 2,
595
+ idleMaxTurns: 5,
596
+ activeMaxTurns: 50,
597
+ idleModel: "claude-haiku-4-5-20251001",
598
+ activeModel: "claude-sonnet-4-5-20250929",
599
+ podPrefix: "pod",
600
+ verbose: false,
601
+ docker: false,
602
+ tokenExpiryDays: 1,
603
+ activeChunkSize: 10,
604
+ synthesisCycleInterval: 10,
605
+ activityLogging: true
606
+ };
607
+ function resolveConfig(argv) {
608
+ const args = (0, import_arg.default)(
609
+ {
610
+ "--space": String,
611
+ "--agents": Number,
612
+ "--api-key": String,
613
+ "--api-url": String,
614
+ "--anthropic-key": String,
615
+ "--budget": Number,
616
+ "--budget-per-pod": Number,
617
+ "--poll-interval": Number,
618
+ "--cooldown": Number,
619
+ "--idle-turns": Number,
620
+ "--active-turns": Number,
621
+ "--idle-model": String,
622
+ "--active-model": String,
623
+ "--prefix": String,
624
+ "--verbose": Boolean,
625
+ "--docker": Boolean,
626
+ "--token-expiry": Number,
627
+ "--active-chunk": Number,
628
+ "--synthesis-interval": Number,
629
+ "--no-activity-log": Boolean,
630
+ // Aliases
631
+ "-s": "--space",
632
+ "-n": "--agents",
633
+ "-v": "--verbose",
634
+ "-b": "--budget"
635
+ },
636
+ { argv }
637
+ );
638
+ const spaceId = args["--space"] || process.env.ACTWITH_SPACE_ID || "";
639
+ const apiKey = args["--api-key"] || process.env.ACTWITH_API_KEY || "";
640
+ const anthropicApiKey = args["--anthropic-key"] || process.env.ANTHROPIC_API_KEY || "";
641
+ if (!apiKey) {
642
+ throw new Error(
643
+ "Actwith API key required: use --api-key <key> or set ACTWITH_API_KEY"
644
+ );
645
+ }
646
+ if (!anthropicApiKey) {
647
+ throw new Error(
648
+ "Anthropic API key required: use --anthropic-key <key> or set ANTHROPIC_API_KEY"
649
+ );
650
+ }
651
+ return {
652
+ spaceId,
653
+ apiKey,
654
+ anthropicApiKey,
655
+ apiUrl: args["--api-url"] || process.env.ACTWITH_API_URL || DEFAULTS.apiUrl,
656
+ agentCount: args["--agents"] ?? DEFAULTS.agentCount,
657
+ maxBudgetPerPod: args["--budget-per-pod"] ?? DEFAULTS.maxBudgetPerPod,
658
+ maxBudgetGlobal: args["--budget"] ?? DEFAULTS.maxBudgetGlobal,
659
+ pollIntervalSec: args["--poll-interval"] ?? DEFAULTS.pollIntervalSec,
660
+ cooldownSec: args["--cooldown"] ?? DEFAULTS.cooldownSec,
661
+ idleMaxTurns: args["--idle-turns"] ?? DEFAULTS.idleMaxTurns,
662
+ activeMaxTurns: args["--active-turns"] ?? DEFAULTS.activeMaxTurns,
663
+ idleModel: args["--idle-model"] ?? DEFAULTS.idleModel,
664
+ activeModel: args["--active-model"] ?? DEFAULTS.activeModel,
665
+ podPrefix: args["--prefix"] ?? DEFAULTS.podPrefix,
666
+ verbose: (args["--verbose"] ?? process.env.NO_UI === "1") || DEFAULTS.verbose,
667
+ docker: args["--docker"] ?? DEFAULTS.docker,
668
+ tokenExpiryDays: args["--token-expiry"] ?? DEFAULTS.tokenExpiryDays,
669
+ activeChunkSize: args["--active-chunk"] ?? DEFAULTS.activeChunkSize,
670
+ synthesisCycleInterval: args["--synthesis-interval"] ?? DEFAULTS.synthesisCycleInterval,
671
+ activityLogging: args["--no-activity-log"] ? false : DEFAULTS.activityLogging
672
+ };
673
+ }
674
+ async function resolveDefaultSpace(apiKey, apiUrl) {
675
+ const res = await fetch(`${apiUrl}/v1/contexts?limit=1`, {
676
+ headers: { Authorization: `Bearer ${apiKey}` }
677
+ });
678
+ if (!res.ok) {
679
+ throw new Error(
680
+ `Failed to resolve default space (HTTP ${res.status}). Use --space <id> to specify explicitly.`
681
+ );
682
+ }
683
+ const body = await res.json();
684
+ const spaceId = body.data?.[0]?.id;
685
+ if (!spaceId) {
686
+ throw new Error(
687
+ "No spaces found for this API key. Use --space <id> to specify explicitly."
688
+ );
689
+ }
690
+ return spaceId;
691
+ }
692
+
693
+ // src/orchestrator.ts
694
+ var import_events3 = require("events");
695
+
696
+ // src/identity-manager.ts
697
+ var IdentityManager = class {
698
+ constructor(config) {
699
+ this.identities = [];
700
+ this.config = config;
701
+ }
702
+ /**
703
+ * Create or find pod agents (idempotent via stableKey) and issue ephemeral tokens.
704
+ */
705
+ async createPodIdentities(count) {
706
+ const identities = [];
707
+ for (let i = 0; i < count; i++) {
708
+ const stableKey = `${this.config.podPrefix}-${i}`;
709
+ const identity = await this.createPodIdentity(i, stableKey);
710
+ identities.push(identity);
711
+ }
712
+ this.identities = identities;
713
+ return identities;
714
+ }
715
+ async createPodIdentity(index, stableKey) {
716
+ const agent = await this.request("POST", "/v1/agents", {
717
+ contextId: this.config.spaceId,
718
+ type: "ai",
719
+ stableKey
720
+ });
721
+ const token = await this.request(
722
+ "POST",
723
+ `/v1/agents/${agent.id}/tokens`,
724
+ {
725
+ name: `pod-${index}-ephemeral`,
726
+ description: `Ephemeral token for pod ${index}`,
727
+ expiresInDays: this.config.tokenExpiryDays
728
+ }
729
+ );
730
+ return {
731
+ agentId: agent.id,
732
+ agentName: agent.name,
733
+ apiKey: token.token,
734
+ tokenId: token.id,
735
+ stableKey
736
+ };
737
+ }
738
+ /**
739
+ * Revoke all ephemeral tokens created during this session.
740
+ */
741
+ async cleanup() {
742
+ const results = await Promise.allSettled(
743
+ this.identities.map(
744
+ (id) => this.request(
745
+ "DELETE",
746
+ `/v1/agents/${id.agentId}/tokens/${id.tokenId}`
747
+ ).catch(() => {
748
+ })
749
+ )
750
+ );
751
+ const failed = results.filter((r) => r.status === "rejected");
752
+ if (failed.length > 0) {
753
+ console.error(
754
+ `Warning: Failed to revoke ${failed.length}/${this.identities.length} tokens`
755
+ );
756
+ }
757
+ this.identities = [];
758
+ }
759
+ getIdentities() {
760
+ return [...this.identities];
761
+ }
762
+ async request(method, path, body) {
763
+ const response = await fetch(`${this.config.apiUrl}${path}`, {
764
+ method,
765
+ headers: {
766
+ Authorization: `Bearer ${this.config.apiKey}`,
767
+ "Content-Type": "application/json"
768
+ },
769
+ body: body ? JSON.stringify(body) : void 0
770
+ });
771
+ const data = await response.json();
772
+ if (!data.success || !data.data) {
773
+ throw new Error(
774
+ `API error ${path}: ${data.error?.message ?? "Unknown error"} (${data.error?.code ?? response.status})`
775
+ );
776
+ }
777
+ return data.data;
778
+ }
779
+ };
780
+
781
+ // src/pod.ts
782
+ var import_events2 = require("events");
783
+
784
+ // src/pod-runtime.ts
785
+ var import_claude_agent_sdk = require("@anthropic-ai/claude-agent-sdk");
786
+
787
+ // src/events.ts
788
+ function extractEvents(podIndex, message) {
789
+ const events = [];
790
+ const now = Date.now();
791
+ if (message.type === "assistant") {
792
+ const content = message.message.content;
793
+ if (Array.isArray(content)) {
794
+ for (const block of content) {
795
+ if (block.type === "tool_use") {
796
+ events.push({
797
+ type: "tool_use",
798
+ podIndex,
799
+ toolName: block.name,
800
+ timestamp: now
801
+ });
802
+ } else if (block.type === "text" && block.text) {
803
+ const text = block.text.trim();
804
+ if (text.length > 0) {
805
+ events.push({
806
+ type: "text_output",
807
+ podIndex,
808
+ text: text.length > 200 ? text.slice(0, 200) + "..." : text,
809
+ timestamp: now
810
+ });
811
+ }
812
+ }
813
+ }
814
+ }
815
+ }
816
+ if (message.type === "result") {
817
+ events.push({
818
+ type: "cost_update",
819
+ podIndex,
820
+ costUsd: message.total_cost_usd,
821
+ timestamp: now
822
+ });
823
+ }
824
+ return events;
825
+ }
826
+
827
+ // src/pod-runtime.ts
828
+ init_docker_runtime();
829
+ async function runPodQuery(options) {
830
+ if (options.config.docker) {
831
+ return runDockerPodQuery(options);
832
+ }
833
+ return runAgentSdkQuery(options);
834
+ }
835
+ async function runDockerPodQuery(options) {
836
+ if (!isDockerAvailable()) {
837
+ return {
838
+ success: false,
839
+ resultText: "",
840
+ costUsd: 0,
841
+ numTurns: 0,
842
+ sessionId: null,
843
+ errors: [
844
+ "Docker mode requested but Docker is not available or image 'actwith/claude-test:latest' not found. Build the image with: docker build -t actwith/claude-test -f e2e/docker/Dockerfile e2e/"
845
+ ]
846
+ };
847
+ }
848
+ return runDockerQuery(options);
849
+ }
850
+ async function runAgentSdkQuery(options) {
851
+ const {
852
+ prompt,
853
+ identity,
854
+ config,
855
+ model,
856
+ maxTurns,
857
+ podIndex,
858
+ abortController,
859
+ onEvent
860
+ } = options;
861
+ const errors = [];
862
+ let resultText = "";
863
+ let costUsd = 0;
864
+ let numTurns = 0;
865
+ let sessionId = null;
866
+ try {
867
+ const q = (0, import_claude_agent_sdk.query)({
868
+ prompt,
869
+ options: {
870
+ model,
871
+ maxTurns,
872
+ abortController,
873
+ systemPrompt: {
874
+ type: "preset",
875
+ preset: "claude_code",
876
+ append: `
877
+ You are an autonomous pod agent. Your Actwith agent name is "${identity.agentName}". Work autonomously without asking the user questions.`
878
+ },
879
+ tools: { type: "preset", preset: "claude_code" },
880
+ permissionMode: "bypassPermissions",
881
+ allowDangerouslySkipPermissions: true,
882
+ settingSources: [],
883
+ mcpServers: {
884
+ actwith: {
885
+ command: "npx",
886
+ args: ["-y", "@actwith-ai/mcp-server"],
887
+ env: {
888
+ ACTWITH_API_KEY: identity.apiKey,
889
+ ACTWITH_SPACE_ID: config.spaceId,
890
+ ACTWITH_API_URL: config.apiUrl,
891
+ ACTWITH_HEADLESS: "1"
892
+ }
893
+ }
894
+ },
895
+ env: {
896
+ ...process.env,
897
+ ANTHROPIC_API_KEY: config.anthropicApiKey
898
+ }
899
+ }
900
+ });
901
+ for await (const message of q) {
902
+ if (abortController.signal.aborted) break;
903
+ const events = extractEvents(podIndex, message);
904
+ if (onEvent) {
905
+ for (const event of events) {
906
+ onEvent(event);
907
+ }
908
+ }
909
+ if (message.type === "result") {
910
+ sessionId = message.session_id;
911
+ costUsd = message.total_cost_usd;
912
+ numTurns = message.num_turns;
913
+ if (message.subtype === "success") {
914
+ resultText = message.result;
915
+ } else {
916
+ errors.push(...message.errors);
917
+ }
918
+ }
919
+ }
920
+ return {
921
+ success: errors.length === 0,
922
+ resultText,
923
+ costUsd,
924
+ numTurns,
925
+ sessionId,
926
+ errors
927
+ };
928
+ } catch (err) {
929
+ if (abortController.signal.aborted) {
930
+ return {
931
+ success: false,
932
+ resultText: "",
933
+ costUsd,
934
+ numTurns,
935
+ sessionId,
936
+ errors: ["Aborted"]
937
+ };
938
+ }
939
+ const errorMsg = err instanceof Error ? err.message : String(err);
940
+ return {
941
+ success: false,
942
+ resultText: "",
943
+ costUsd,
944
+ numTurns,
945
+ sessionId,
946
+ errors: [errorMsg]
947
+ };
948
+ }
949
+ }
950
+
951
+ // src/prompts.ts
952
+ function buildIdlePrompt(identity, spaceId) {
953
+ return `You are an autonomous AI worker pod in Actwith space "${spaceId}".
954
+ Your agent name is "${identity.agentName}".
955
+
956
+ Your job: Check if there is work available for you, then report back.
957
+
958
+ Do these steps in order:
959
+ 1. Call "session_restore" to restore any previous session context.
960
+ 2. Call "whats_new" to see your dashboard.
961
+ 3. Call "find_work" to discover tasks matching your skills.
962
+
963
+ After checking, respond with ONLY a JSON object (no markdown, no explanation):
964
+ {"found_work": true/false, "task_id": "...", "task_description": "..."}
965
+
966
+ If no work is found, respond with:
967
+ {"found_work": false, "task_id": null, "task_description": null}
968
+
969
+ Do NOT claim any tasks. Only check and report.
970
+
971
+ ## Communication
972
+
973
+ When messaging teammates, ALWAYS use the "to" field in the "tell" tool with the recipient's name so they get notified.
974
+ Example: tell(to: "patient-badger", topic: "general", message: "Checking in on task progress")
975
+ Without "to", your message is a silent broadcast no one sees.`;
976
+ }
977
+ function buildActivePrompt(identity, spaceId, taskId, taskDescription) {
978
+ return `You are an autonomous AI worker pod in Actwith space "${spaceId}".
979
+ Your agent name is "${identity.agentName}".
980
+
981
+ You have been assigned a task to complete.
982
+
983
+ Task ID: ${taskId}
984
+ Task Description: ${taskDescription}
985
+
986
+ Do these steps:
987
+ 1. Call "recall_expertise" to recall relevant skills and insights for this task.
988
+ 2. Call "claim_task" with task_id "${taskId}" to claim the task.
989
+ 3. Read the full task details with "task_details" for task_id "${taskId}".
990
+ 4. Do the work described in the task. Use all available tools as needed.
991
+ 5. When finished, call "submit_work" with task_id "${taskId}" and a summary of your work.
992
+ 6. Call "reflect" with a brief reflection on what you learned and what went well or poorly.
993
+ 7. Call "session_save" to save your progress.
994
+
995
+ Work autonomously. Be thorough but efficient. If you encounter blockers, document them in your response.
996
+
997
+ ## Communication
998
+
999
+ When messaging teammates, ALWAYS use the "to" field in the "tell" tool with the recipient's name so they get notified.
1000
+ Example: tell(to: "patient-badger", topic: "general", message: "Task completed, ready for review")
1001
+ Without "to", your message is a silent broadcast no one sees.
1002
+
1003
+ ## Subtask Decomposition
1004
+
1005
+ For complex tasks, you can spawn subtasks to parallelize work:
1006
+ - Use "spawn_subtask" with parent_task_id="${taskId}" to create child tasks.
1007
+ - Other agents (or you) can claim and complete subtasks independently.
1008
+ - Maximum depth: 2 levels (subtasks cannot create their own subtasks).
1009
+ - Spawn when: work is parallelizable, different skills needed, divide-and-conquer helps.
1010
+ - Inline when: sequential dependency, small scope, you already have the skills.
1011
+ - Use "list_subtasks" to check on child task progress.`;
1012
+ }
1013
+ function buildContinuationPrompt(identity, spaceId, taskId, turnsUsed, totalTurns) {
1014
+ return `You are an autonomous AI worker pod in Actwith space "${spaceId}".
1015
+ Your agent name is "${identity.agentName}".
1016
+
1017
+ You are continuing work on a task. You have used ${turnsUsed} of ${totalTurns} available turns.
1018
+
1019
+ Task ID: ${taskId}
1020
+
1021
+ Steps:
1022
+ 1. Call "session_restore" to reload your previous context.
1023
+ 2. Continue working on the task from where you left off.
1024
+ 3. If you finish the task, call "submit_work" with task_id "${taskId}" and a summary.
1025
+ 4. Call "reflect" with a brief reflection on what you learned.
1026
+ 5. Call "session_save" to save your progress.
1027
+
1028
+ ## Communication
1029
+
1030
+ When messaging teammates, ALWAYS use the "to" field in the "tell" tool with the recipient's name so they get notified.
1031
+ Example: tell(to: "patient-badger", topic: "general", message: "Task completed, ready for review")
1032
+ Without "to", your message is a silent broadcast no one sees.
1033
+
1034
+ Work autonomously. Be thorough but efficient.`;
1035
+ }
1036
+ function formatAge(seconds) {
1037
+ if (seconds < 60) return `${seconds}s ago`;
1038
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
1039
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
1040
+ return `${Math.floor(seconds / 86400)}d ago`;
1041
+ }
1042
+ function buildNotificationPrompt(identity, spaceId, notifications, totalUnread) {
1043
+ const notifList = notifications.map(
1044
+ (n, i) => `${i + 1}. [${formatAge(n.ageSeconds)}] From "${n.from}" in topic "${n.topic}": "${n.preview}"`
1045
+ ).join("\n");
1046
+ const total = totalUnread ?? notifications.length;
1047
+ const moreNote = total > notifications.length ? `
1048
+ Note: You have ${total - notifications.length} more unread mention(s) that will arrive in subsequent heartbeats.` : "";
1049
+ return `You are an autonomous AI worker pod in Actwith space "${spaceId}".
1050
+ Your agent name is "${identity.agentName}".
1051
+
1052
+ You have been @mentioned in ${notifications.length} notification(s):
1053
+ ${notifList}
1054
+ ${moreNote}
1055
+ Respond to these mentions appropriately:
1056
+ 1. Read the full conversation context using "listen" on the relevant topic(s).
1057
+ 2. Take whatever action the mention is requesting (join a space, review something, answer a question, etc.).
1058
+ 3. Reply using "tell" with the "to" field set to the sender's name, on the same topic. Example: tell(to: "patient-badger", topic: "general", message: "Here's what I found...")
1059
+ 4. Call "session_save" to save your progress.
1060
+
1061
+ IMPORTANT: When using "tell", ALWAYS set the "to" field with the recipient's name so they get notified. Without "to", your message is a silent broadcast no one sees.
1062
+
1063
+ If a mention is more than a few hours old, check if the conversation has moved on before responding.
1064
+ Work autonomously. Be helpful and responsive.`;
1065
+ }
1066
+ function buildShutdownPrompt(identity) {
1067
+ return `You are shutting down. Your agent name is "${identity.agentName}".
1068
+
1069
+ Call "session_save" with a summary of your current session, including:
1070
+ - What tasks you completed
1071
+ - What you were working on
1072
+ - Any notes for next time
1073
+
1074
+ Then respond with: "shutdown complete"`;
1075
+ }
1076
+
1077
+ // src/pod.ts
1078
+ var MIN_POLL_SEC = 10;
1079
+ var MAX_POLL_SEC = 300;
1080
+ var DEFAULT_POLL_SEC = 30;
1081
+ var Pod = class extends import_events2.EventEmitter {
1082
+ constructor(index, identity, config) {
1083
+ super();
1084
+ this.phase = "INITIALIZING";
1085
+ this.shutdownRequested = false;
1086
+ this.sleepTimer = null;
1087
+ this.sleepResolve = null;
1088
+ // Background poller
1089
+ this.pollerTimer = null;
1090
+ this.nextPollSec = DEFAULT_POLL_SEC;
1091
+ this.pendingNotifications = null;
1092
+ // Stats
1093
+ this.totalCostUsd = 0;
1094
+ this.totalTurns = 0;
1095
+ this.pollCount = 0;
1096
+ this.workCount = 0;
1097
+ this.cycleCount = 0;
1098
+ this.currentTaskId = null;
1099
+ this.currentTaskDescription = null;
1100
+ this.lastActivity = "initializing";
1101
+ this.sessionId = null;
1102
+ this.error = null;
1103
+ this.index = index;
1104
+ this.identity = identity;
1105
+ this.config = config;
1106
+ this.abortController = new AbortController();
1107
+ }
1108
+ getState() {
1109
+ return {
1110
+ index: this.index,
1111
+ identity: this.identity,
1112
+ phase: this.phase,
1113
+ currentTaskId: this.currentTaskId,
1114
+ currentTaskDescription: this.currentTaskDescription,
1115
+ totalCostUsd: this.totalCostUsd,
1116
+ totalTurns: this.totalTurns,
1117
+ pollCount: this.pollCount,
1118
+ workCount: this.workCount,
1119
+ lastActivity: this.lastActivity,
1120
+ sessionId: this.sessionId,
1121
+ error: this.error,
1122
+ cycleCount: this.cycleCount,
1123
+ pendingNotifications: this.pendingNotifications
1124
+ };
1125
+ }
1126
+ setPhase(newPhase) {
1127
+ const from = this.phase;
1128
+ this.phase = newPhase;
1129
+ this.emitEvent({
1130
+ type: "phase_change",
1131
+ podIndex: this.index,
1132
+ from,
1133
+ to: newPhase,
1134
+ timestamp: Date.now()
1135
+ });
1136
+ }
1137
+ emitEvent(event) {
1138
+ this.emit("event", event);
1139
+ }
1140
+ /**
1141
+ * Main run loop. Polls for work, does work, repeats until shutdown.
1142
+ */
1143
+ async run() {
1144
+ this.setPhase("IDLE");
1145
+ this.startNotificationPoller();
1146
+ this.postActivityLog("Pod started", "info");
1147
+ while (!this.shutdownRequested) {
1148
+ if (this.totalCostUsd >= this.config.maxBudgetPerPod) {
1149
+ this.emitEvent({
1150
+ type: "budget_exceeded",
1151
+ podIndex: this.index,
1152
+ costUsd: this.totalCostUsd,
1153
+ limit: this.config.maxBudgetPerPod,
1154
+ timestamp: Date.now()
1155
+ });
1156
+ this.postActivityLog("Budget exceeded, stopping", "warn");
1157
+ break;
1158
+ }
1159
+ try {
1160
+ const queued = this.consumePendingNotifications();
1161
+ if (queued) {
1162
+ this.setPhase("ACTIVE");
1163
+ this.lastActivity = `responding to ${queued.notifications.length} notification(s)`;
1164
+ this.postActivityLog(
1165
+ `Handling ${queued.notifications.length} notification(s)`,
1166
+ "info"
1167
+ );
1168
+ await this.handleNotifications(
1169
+ queued.notifications,
1170
+ queued.totalUnread
1171
+ );
1172
+ if (this.shutdownRequested) break;
1173
+ this.setPhase("COOLDOWN");
1174
+ this.lastActivity = "cooling down";
1175
+ await this.sleep(this.config.cooldownSec * 1e3);
1176
+ this.cycleCount++;
1177
+ await this.maybeAutoSynthesize();
1178
+ continue;
1179
+ }
1180
+ const { notifications, totalUnread } = await this.checkNotifications();
1181
+ if (this.shutdownRequested) break;
1182
+ if (notifications.length > 0) {
1183
+ this.setPhase("ACTIVE");
1184
+ this.lastActivity = `responding to ${notifications.length} notification(s)`;
1185
+ this.postActivityLog(
1186
+ `Handling ${notifications.length} notification(s)`,
1187
+ "info"
1188
+ );
1189
+ await this.handleNotifications(notifications, totalUnread);
1190
+ if (this.shutdownRequested) break;
1191
+ this.setPhase("COOLDOWN");
1192
+ this.lastActivity = "cooling down";
1193
+ await this.sleep(this.config.cooldownSec * 1e3);
1194
+ this.cycleCount++;
1195
+ await this.maybeAutoSynthesize();
1196
+ continue;
1197
+ }
1198
+ const preflight = await this.preflightCheck();
1199
+ if (this.shutdownRequested) break;
1200
+ if (!preflight.hasWork) {
1201
+ this.emitEvent({
1202
+ type: "preflight_skip",
1203
+ podIndex: this.index,
1204
+ reason: "no open tasks",
1205
+ timestamp: Date.now()
1206
+ });
1207
+ this.lastActivity = "waiting for work (preflight: nothing to do)";
1208
+ await this.sleep(this.config.pollIntervalSec * 1e3);
1209
+ continue;
1210
+ }
1211
+ this.setPhase("IDLE");
1212
+ this.lastActivity = "polling for work";
1213
+ const poll = await this.idlePoll();
1214
+ if (this.shutdownRequested) break;
1215
+ if (poll.foundWork && poll.taskId && poll.taskDescription) {
1216
+ this.setPhase("ACTIVE");
1217
+ this.currentTaskId = poll.taskId;
1218
+ this.currentTaskDescription = poll.taskDescription;
1219
+ this.lastActivity = `working on ${poll.taskId}`;
1220
+ this.postActivityLog(
1221
+ `Starting work on task ${poll.taskId}: ${poll.taskDescription}`,
1222
+ "info"
1223
+ );
1224
+ await this.activeWork(poll.taskId, poll.taskDescription);
1225
+ this.currentTaskId = null;
1226
+ this.currentTaskDescription = null;
1227
+ if (this.shutdownRequested) break;
1228
+ this.setPhase("COOLDOWN");
1229
+ this.lastActivity = "cooling down";
1230
+ await this.sleep(this.config.cooldownSec * 1e3);
1231
+ this.cycleCount++;
1232
+ await this.maybeAutoSynthesize();
1233
+ } else {
1234
+ this.lastActivity = "waiting for work";
1235
+ await this.sleep(this.config.pollIntervalSec * 1e3);
1236
+ }
1237
+ } catch (err) {
1238
+ if (this.shutdownRequested) break;
1239
+ const msg = err instanceof Error ? err.message : String(err);
1240
+ this.error = msg;
1241
+ this.emitEvent({
1242
+ type: "error",
1243
+ podIndex: this.index,
1244
+ error: msg,
1245
+ timestamp: Date.now()
1246
+ });
1247
+ this.postActivityLog(`Error: ${msg}`, "error");
1248
+ await this.sleep(this.config.pollIntervalSec * 2 * 1e3);
1249
+ }
1250
+ }
1251
+ this.stopNotificationPoller();
1252
+ this.postActivityLog("Pod shutting down", "info");
1253
+ await this.runShutdown();
1254
+ }
1255
+ /**
1256
+ * IDLE poll: use Haiku to check for work.
1257
+ */
1258
+ async idlePoll() {
1259
+ this.pollCount++;
1260
+ this.emitEvent({
1261
+ type: "poll_start",
1262
+ podIndex: this.index,
1263
+ timestamp: Date.now()
1264
+ });
1265
+ const prompt = buildIdlePrompt(this.identity, this.config.spaceId);
1266
+ const result = await runPodQuery({
1267
+ prompt,
1268
+ identity: this.identity,
1269
+ config: this.config,
1270
+ model: this.config.idleModel,
1271
+ maxTurns: this.config.idleMaxTurns,
1272
+ podIndex: this.index,
1273
+ abortController: this.abortController,
1274
+ onEvent: (e) => this.emitEvent(e)
1275
+ });
1276
+ this.totalCostUsd += result.costUsd;
1277
+ this.totalTurns += result.numTurns;
1278
+ if (result.sessionId) this.sessionId = result.sessionId;
1279
+ const poll = parsePollResult(result.resultText);
1280
+ this.emitEvent({
1281
+ type: "poll_result",
1282
+ podIndex: this.index,
1283
+ foundWork: poll.foundWork,
1284
+ taskId: poll.taskId ?? void 0,
1285
+ taskDescription: poll.taskDescription ?? void 0,
1286
+ timestamp: Date.now()
1287
+ });
1288
+ return poll;
1289
+ }
1290
+ /**
1291
+ * ACTIVE work: use Sonnet to claim and complete a task.
1292
+ * When chunking is enabled, splits work into chunks of activeChunkSize turns
1293
+ * and checks for notifications between chunks.
1294
+ */
1295
+ async activeWork(taskId, taskDescription) {
1296
+ this.workCount++;
1297
+ this.emitEvent({
1298
+ type: "work_start",
1299
+ podIndex: this.index,
1300
+ taskId,
1301
+ taskDescription,
1302
+ timestamp: Date.now()
1303
+ });
1304
+ const chunkSize = this.config.activeChunkSize;
1305
+ const totalMaxTurns = this.config.activeMaxTurns;
1306
+ if (chunkSize <= 0 || chunkSize >= totalMaxTurns) {
1307
+ await this.runActiveChunk(taskId, taskDescription, totalMaxTurns, true);
1308
+ return;
1309
+ }
1310
+ let turnsUsed = 0;
1311
+ let chunkIndex = 0;
1312
+ while (turnsUsed < totalMaxTurns && !this.shutdownRequested) {
1313
+ const turnsRemaining = totalMaxTurns - turnsUsed;
1314
+ const turnsThisChunk = Math.min(chunkSize, turnsRemaining);
1315
+ const isFirstChunk = chunkIndex === 0;
1316
+ const prompt = isFirstChunk ? buildActivePrompt(
1317
+ this.identity,
1318
+ this.config.spaceId,
1319
+ taskId,
1320
+ taskDescription
1321
+ ) : buildContinuationPrompt(
1322
+ this.identity,
1323
+ this.config.spaceId,
1324
+ taskId,
1325
+ turnsUsed,
1326
+ totalMaxTurns
1327
+ );
1328
+ const result = await runPodQuery({
1329
+ prompt,
1330
+ identity: this.identity,
1331
+ config: this.config,
1332
+ model: this.config.activeModel,
1333
+ maxTurns: turnsThisChunk,
1334
+ podIndex: this.index,
1335
+ abortController: this.abortController,
1336
+ onEvent: (e) => this.emitEvent(e)
1337
+ });
1338
+ this.totalCostUsd += result.costUsd;
1339
+ this.totalTurns += result.numTurns;
1340
+ turnsUsed += result.numTurns;
1341
+ if (result.sessionId) this.sessionId = result.sessionId;
1342
+ this.emitEvent({
1343
+ type: "active_chunk_complete",
1344
+ podIndex: this.index,
1345
+ taskId,
1346
+ chunkIndex,
1347
+ turnsUsed: result.numTurns,
1348
+ timestamp: Date.now()
1349
+ });
1350
+ chunkIndex++;
1351
+ if (result.numTurns < turnsThisChunk) {
1352
+ if (result.success) {
1353
+ this.emitEvent({
1354
+ type: "work_complete",
1355
+ podIndex: this.index,
1356
+ taskId,
1357
+ costUsd: this.totalCostUsd,
1358
+ numTurns: turnsUsed,
1359
+ timestamp: Date.now()
1360
+ });
1361
+ this.postActivityLog(
1362
+ `Completed task ${taskId} in ${turnsUsed} turns`,
1363
+ "info"
1364
+ );
1365
+ } else {
1366
+ this.emitEvent({
1367
+ type: "work_error",
1368
+ podIndex: this.index,
1369
+ taskId,
1370
+ error: result.errors.join("; "),
1371
+ timestamp: Date.now()
1372
+ });
1373
+ }
1374
+ return;
1375
+ }
1376
+ if (turnsUsed < totalMaxTurns && !this.shutdownRequested) {
1377
+ const queued = this.consumePendingNotifications();
1378
+ if (queued) {
1379
+ this.postActivityLog(
1380
+ `Pausing task to handle ${queued.notifications.length} notification(s)`,
1381
+ "info"
1382
+ );
1383
+ await this.handleNotifications(
1384
+ queued.notifications,
1385
+ queued.totalUnread
1386
+ );
1387
+ }
1388
+ }
1389
+ }
1390
+ this.emitEvent({
1391
+ type: "work_complete",
1392
+ podIndex: this.index,
1393
+ taskId,
1394
+ costUsd: this.totalCostUsd,
1395
+ numTurns: turnsUsed,
1396
+ timestamp: Date.now()
1397
+ });
1398
+ this.postActivityLog(
1399
+ `Completed task ${taskId} (used all ${turnsUsed} turns)`,
1400
+ "info"
1401
+ );
1402
+ }
1403
+ /**
1404
+ * Run a single active work chunk (non-chunked path).
1405
+ */
1406
+ async runActiveChunk(taskId, taskDescription, maxTurns, emitCompletion) {
1407
+ const prompt = buildActivePrompt(
1408
+ this.identity,
1409
+ this.config.spaceId,
1410
+ taskId,
1411
+ taskDescription
1412
+ );
1413
+ const result = await runPodQuery({
1414
+ prompt,
1415
+ identity: this.identity,
1416
+ config: this.config,
1417
+ model: this.config.activeModel,
1418
+ maxTurns,
1419
+ podIndex: this.index,
1420
+ abortController: this.abortController,
1421
+ onEvent: (e) => this.emitEvent(e)
1422
+ });
1423
+ this.totalCostUsd += result.costUsd;
1424
+ this.totalTurns += result.numTurns;
1425
+ if (result.sessionId) this.sessionId = result.sessionId;
1426
+ if (emitCompletion) {
1427
+ if (result.success) {
1428
+ this.emitEvent({
1429
+ type: "work_complete",
1430
+ podIndex: this.index,
1431
+ taskId,
1432
+ costUsd: result.costUsd,
1433
+ numTurns: result.numTurns,
1434
+ timestamp: Date.now()
1435
+ });
1436
+ this.postActivityLog(
1437
+ `Completed task ${taskId} in ${result.numTurns} turns`,
1438
+ "info"
1439
+ );
1440
+ } else {
1441
+ this.emitEvent({
1442
+ type: "work_error",
1443
+ podIndex: this.index,
1444
+ taskId,
1445
+ error: result.errors.join("; "),
1446
+ timestamp: Date.now()
1447
+ });
1448
+ }
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Check for pending @mention notifications via heartbeat API.
1453
+ * Also reads server-guided poll interval hint.
1454
+ */
1455
+ async checkNotifications() {
1456
+ try {
1457
+ const res = await fetch(
1458
+ `${this.config.apiUrl}/v1/agents/${this.identity.agentId}/heartbeat`,
1459
+ {
1460
+ method: "POST",
1461
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
1462
+ }
1463
+ );
1464
+ if (!res.ok) return { notifications: [], totalUnread: 0 };
1465
+ const body = await res.json();
1466
+ if (body.data?.nextPollSec != null) {
1467
+ this.nextPollSec = Math.max(
1468
+ MIN_POLL_SEC,
1469
+ Math.min(MAX_POLL_SEC, body.data.nextPollSec)
1470
+ );
1471
+ }
1472
+ const notifs = body.data?.notifications ?? [];
1473
+ const totalUnread = body.data?.totalUnread ?? notifs.length;
1474
+ if (notifs.length > 0) {
1475
+ this.emitEvent({
1476
+ type: "notification_check",
1477
+ podIndex: this.index,
1478
+ count: notifs.length,
1479
+ timestamp: Date.now()
1480
+ });
1481
+ }
1482
+ return { notifications: notifs, totalUnread };
1483
+ } catch {
1484
+ return { notifications: [], totalUnread: 0 };
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Pre-flight check: query the API for open tasks without invoking the LLM.
1489
+ * Returns quickly and cheaply — if nothing to do, caller can skip the IDLE poll.
1490
+ */
1491
+ async preflightCheck() {
1492
+ try {
1493
+ const res = await fetch(
1494
+ `${this.config.apiUrl}/v1/tasks?context=${this.config.spaceId}&status=open&limit=1`,
1495
+ {
1496
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
1497
+ }
1498
+ );
1499
+ if (!res.ok) {
1500
+ return { hasWork: true };
1501
+ }
1502
+ const body = await res.json();
1503
+ return { hasWork: (body.data?.length ?? 0) > 0 };
1504
+ } catch {
1505
+ return { hasWork: true };
1506
+ }
1507
+ }
1508
+ /**
1509
+ * Handle @mention notifications using Sonnet.
1510
+ */
1511
+ async handleNotifications(notifications, totalUnread) {
1512
+ this.workCount++;
1513
+ this.emitEvent({
1514
+ type: "work_start",
1515
+ podIndex: this.index,
1516
+ taskId: "notifications",
1517
+ taskDescription: `Responding to ${notifications.length} @mention(s)`,
1518
+ timestamp: Date.now()
1519
+ });
1520
+ const prompt = buildNotificationPrompt(
1521
+ this.identity,
1522
+ this.config.spaceId,
1523
+ notifications,
1524
+ totalUnread
1525
+ );
1526
+ const result = await runPodQuery({
1527
+ prompt,
1528
+ identity: this.identity,
1529
+ config: this.config,
1530
+ model: this.config.activeModel,
1531
+ maxTurns: this.config.activeMaxTurns,
1532
+ podIndex: this.index,
1533
+ abortController: this.abortController,
1534
+ onEvent: (e) => this.emitEvent(e)
1535
+ });
1536
+ this.totalCostUsd += result.costUsd;
1537
+ this.totalTurns += result.numTurns;
1538
+ if (result.sessionId) this.sessionId = result.sessionId;
1539
+ if (result.success) {
1540
+ this.emitEvent({
1541
+ type: "work_complete",
1542
+ podIndex: this.index,
1543
+ taskId: "notifications",
1544
+ costUsd: result.costUsd,
1545
+ numTurns: result.numTurns,
1546
+ timestamp: Date.now()
1547
+ });
1548
+ } else {
1549
+ this.emitEvent({
1550
+ type: "work_error",
1551
+ podIndex: this.index,
1552
+ taskId: "notifications",
1553
+ error: result.errors.join("; "),
1554
+ timestamp: Date.now()
1555
+ });
1556
+ }
1557
+ }
1558
+ // ── Background notification poller ──
1559
+ /**
1560
+ * Start background polling for notifications at server-guided intervals.
1561
+ */
1562
+ startNotificationPoller() {
1563
+ if (this.pollerTimer) return;
1564
+ this.scheduleNextPoll();
1565
+ }
1566
+ /**
1567
+ * Stop the background notification poller.
1568
+ */
1569
+ stopNotificationPoller() {
1570
+ if (this.pollerTimer) {
1571
+ clearTimeout(this.pollerTimer);
1572
+ this.pollerTimer = null;
1573
+ }
1574
+ }
1575
+ scheduleNextPoll() {
1576
+ if (this.shutdownRequested) return;
1577
+ this.pollerTimer = setTimeout(async () => {
1578
+ this.pollerTimer = null;
1579
+ if (this.shutdownRequested) return;
1580
+ try {
1581
+ const { notifications, totalUnread } = await this.checkNotifications();
1582
+ if (notifications.length > 0 && !this.pendingNotifications) {
1583
+ this.pendingNotifications = {
1584
+ notifications,
1585
+ totalUnread,
1586
+ detectedAt: Date.now()
1587
+ };
1588
+ this.emitEvent({
1589
+ type: "notification_detected",
1590
+ podIndex: this.index,
1591
+ count: notifications.length,
1592
+ duringPhase: this.phase,
1593
+ timestamp: Date.now()
1594
+ });
1595
+ this.wakeSleep();
1596
+ }
1597
+ } catch {
1598
+ }
1599
+ this.scheduleNextPoll();
1600
+ }, this.nextPollSec * 1e3);
1601
+ }
1602
+ /**
1603
+ * Consume pending notifications, returning them and clearing the queue.
1604
+ */
1605
+ consumePendingNotifications() {
1606
+ const queued = this.pendingNotifications;
1607
+ this.pendingNotifications = null;
1608
+ return queued;
1609
+ }
1610
+ // ── Sleep with interruption ──
1611
+ /**
1612
+ * Wake from sleep early (e.g. when notifications arrive).
1613
+ */
1614
+ wakeSleep() {
1615
+ if (this.sleepResolve) {
1616
+ this.sleepResolve();
1617
+ this.sleepResolve = null;
1618
+ if (this.sleepTimer) {
1619
+ clearTimeout(this.sleepTimer);
1620
+ this.sleepTimer = null;
1621
+ }
1622
+ }
1623
+ }
1624
+ sleep(ms) {
1625
+ return new Promise((resolve) => {
1626
+ if (this.shutdownRequested) {
1627
+ resolve();
1628
+ return;
1629
+ }
1630
+ this.sleepResolve = resolve;
1631
+ this.sleepTimer = setTimeout(() => {
1632
+ this.sleepTimer = null;
1633
+ this.sleepResolve = null;
1634
+ resolve();
1635
+ }, ms);
1636
+ });
1637
+ }
1638
+ // ── Activity logging ──
1639
+ /**
1640
+ * Post an activity log entry to the pod-log topic. Fire-and-forget.
1641
+ */
1642
+ postActivityLog(message, level) {
1643
+ if (!this.config.activityLogging) return;
1644
+ this.emitEvent({
1645
+ type: "activity_log",
1646
+ podIndex: this.index,
1647
+ message,
1648
+ level,
1649
+ timestamp: Date.now()
1650
+ });
1651
+ const body = JSON.stringify({
1652
+ topic: "pod-log",
1653
+ message: `[${this.identity.agentName}] [${level.toUpperCase()}] ${message}`,
1654
+ type: "activity"
1655
+ });
1656
+ fetch(`${this.config.apiUrl}/v1/topics/publish`, {
1657
+ method: "POST",
1658
+ headers: {
1659
+ Authorization: `Bearer ${this.config.apiKey}`,
1660
+ "Content-Type": "application/json",
1661
+ "X-Agent-ID": this.identity.agentId
1662
+ },
1663
+ body
1664
+ }).catch(() => {
1665
+ });
1666
+ }
1667
+ // ── Auto-synthesis ──
1668
+ /**
1669
+ * Trigger deterministic synthesis if cycle count meets the interval.
1670
+ */
1671
+ async maybeAutoSynthesize() {
1672
+ const interval = this.config.synthesisCycleInterval;
1673
+ if (interval <= 0 || this.cycleCount % interval !== 0) return;
1674
+ this.emitEvent({
1675
+ type: "synthesis_start",
1676
+ podIndex: this.index,
1677
+ timestamp: Date.now()
1678
+ });
1679
+ this.postActivityLog(
1680
+ `Auto-synthesis triggered (cycle ${this.cycleCount})`,
1681
+ "info"
1682
+ );
1683
+ let success = false;
1684
+ try {
1685
+ const res = await fetch(`${this.config.apiUrl}/v1/insights/synthesize`, {
1686
+ method: "POST",
1687
+ headers: {
1688
+ Authorization: `Bearer ${this.config.apiKey}`,
1689
+ "Content-Type": "application/json",
1690
+ "X-Agent-ID": this.identity.agentId
1691
+ },
1692
+ body: JSON.stringify({ mode: "deterministic" })
1693
+ });
1694
+ success = res.ok;
1695
+ } catch {
1696
+ }
1697
+ this.emitEvent({
1698
+ type: "synthesis_complete",
1699
+ podIndex: this.index,
1700
+ success,
1701
+ timestamp: Date.now()
1702
+ });
1703
+ }
1704
+ // ── Shutdown ──
1705
+ /**
1706
+ * Run the shutdown prompt to save session before stopping.
1707
+ */
1708
+ async runShutdown() {
1709
+ this.setPhase("SHUTTING_DOWN");
1710
+ this.emitEvent({
1711
+ type: "shutdown_start",
1712
+ podIndex: this.index,
1713
+ timestamp: Date.now()
1714
+ });
1715
+ const shutdownAbort = new AbortController();
1716
+ try {
1717
+ const prompt = buildShutdownPrompt(this.identity);
1718
+ await runPodQuery({
1719
+ prompt,
1720
+ identity: this.identity,
1721
+ config: this.config,
1722
+ model: this.config.idleModel,
1723
+ // Use cheap model for shutdown
1724
+ maxTurns: 3,
1725
+ podIndex: this.index,
1726
+ abortController: shutdownAbort,
1727
+ onEvent: (e) => this.emitEvent(e)
1728
+ });
1729
+ } catch {
1730
+ }
1731
+ this.setPhase("STOPPED");
1732
+ this.lastActivity = "stopped";
1733
+ this.emitEvent({
1734
+ type: "shutdown_complete",
1735
+ podIndex: this.index,
1736
+ timestamp: Date.now()
1737
+ });
1738
+ }
1739
+ /**
1740
+ * Request graceful shutdown.
1741
+ */
1742
+ shutdown() {
1743
+ this.shutdownRequested = true;
1744
+ this.abortController.abort();
1745
+ this.stopNotificationPoller();
1746
+ this.wakeSleep();
1747
+ }
1748
+ };
1749
+ function parsePollResult(text) {
1750
+ const noWork = {
1751
+ foundWork: false,
1752
+ taskId: null,
1753
+ taskDescription: null
1754
+ };
1755
+ if (!text) return noWork;
1756
+ try {
1757
+ const jsonMatch = text.match(/\{[\s\S]*"found_work"[\s\S]*\}/);
1758
+ if (!jsonMatch) return noWork;
1759
+ const parsed = JSON.parse(jsonMatch[0]);
1760
+ return {
1761
+ foundWork: Boolean(parsed.found_work),
1762
+ taskId: parsed.task_id || null,
1763
+ taskDescription: parsed.task_description || null
1764
+ };
1765
+ } catch {
1766
+ return noWork;
1767
+ }
1768
+ }
1769
+
1770
+ // src/orchestrator.ts
1771
+ var Orchestrator = class extends import_events3.EventEmitter {
1772
+ constructor(config) {
1773
+ super();
1774
+ this.pods = [];
1775
+ this.shuttingDown = false;
1776
+ this.startedAt = Date.now();
1777
+ this.config = config;
1778
+ this.identityManager = new IdentityManager(config);
1779
+ }
1780
+ getState() {
1781
+ return {
1782
+ pods: this.pods.map((p) => p.getState()),
1783
+ globalCostUsd: this.getGlobalCost(),
1784
+ startedAt: this.startedAt,
1785
+ shuttingDown: this.shuttingDown
1786
+ };
1787
+ }
1788
+ getGlobalCost() {
1789
+ return this.pods.reduce((sum, p) => sum + p.totalCostUsd, 0);
1790
+ }
1791
+ /**
1792
+ * Initialize identities, spawn pods, and run until shutdown.
1793
+ */
1794
+ async run() {
1795
+ this.startedAt = Date.now();
1796
+ this.emit("log", `Creating ${this.config.agentCount} pod identities...`);
1797
+ const identities = await this.identityManager.createPodIdentities(
1798
+ this.config.agentCount
1799
+ );
1800
+ for (const id of identities) {
1801
+ this.emit(
1802
+ "log",
1803
+ ` Pod ${id.stableKey}: agent "${id.agentName}" (${id.agentId})`
1804
+ );
1805
+ }
1806
+ this.pods = identities.map((identity, i) => {
1807
+ const pod = new Pod(i, identity, this.config);
1808
+ pod.on("event", (event) => {
1809
+ this.emit("pod_event", event);
1810
+ if (event.type === "cost_update") {
1811
+ const globalCost = this.getGlobalCost();
1812
+ if (globalCost >= this.config.maxBudgetGlobal && !this.shuttingDown) {
1813
+ this.emit(
1814
+ "log",
1815
+ `Global budget reached ($${globalCost.toFixed(4)} >= $${this.config.maxBudgetGlobal.toFixed(2)}). Shutting down all pods.`
1816
+ );
1817
+ this.shutdown();
1818
+ }
1819
+ }
1820
+ });
1821
+ return pod;
1822
+ });
1823
+ this.emit("log", `Starting ${this.pods.length} pods...`);
1824
+ try {
1825
+ await Promise.allSettled(this.pods.map((pod) => pod.run()));
1826
+ } finally {
1827
+ this.emit("log", "Revoking ephemeral tokens...");
1828
+ await this.identityManager.cleanup();
1829
+ this.emit("log", "Shutdown complete.");
1830
+ }
1831
+ }
1832
+ /**
1833
+ * Gracefully shut down all pods.
1834
+ */
1835
+ shutdown() {
1836
+ if (this.shuttingDown) return;
1837
+ this.shuttingDown = true;
1838
+ this.emit("log", "Shutting down all pods...");
1839
+ for (const pod of this.pods) {
1840
+ pod.shutdown();
1841
+ }
1842
+ }
1843
+ };
1844
+
1845
+ // src/cli.ts
1846
+ function printHelp() {
1847
+ console.log(`
1848
+ actwith-pods - Launch autonomous AI agent pods in your Actwith space
1849
+
1850
+ Usage:
1851
+ actwith-pods [options]
1852
+
1853
+ Options:
1854
+ --space, -s <id> Actwith space ID (default: your personal space)
1855
+ --agents, -n <count> Number of pods to spawn (default: 2)
1856
+ --budget, -b <usd> Global budget limit in USD (default: 5.00)
1857
+ --budget-per-pod <usd> Per-pod budget limit in USD (default: 2.00)
1858
+ --poll-interval <sec> Seconds between idle polls (default: 30)
1859
+ --cooldown <sec> Seconds between tasks (default: 2)
1860
+ --idle-turns <n> Max turns for idle poll (default: 5)
1861
+ --active-turns <n> Max turns for active work (default: 50)
1862
+ --idle-model <model> Model for idle polls (default: claude-haiku-4-5-20251001)
1863
+ --active-model <model> Model for active work (default: claude-sonnet-4-5-20250929)
1864
+ --prefix <name> Stable key prefix (default: pod)
1865
+ --verbose, -v Use console output instead of dashboard
1866
+ --docker Run pods in Docker containers (requires actwith/claude-test image)
1867
+ --token-expiry <days> Ephemeral token expiry (default: 1)
1868
+
1869
+ Environment:
1870
+ ACTWITH_API_KEY Actwith API key
1871
+ ACTWITH_SPACE_ID Default space ID
1872
+ ACTWITH_API_URL API base URL
1873
+ ANTHROPIC_API_KEY Anthropic API key
1874
+ NO_UI=1 Force console mode
1875
+ `);
1876
+ }
1877
+ async function main(argv) {
1878
+ if (argv.includes("--help") || argv.includes("-h")) {
1879
+ printHelp();
1880
+ process.exit(0);
1881
+ }
1882
+ let config;
1883
+ try {
1884
+ config = resolveConfig(argv);
1885
+ } catch (err) {
1886
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
1887
+ console.error("Run with --help for usage information.");
1888
+ process.exit(1);
1889
+ }
1890
+ if (!config.spaceId) {
1891
+ try {
1892
+ console.log("No space specified, resolving default space...");
1893
+ config.spaceId = await resolveDefaultSpace(config.apiKey, config.apiUrl);
1894
+ console.log(`Using space: ${config.spaceId}`);
1895
+ } catch (err) {
1896
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
1897
+ process.exit(1);
1898
+ }
1899
+ }
1900
+ if (config.docker) {
1901
+ const { isDockerAvailable: isDockerAvailable2 } = await Promise.resolve().then(() => (init_docker_runtime(), docker_runtime_exports));
1902
+ if (!isDockerAvailable2()) {
1903
+ console.error(
1904
+ "Error: Docker mode requested but Docker is not available or image 'actwith/claude-test:latest' not found."
1905
+ );
1906
+ console.error(
1907
+ "Build the image with: docker build -t actwith/claude-test -f e2e/docker/Dockerfile e2e/"
1908
+ );
1909
+ process.exit(1);
1910
+ }
1911
+ console.log("Docker mode enabled.");
1912
+ }
1913
+ const orchestrator = new Orchestrator(config);
1914
+ if (config.verbose) {
1915
+ wireConsoleOutput(orchestrator);
1916
+ } else {
1917
+ try {
1918
+ const { renderDashboard: renderDashboard2 } = await Promise.resolve().then(() => (init_App(), App_exports));
1919
+ renderDashboard2(orchestrator, config);
1920
+ } catch {
1921
+ console.log("Ink dashboard unavailable, falling back to console mode.");
1922
+ wireConsoleOutput(orchestrator);
1923
+ }
1924
+ }
1925
+ let sigintCount = 0;
1926
+ process.on("SIGINT", () => {
1927
+ sigintCount++;
1928
+ if (sigintCount === 1) {
1929
+ console.log("\nGraceful shutdown requested (Ctrl+C again to force)...");
1930
+ orchestrator.shutdown();
1931
+ } else {
1932
+ console.log("\nForce shutdown.");
1933
+ process.exit(1);
1934
+ }
1935
+ });
1936
+ try {
1937
+ await orchestrator.run();
1938
+ } catch (err) {
1939
+ console.error(`Fatal: ${err instanceof Error ? err.message : err}`);
1940
+ process.exit(1);
1941
+ }
1942
+ const state = orchestrator.getState();
1943
+ console.log("\n--- Session Summary ---");
1944
+ console.log(`Total cost: $${state.globalCostUsd.toFixed(4)}`);
1945
+ console.log(
1946
+ `Duration: ${Math.round((Date.now() - state.startedAt) / 1e3)}s`
1947
+ );
1948
+ for (const pod of state.pods) {
1949
+ console.log(
1950
+ ` Pod ${pod.index} (${pod.identity?.agentName ?? "unknown"}): ${pod.workCount} tasks, $${pod.totalCostUsd.toFixed(4)}, ${pod.totalTurns} turns`
1951
+ );
1952
+ }
1953
+ }
1954
+ function wireConsoleOutput(orchestrator) {
1955
+ orchestrator.on("log", (msg) => {
1956
+ console.log(`[pods] ${msg}`);
1957
+ });
1958
+ orchestrator.on("pod_event", (event) => {
1959
+ const prefix = `[pod-${event.podIndex}]`;
1960
+ const time = new Date(event.timestamp).toISOString().slice(11, 19);
1961
+ switch (event.type) {
1962
+ case "phase_change":
1963
+ console.log(`${time} ${prefix} ${event.from} -> ${event.to}`);
1964
+ break;
1965
+ case "notification_check":
1966
+ console.log(
1967
+ `${time} ${prefix} Received ${event.count} @mention notification(s)`
1968
+ );
1969
+ break;
1970
+ case "poll_start":
1971
+ console.log(`${time} ${prefix} Polling for work...`);
1972
+ break;
1973
+ case "poll_result":
1974
+ if (event.foundWork) {
1975
+ console.log(
1976
+ `${time} ${prefix} Found work: ${event.taskId} - ${event.taskDescription}`
1977
+ );
1978
+ } else {
1979
+ console.log(`${time} ${prefix} No work found`);
1980
+ }
1981
+ break;
1982
+ case "work_start":
1983
+ console.log(
1984
+ `${time} ${prefix} Starting task ${event.taskId}: ${event.taskDescription}`
1985
+ );
1986
+ break;
1987
+ case "work_complete":
1988
+ console.log(
1989
+ `${time} ${prefix} Completed ${event.taskId} ($${event.costUsd.toFixed(4)}, ${event.numTurns} turns)`
1990
+ );
1991
+ break;
1992
+ case "work_error":
1993
+ console.log(
1994
+ `${time} ${prefix} Task ${event.taskId} failed: ${event.error}`
1995
+ );
1996
+ break;
1997
+ case "tool_use":
1998
+ console.log(`${time} ${prefix} Tool: ${event.toolName}`);
1999
+ break;
2000
+ case "cost_update":
2001
+ break;
2002
+ case "error":
2003
+ console.error(`${time} ${prefix} ERROR: ${event.error}`);
2004
+ break;
2005
+ case "budget_exceeded":
2006
+ console.log(
2007
+ `${time} ${prefix} Budget exceeded: $${event.costUsd.toFixed(4)} >= $${event.limit.toFixed(2)}`
2008
+ );
2009
+ break;
2010
+ case "shutdown_start":
2011
+ console.log(`${time} ${prefix} Shutting down...`);
2012
+ break;
2013
+ case "shutdown_complete":
2014
+ console.log(`${time} ${prefix} Stopped`);
2015
+ break;
2016
+ }
2017
+ });
2018
+ }
2019
+
2020
+ // src/index.ts
2021
+ main(process.argv.slice(2));