@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.d.ts +2 -0
- package/dist/index.js +2021 -0
- package/package.json +43 -0
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));
|