wayang-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/cli.js +4407 -0
- package/package.json +73 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import * as path4 from "path";
|
|
5
|
+
import meow from "meow";
|
|
6
|
+
|
|
7
|
+
// src/infra/logger.ts
|
|
8
|
+
import pino from "pino";
|
|
9
|
+
function createLogger(level = "info", logFilePath) {
|
|
10
|
+
const opts = {
|
|
11
|
+
level,
|
|
12
|
+
serializers: {
|
|
13
|
+
err: pino.stdSerializers.err,
|
|
14
|
+
error: pino.stdSerializers.err
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
if (logFilePath) {
|
|
18
|
+
return pino(opts, pino.transport({
|
|
19
|
+
target: "pino-pretty",
|
|
20
|
+
options: {
|
|
21
|
+
destination: logFilePath,
|
|
22
|
+
colorize: false,
|
|
23
|
+
singleLine: true,
|
|
24
|
+
ignore: "pid,hostname",
|
|
25
|
+
translateTime: "SYS:HH:MM:ss.l"
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
if (level === "silent") {
|
|
30
|
+
return pino(opts, pino.destination("/dev/null"));
|
|
31
|
+
}
|
|
32
|
+
return pino(opts, pino.transport({ target: "pino-pretty", options: { colorize: true, destination: 2 } }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/infra/system-context.ts
|
|
36
|
+
var SystemContext = class {
|
|
37
|
+
sessionId;
|
|
38
|
+
sessionDir;
|
|
39
|
+
workspaceDir;
|
|
40
|
+
startedAt;
|
|
41
|
+
logLevel;
|
|
42
|
+
controllerProvider;
|
|
43
|
+
workerProvider;
|
|
44
|
+
maxConcurrency;
|
|
45
|
+
logger;
|
|
46
|
+
abortController;
|
|
47
|
+
constructor(config, sessionId, sessionDir, workspaceDir2, logLevel) {
|
|
48
|
+
this.sessionId = sessionId;
|
|
49
|
+
this.sessionDir = sessionDir;
|
|
50
|
+
this.workspaceDir = workspaceDir2;
|
|
51
|
+
this.startedAt = Date.now();
|
|
52
|
+
this.logLevel = logLevel ?? "info";
|
|
53
|
+
this.abortController = new AbortController();
|
|
54
|
+
this.controllerProvider = config.providers[config.controller.provider];
|
|
55
|
+
this.workerProvider = config.providers[config.worker.provider];
|
|
56
|
+
this.maxConcurrency = config.worker.maxConcurrency;
|
|
57
|
+
this.logger = createLogger(this.logLevel, `${sessionDir}/wayang.log`).child({
|
|
58
|
+
session: sessionId
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/infra/event-bus.ts
|
|
64
|
+
var EventBus = class {
|
|
65
|
+
listeners = /* @__PURE__ */ new Map();
|
|
66
|
+
on(event, listener) {
|
|
67
|
+
if (!this.listeners.has(event)) {
|
|
68
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
69
|
+
}
|
|
70
|
+
this.listeners.get(event).add(listener);
|
|
71
|
+
return () => this.listeners.get(event)?.delete(listener);
|
|
72
|
+
}
|
|
73
|
+
emit(event, ...args) {
|
|
74
|
+
this.listeners.get(event)?.forEach((fn) => fn(...args));
|
|
75
|
+
}
|
|
76
|
+
removeAll() {
|
|
77
|
+
this.listeners.clear();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/services/task/task-pool.ts
|
|
82
|
+
var TaskPool = class {
|
|
83
|
+
constructor(state, eventBus, logger) {
|
|
84
|
+
this.state = state;
|
|
85
|
+
this.eventBus = eventBus;
|
|
86
|
+
this.logger = logger;
|
|
87
|
+
}
|
|
88
|
+
state;
|
|
89
|
+
eventBus;
|
|
90
|
+
logger;
|
|
91
|
+
/** Expose state for UI subscription via useWayangState. */
|
|
92
|
+
get stateRef() {
|
|
93
|
+
return this.state;
|
|
94
|
+
}
|
|
95
|
+
add(task) {
|
|
96
|
+
this.state.append("tasks.pending", task);
|
|
97
|
+
this.eventBus.emit("task:added", task);
|
|
98
|
+
this.logger.info({ taskId: task.id }, "Task added");
|
|
99
|
+
}
|
|
100
|
+
get(id) {
|
|
101
|
+
const all = [
|
|
102
|
+
...this.state.get("tasks.pending"),
|
|
103
|
+
...this.state.get("tasks.running"),
|
|
104
|
+
...this.state.get("tasks.history")
|
|
105
|
+
];
|
|
106
|
+
return all.find((t) => t.id === id);
|
|
107
|
+
}
|
|
108
|
+
moveToRunning(id, workerSessionId) {
|
|
109
|
+
const pending = [...this.state.get("tasks.pending")];
|
|
110
|
+
const idx = pending.findIndex((t) => t.id === id);
|
|
111
|
+
if (idx === -1) {
|
|
112
|
+
this.logger.warn({ taskId: id }, "Task not found in pending");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const [task] = pending.splice(idx, 1);
|
|
116
|
+
const updated = {
|
|
117
|
+
...task,
|
|
118
|
+
status: "running",
|
|
119
|
+
startedAt: Date.now(),
|
|
120
|
+
workerSessionId
|
|
121
|
+
};
|
|
122
|
+
this.state.set("tasks.pending", pending);
|
|
123
|
+
this.state.append("tasks.running", updated);
|
|
124
|
+
this.logger.info({ taskId: id, workerId: workerSessionId }, "Task moved to running");
|
|
125
|
+
}
|
|
126
|
+
complete(id, result) {
|
|
127
|
+
this.moveToHistory(id, "completed", { result });
|
|
128
|
+
this.eventBus.emit("task:completed", { taskId: id });
|
|
129
|
+
this.logger.info({ taskId: id }, "Task completed");
|
|
130
|
+
}
|
|
131
|
+
fail(id, error) {
|
|
132
|
+
this.moveToHistory(id, "failed", { error });
|
|
133
|
+
this.eventBus.emit("task:failed", { taskId: id, error });
|
|
134
|
+
this.logger.info({ taskId: id, error }, "Task failed");
|
|
135
|
+
}
|
|
136
|
+
cancel(id) {
|
|
137
|
+
const running = this.state.get("tasks.running");
|
|
138
|
+
const runIdx = running.findIndex((t) => t.id === id);
|
|
139
|
+
if (runIdx !== -1) {
|
|
140
|
+
this.moveToHistory(id, "cancelled", {});
|
|
141
|
+
this.eventBus.emit("task:cancelled", { taskId: id });
|
|
142
|
+
this.logger.info({ taskId: id }, "Running task cancelled");
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
const pending = [...this.state.get("tasks.pending")];
|
|
146
|
+
const pendIdx = pending.findIndex((t) => t.id === id);
|
|
147
|
+
if (pendIdx !== -1) {
|
|
148
|
+
const [task] = pending.splice(pendIdx, 1);
|
|
149
|
+
this.state.set("tasks.pending", pending);
|
|
150
|
+
this.state.append("tasks.history", {
|
|
151
|
+
...task,
|
|
152
|
+
status: "cancelled",
|
|
153
|
+
completedAt: Date.now()
|
|
154
|
+
});
|
|
155
|
+
this.eventBus.emit("task:cancelled", { taskId: id });
|
|
156
|
+
this.logger.info({ taskId: id }, "Pending task cancelled");
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
this.logger.warn({ taskId: id }, "Task not found for cancel");
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
/** Update a pending task's description or priority. */
|
|
163
|
+
updatePending(id, updates) {
|
|
164
|
+
const pending = [...this.state.get("tasks.pending")];
|
|
165
|
+
const idx = pending.findIndex((t) => t.id === id);
|
|
166
|
+
if (idx === -1) {
|
|
167
|
+
this.logger.warn({ taskId: id }, "Task not found in pending for update");
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
pending[idx] = { ...pending[idx], ...updates };
|
|
171
|
+
this.state.set("tasks.pending", pending);
|
|
172
|
+
this.logger.info({ taskId: id, updates }, "Pending task updated");
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
list(status) {
|
|
176
|
+
if (!status) {
|
|
177
|
+
return [
|
|
178
|
+
...this.state.get("tasks.pending"),
|
|
179
|
+
...this.state.get("tasks.running"),
|
|
180
|
+
...this.state.get("tasks.history")
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
if (status === "pending") return [...this.state.get("tasks.pending")];
|
|
184
|
+
if (status === "running") return [...this.state.get("tasks.running")];
|
|
185
|
+
return this.state.get("tasks.history").filter((t) => t.status === status);
|
|
186
|
+
}
|
|
187
|
+
hasPending() {
|
|
188
|
+
return this.state.get("tasks.pending").length > 0;
|
|
189
|
+
}
|
|
190
|
+
/** Pick the highest priority task without removing it. */
|
|
191
|
+
peekHighestPriority() {
|
|
192
|
+
const pending = this.state.get("tasks.pending");
|
|
193
|
+
if (pending.length === 0) return null;
|
|
194
|
+
const highIdx = pending.findIndex((t) => t.priority === "high");
|
|
195
|
+
const targetIdx = highIdx !== -1 ? highIdx : 0;
|
|
196
|
+
return pending[targetIdx];
|
|
197
|
+
}
|
|
198
|
+
getRunningCount() {
|
|
199
|
+
return this.state.get("tasks.running").length;
|
|
200
|
+
}
|
|
201
|
+
async restore() {
|
|
202
|
+
await this.state.restore();
|
|
203
|
+
}
|
|
204
|
+
moveToHistory(id, status, extra) {
|
|
205
|
+
const running = this.state.get("tasks.running");
|
|
206
|
+
const idx = running.findIndex((t) => t.id === id);
|
|
207
|
+
if (idx === -1) {
|
|
208
|
+
this.logger.warn({ taskId: id }, "Task not found in running");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const updated = [...this.state.get("tasks.running")];
|
|
212
|
+
const [task] = updated.splice(idx, 1);
|
|
213
|
+
this.state.set("tasks.running", updated);
|
|
214
|
+
this.state.append("tasks.history", {
|
|
215
|
+
...task,
|
|
216
|
+
...extra,
|
|
217
|
+
status,
|
|
218
|
+
completedAt: Date.now()
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// src/services/signal/signal-queue.ts
|
|
224
|
+
var SignalQueue = class {
|
|
225
|
+
constructor(state, logger) {
|
|
226
|
+
this.state = state;
|
|
227
|
+
this.logger = logger;
|
|
228
|
+
const sigs = state.get("signals");
|
|
229
|
+
this.counter = sigs.reduce((max, s) => {
|
|
230
|
+
const n = parseInt(s.id.replace("sig-", ""), 10);
|
|
231
|
+
return isNaN(n) ? max : Math.max(max, n);
|
|
232
|
+
}, 0);
|
|
233
|
+
}
|
|
234
|
+
state;
|
|
235
|
+
logger;
|
|
236
|
+
resolveWait = null;
|
|
237
|
+
/** Expose state for UI subscription via useWayangState. */
|
|
238
|
+
get stateRef() {
|
|
239
|
+
return this.state;
|
|
240
|
+
}
|
|
241
|
+
counter;
|
|
242
|
+
enqueue(sig) {
|
|
243
|
+
const full = {
|
|
244
|
+
...sig,
|
|
245
|
+
id: `sig-${++this.counter}`,
|
|
246
|
+
status: "unread",
|
|
247
|
+
timestamp: Date.now()
|
|
248
|
+
};
|
|
249
|
+
if (sig.source === "worker" && sig.type === "progress") {
|
|
250
|
+
const { workerId } = sig.payload;
|
|
251
|
+
if (workerId) {
|
|
252
|
+
const allSigs = this.state.get("signals");
|
|
253
|
+
let changed = false;
|
|
254
|
+
for (const old of allSigs) {
|
|
255
|
+
if (old.status === "unread" && old.source === "worker" && old.type === "progress" && old.payload.workerId === workerId) {
|
|
256
|
+
old.status = "discarded";
|
|
257
|
+
changed = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (changed) {
|
|
261
|
+
this.state.set("signals", [...allSigs]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.state.append("signals", full);
|
|
266
|
+
this.logger.debug({ sigId: full.id, type: full.type }, "Signal enqueued");
|
|
267
|
+
if (this.resolveWait) {
|
|
268
|
+
this.resolveWait();
|
|
269
|
+
this.resolveWait = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
dequeueUnread() {
|
|
273
|
+
const all = this.state.get("signals");
|
|
274
|
+
const unread = all.filter((s) => s.status === "unread");
|
|
275
|
+
this.logger.debug({ total: all.length, unreadCount: unread.length, unreadIds: unread.map((s) => s.id), allStatuses: all.map((s) => `${s.id}=${s.status}`) }, "dequeueUnread");
|
|
276
|
+
for (const sig of unread) {
|
|
277
|
+
sig.status = "read";
|
|
278
|
+
}
|
|
279
|
+
if (unread.length > 0) {
|
|
280
|
+
this.state.set("signals", [...all]);
|
|
281
|
+
}
|
|
282
|
+
return unread;
|
|
283
|
+
}
|
|
284
|
+
waitForSignal() {
|
|
285
|
+
const unread = this.state.get("signals").filter((s) => s.status === "unread");
|
|
286
|
+
if (unread.length > 0) return Promise.resolve();
|
|
287
|
+
return new Promise((resolve4) => {
|
|
288
|
+
this.resolveWait = () => {
|
|
289
|
+
this.logger.debug("waitForSignal resolved");
|
|
290
|
+
resolve4();
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async restore() {
|
|
295
|
+
await this.state.restore();
|
|
296
|
+
const sigs = this.state.get("signals");
|
|
297
|
+
this.counter = sigs.reduce((max, s) => {
|
|
298
|
+
const n = parseInt(s.id.replace("sig-", ""), 10);
|
|
299
|
+
return isNaN(n) ? max : Math.max(max, n);
|
|
300
|
+
}, 0);
|
|
301
|
+
}
|
|
302
|
+
/** Get all unread signals without modifying state. Used by UI to display pending signals. */
|
|
303
|
+
getUnreadSignals() {
|
|
304
|
+
return this.state.get("signals").filter((s) => s.status === "unread");
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Query signals by filter criteria.
|
|
308
|
+
* Returns a filtered list without modifying state.
|
|
309
|
+
*/
|
|
310
|
+
query(filter) {
|
|
311
|
+
const all = this.state.get("signals");
|
|
312
|
+
return all.filter((sig) => {
|
|
313
|
+
if (filter.status !== void 0 && sig.status !== filter.status) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
if (filter.source !== void 0 && sig.source !== filter.source) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if (filter.type !== void 0 && sig.type !== filter.type) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// src/utils/llm-error.ts
|
|
328
|
+
function formatLlmError(err) {
|
|
329
|
+
if (!(err instanceof Error)) return String(err);
|
|
330
|
+
const name = err.name ?? err.constructor?.name ?? "Error";
|
|
331
|
+
if (name === "AI_RetryError" || name === "RetryError") {
|
|
332
|
+
const retryErr = err;
|
|
333
|
+
const lastError = retryErr.lastError;
|
|
334
|
+
if (lastError) {
|
|
335
|
+
const inner = formatApiCallError(lastError);
|
|
336
|
+
const attempts = retryErr.errors?.length ?? "?";
|
|
337
|
+
return `LLM request failed after ${attempts} attempts: ${inner}`;
|
|
338
|
+
}
|
|
339
|
+
return `LLM request failed: ${err.message}`;
|
|
340
|
+
}
|
|
341
|
+
if (name === "AI_APICallError" || name === "APICallError") {
|
|
342
|
+
return `LLM API error: ${formatApiCallError(err)}`;
|
|
343
|
+
}
|
|
344
|
+
return err.message || String(err);
|
|
345
|
+
}
|
|
346
|
+
function formatApiCallError(err) {
|
|
347
|
+
const status = err.statusCode;
|
|
348
|
+
const body = err.responseBody;
|
|
349
|
+
let providerMsg = "";
|
|
350
|
+
if (typeof body === "string" && body) {
|
|
351
|
+
try {
|
|
352
|
+
const parsed = JSON.parse(body);
|
|
353
|
+
providerMsg = parsed?.error?.message ?? parsed?.message ?? "";
|
|
354
|
+
} catch {
|
|
355
|
+
providerMsg = body.slice(0, 200);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (status && providerMsg) {
|
|
359
|
+
return `[${status}] ${providerMsg}`;
|
|
360
|
+
}
|
|
361
|
+
if (status) {
|
|
362
|
+
return `[${status}] ${err.message ?? "Unknown error"}`;
|
|
363
|
+
}
|
|
364
|
+
return providerMsg || err.message || String(err);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/utils/id.ts
|
|
368
|
+
function generateId(prefix) {
|
|
369
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
370
|
+
}
|
|
371
|
+
function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
372
|
+
const Y = date.getFullYear();
|
|
373
|
+
const M = String(date.getMonth() + 1).padStart(2, "0");
|
|
374
|
+
const D = String(date.getDate()).padStart(2, "0");
|
|
375
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
376
|
+
const m = String(date.getMinutes()).padStart(2, "0");
|
|
377
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
378
|
+
return `${Y}${M}${D}-${h}${m}${s}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/services/agents/worker-defaults.ts
|
|
382
|
+
var PUPPET_DEFAULTS = {
|
|
383
|
+
/** Display name. */
|
|
384
|
+
label: "puppet",
|
|
385
|
+
/** Emoji icon for UI display. */
|
|
386
|
+
emoji: "\u{1F9F8}",
|
|
387
|
+
// 🧸
|
|
388
|
+
/** Description shown to the Controller LLM. */
|
|
389
|
+
description: "Built-in worker for general tasks using shell commands and file I/O.",
|
|
390
|
+
/** Capability tags. */
|
|
391
|
+
capabilities: ["shell/bash", "file-io", "general"]
|
|
392
|
+
};
|
|
393
|
+
function getWorkerMeta(workerType, workerConfigs) {
|
|
394
|
+
const type = workerType ?? "puppet";
|
|
395
|
+
if (type === "puppet") {
|
|
396
|
+
return { label: PUPPET_DEFAULTS.label, emoji: PUPPET_DEFAULTS.emoji };
|
|
397
|
+
}
|
|
398
|
+
const config = workerConfigs?.[type];
|
|
399
|
+
return {
|
|
400
|
+
label: type,
|
|
401
|
+
emoji: config?.emoji ?? "\u{1F916}"
|
|
402
|
+
// 🤖 fallback
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/services/task/task-scheduler.ts
|
|
407
|
+
var TaskScheduler = class {
|
|
408
|
+
constructor(logger, taskPool, signalQueue, eventBus, ctx, maxConcurrency = 3, workerConfigs) {
|
|
409
|
+
this.logger = logger;
|
|
410
|
+
this.taskPool = taskPool;
|
|
411
|
+
this.signalQueue = signalQueue;
|
|
412
|
+
this.eventBus = eventBus;
|
|
413
|
+
this.ctx = ctx;
|
|
414
|
+
this.maxConcurrency = maxConcurrency;
|
|
415
|
+
this.workerConfigs = workerConfigs;
|
|
416
|
+
}
|
|
417
|
+
logger;
|
|
418
|
+
taskPool;
|
|
419
|
+
signalQueue;
|
|
420
|
+
eventBus;
|
|
421
|
+
ctx;
|
|
422
|
+
maxConcurrency;
|
|
423
|
+
workerConfigs;
|
|
424
|
+
spawnFn = null;
|
|
425
|
+
/** Inject worker spawn function (breaks circular dep) */
|
|
426
|
+
setSpawnFn(fn) {
|
|
427
|
+
this.spawnFn = fn;
|
|
428
|
+
}
|
|
429
|
+
start() {
|
|
430
|
+
this.eventBus.on("task:added", () => this.schedule());
|
|
431
|
+
this.logger.info("Scheduler started");
|
|
432
|
+
}
|
|
433
|
+
schedule() {
|
|
434
|
+
while (this.taskPool.hasPending() && this.taskPool.getRunningCount() < this.maxConcurrency) {
|
|
435
|
+
const task = this.taskPool.peekHighestPriority();
|
|
436
|
+
if (!task) break;
|
|
437
|
+
if (!this.spawnFn) {
|
|
438
|
+
this.logger.error("spawnFn not set, cannot schedule task");
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
const workerId = generateId("w");
|
|
442
|
+
const workerType = task.workerType ?? "puppet";
|
|
443
|
+
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
444
|
+
this.taskPool.moveToRunning(task.id, workerId);
|
|
445
|
+
this.ctx.addActiveWorker({
|
|
446
|
+
workerId,
|
|
447
|
+
taskId: task.id,
|
|
448
|
+
startedAt: Date.now(),
|
|
449
|
+
workerType: meta.label,
|
|
450
|
+
taskTitle: task.title,
|
|
451
|
+
emoji: meta.emoji
|
|
452
|
+
});
|
|
453
|
+
this.spawnFn({ ...task, workerId }).then((result) => {
|
|
454
|
+
this.handleDone(workerId, task, result);
|
|
455
|
+
}).catch((err) => {
|
|
456
|
+
this.handleFail(workerId, task.id, task.title, meta.label, meta.emoji, formatLlmError(err));
|
|
457
|
+
});
|
|
458
|
+
this.logger.info({ taskId: task.id, workerId }, "Worker spawned");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
handleDone(workerId, task, result) {
|
|
462
|
+
const { id: taskId, title: taskTitle, workerType: wt } = task;
|
|
463
|
+
const workerType = wt ?? "puppet";
|
|
464
|
+
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
465
|
+
if (result.status === "completed") {
|
|
466
|
+
this.taskPool.complete(taskId, result.summary ?? "");
|
|
467
|
+
} else {
|
|
468
|
+
this.taskPool.fail(taskId, result.error ?? "Unknown error");
|
|
469
|
+
}
|
|
470
|
+
if (result.status === "completed") {
|
|
471
|
+
this.signalQueue.enqueue({
|
|
472
|
+
source: "worker",
|
|
473
|
+
type: "completed",
|
|
474
|
+
payload: {
|
|
475
|
+
taskId,
|
|
476
|
+
workerId,
|
|
477
|
+
taskTitle,
|
|
478
|
+
workerType,
|
|
479
|
+
emoji: meta.emoji,
|
|
480
|
+
summary: result.summary
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
} else {
|
|
484
|
+
this.signalQueue.enqueue({
|
|
485
|
+
source: "worker",
|
|
486
|
+
type: "failed",
|
|
487
|
+
payload: {
|
|
488
|
+
taskId,
|
|
489
|
+
workerId,
|
|
490
|
+
taskTitle,
|
|
491
|
+
workerType,
|
|
492
|
+
emoji: meta.emoji,
|
|
493
|
+
error: result.error ?? "Unknown error"
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
this.ctx.removeActiveWorker(workerId);
|
|
498
|
+
this.ctx.removeWorker(workerId);
|
|
499
|
+
this.schedule();
|
|
500
|
+
}
|
|
501
|
+
handleFail(workerId, taskId, taskTitle, workerType, emoji, error) {
|
|
502
|
+
this.taskPool.fail(taskId, error);
|
|
503
|
+
this.signalQueue.enqueue({
|
|
504
|
+
source: "worker",
|
|
505
|
+
type: "failed",
|
|
506
|
+
payload: { taskId, workerId, taskTitle, workerType, emoji, error }
|
|
507
|
+
});
|
|
508
|
+
this.ctx.removeActiveWorker(workerId);
|
|
509
|
+
this.ctx.removeWorker(workerId);
|
|
510
|
+
this.schedule();
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/services/agents/controller-state.ts
|
|
515
|
+
import { join } from "path";
|
|
516
|
+
|
|
517
|
+
// src/types/config.ts
|
|
518
|
+
function validateConfig(config) {
|
|
519
|
+
for (const [name, provider] of Object.entries(config.providers)) {
|
|
520
|
+
if (!provider.endpoint) return `providers.${name}.endpoint is required`;
|
|
521
|
+
if (!provider.apiKey) return `providers.${name}.apiKey is required (set WAYANG_LLM_API_KEY or configure in config file)`;
|
|
522
|
+
if (!provider.modelName) return `providers.${name}.modelName is required`;
|
|
523
|
+
}
|
|
524
|
+
const cp = config.controller.provider;
|
|
525
|
+
if (!config.providers[cp]) return `controller.provider "${cp}" not found`;
|
|
526
|
+
const wp = config.worker.provider;
|
|
527
|
+
if (!config.providers[wp]) return `worker.provider "${wp}" not found`;
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/types/conversation.ts
|
|
532
|
+
function isSystemEntry(entry) {
|
|
533
|
+
return entry.type === "system" /* System */;
|
|
534
|
+
}
|
|
535
|
+
function getEntryContent(entry) {
|
|
536
|
+
if (entry.type === "signal" /* Signal */ || entry.type === "system" /* System */) {
|
|
537
|
+
return entry.content;
|
|
538
|
+
}
|
|
539
|
+
return entry.message.content;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/types/signal.ts
|
|
543
|
+
function getSignalField(sig, field) {
|
|
544
|
+
return sig.payload[field];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/infra/state/base-state.ts
|
|
548
|
+
import { get, set, cloneDeep } from "lodash-es";
|
|
549
|
+
var BaseWayangState = class {
|
|
550
|
+
data;
|
|
551
|
+
persistMappings;
|
|
552
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
553
|
+
constructor(initialData, persistMappings = []) {
|
|
554
|
+
this.data = initialData;
|
|
555
|
+
this.persistMappings = persistMappings;
|
|
556
|
+
}
|
|
557
|
+
// --- Read ---
|
|
558
|
+
get(path5) {
|
|
559
|
+
return get(this.data, path5);
|
|
560
|
+
}
|
|
561
|
+
// --- Write operations ---
|
|
562
|
+
set(path5, value) {
|
|
563
|
+
const prev = get(this.data, path5);
|
|
564
|
+
set(this.data, path5, value);
|
|
565
|
+
this.persist(path5);
|
|
566
|
+
this.notify(path5, "set", prev);
|
|
567
|
+
}
|
|
568
|
+
update(path5, partial) {
|
|
569
|
+
const prev = cloneDeep(get(this.data, path5));
|
|
570
|
+
const target = get(this.data, path5);
|
|
571
|
+
if (target && typeof target === "object") {
|
|
572
|
+
Object.assign(target, partial);
|
|
573
|
+
} else {
|
|
574
|
+
set(this.data, path5, partial);
|
|
575
|
+
}
|
|
576
|
+
this.persist(path5);
|
|
577
|
+
this.notify(path5, "update", prev);
|
|
578
|
+
}
|
|
579
|
+
append(path5, entry) {
|
|
580
|
+
const arr = get(this.data, path5);
|
|
581
|
+
if (!Array.isArray(arr)) {
|
|
582
|
+
throw new Error(`State.append: path "${path5}" is not an array`);
|
|
583
|
+
}
|
|
584
|
+
arr.push(entry);
|
|
585
|
+
set(this.data, path5, [...arr]);
|
|
586
|
+
this.persist(path5);
|
|
587
|
+
this.notify(path5, "append", void 0);
|
|
588
|
+
}
|
|
589
|
+
remove(path5, idx) {
|
|
590
|
+
const arr = get(this.data, path5);
|
|
591
|
+
if (!Array.isArray(arr)) {
|
|
592
|
+
throw new Error(`State.remove: path "${path5}" is not an array`);
|
|
593
|
+
}
|
|
594
|
+
const prev = idx !== void 0 ? arr[idx] : arr[arr.length - 1];
|
|
595
|
+
if (idx === void 0) {
|
|
596
|
+
arr.pop();
|
|
597
|
+
} else {
|
|
598
|
+
arr.splice(idx, 1);
|
|
599
|
+
}
|
|
600
|
+
this.persist(path5);
|
|
601
|
+
this.notify(path5, "remove", prev);
|
|
602
|
+
}
|
|
603
|
+
// --- Subscription ---
|
|
604
|
+
on(path5, callback) {
|
|
605
|
+
if (!this.subscriptions.has(path5)) {
|
|
606
|
+
this.subscriptions.set(path5, /* @__PURE__ */ new Set());
|
|
607
|
+
}
|
|
608
|
+
this.subscriptions.get(path5).add(callback);
|
|
609
|
+
return () => this.subscriptions.get(path5)?.delete(callback);
|
|
610
|
+
}
|
|
611
|
+
// --- Persistence ---
|
|
612
|
+
persist(path5) {
|
|
613
|
+
const topKey = path5.split(".")[0];
|
|
614
|
+
for (const mapping of this.persistMappings) {
|
|
615
|
+
if (mapping.path !== topKey) continue;
|
|
616
|
+
if (mapping.helper.mode === "save") {
|
|
617
|
+
mapping.helper.write(this.data[topKey]);
|
|
618
|
+
} else if (mapping.helper.mode === "append") {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/** Append-mode persistence for subclasses to call directly. */
|
|
623
|
+
persistAppend(path5, entry) {
|
|
624
|
+
const topKey = path5.split(".")[0];
|
|
625
|
+
for (const mapping of this.persistMappings) {
|
|
626
|
+
if (mapping.path === topKey && mapping.helper.mode === "append") {
|
|
627
|
+
mapping.helper.write(entry);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// --- Notification ---
|
|
632
|
+
notify(changePath, type, prev) {
|
|
633
|
+
const event = { type, path: changePath, data: get(this.data, changePath), prev };
|
|
634
|
+
for (const [subPath, callbacks] of this.subscriptions) {
|
|
635
|
+
if (changePath === subPath) {
|
|
636
|
+
for (const cb of callbacks) cb(event);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (changePath.startsWith(subPath + ".")) {
|
|
640
|
+
for (const cb of callbacks) {
|
|
641
|
+
cb({ ...event, data: get(this.data, subPath) });
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (subPath.startsWith(changePath + ".")) {
|
|
646
|
+
const newVal = get(this.data, subPath);
|
|
647
|
+
for (const cb of callbacks) {
|
|
648
|
+
cb({ ...event, data: newVal });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// src/infra/state/persistence/json-file.ts
|
|
656
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
657
|
+
import { dirname } from "path";
|
|
658
|
+
var JSONFileHelper = class {
|
|
659
|
+
constructor(filePath) {
|
|
660
|
+
this.filePath = filePath;
|
|
661
|
+
}
|
|
662
|
+
filePath;
|
|
663
|
+
mode = "save";
|
|
664
|
+
write(data) {
|
|
665
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
666
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
667
|
+
}
|
|
668
|
+
read() {
|
|
669
|
+
if (!existsSync(this.filePath)) return null;
|
|
670
|
+
return JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
671
|
+
}
|
|
672
|
+
clear() {
|
|
673
|
+
writeFileSync(this.filePath, "{}");
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// src/infra/state/persistence/jsonl-file.ts
|
|
678
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
679
|
+
import { dirname as dirname2 } from "path";
|
|
680
|
+
var JSONLFileHelper = class {
|
|
681
|
+
constructor(filePath) {
|
|
682
|
+
this.filePath = filePath;
|
|
683
|
+
}
|
|
684
|
+
filePath;
|
|
685
|
+
mode = "append";
|
|
686
|
+
write(entry) {
|
|
687
|
+
mkdirSync2(dirname2(this.filePath), { recursive: true });
|
|
688
|
+
appendFileSync(this.filePath, JSON.stringify(entry) + "\n");
|
|
689
|
+
}
|
|
690
|
+
read() {
|
|
691
|
+
if (!existsSync2(this.filePath)) return [];
|
|
692
|
+
return readFileSync2(this.filePath, "utf-8").trim().split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
693
|
+
}
|
|
694
|
+
readFromEnd(stopCondition) {
|
|
695
|
+
const all = this.read();
|
|
696
|
+
const result = [];
|
|
697
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
698
|
+
if (stopCondition(all[i])) break;
|
|
699
|
+
result.unshift(all[i]);
|
|
700
|
+
}
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
clear() {
|
|
704
|
+
writeFileSync2(this.filePath, "");
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// src/services/agents/controller-state.ts
|
|
709
|
+
var ControllerAgentState = class extends BaseWayangState {
|
|
710
|
+
constructor(sessionDir, logger) {
|
|
711
|
+
const jsonFile = new JSONFileHelper(join(sessionDir, "runtime-state.json"));
|
|
712
|
+
const jsonlFile = new JSONLFileHelper(join(sessionDir, "conversation.jsonl"));
|
|
713
|
+
const initialData = {
|
|
714
|
+
runtimeState: {
|
|
715
|
+
session: { id: "", startedAt: 0 },
|
|
716
|
+
activeWorkers: [],
|
|
717
|
+
maxConcurrency: 3
|
|
718
|
+
},
|
|
719
|
+
conversation: [],
|
|
720
|
+
compactSummary: null,
|
|
721
|
+
dynamicState: { streamingEntries: [], busy: false }
|
|
722
|
+
};
|
|
723
|
+
super(initialData, [
|
|
724
|
+
{ path: "runtimeState", helper: jsonFile },
|
|
725
|
+
{ path: "conversation", helper: jsonlFile }
|
|
726
|
+
]);
|
|
727
|
+
this.sessionDir = sessionDir;
|
|
728
|
+
this.logger = logger;
|
|
729
|
+
this.jsonFile = jsonFile;
|
|
730
|
+
this.jsonlFile = jsonlFile;
|
|
731
|
+
}
|
|
732
|
+
sessionDir;
|
|
733
|
+
logger;
|
|
734
|
+
jsonFile;
|
|
735
|
+
jsonlFile;
|
|
736
|
+
async restore() {
|
|
737
|
+
const runtimeState = this.jsonFile.read();
|
|
738
|
+
if (runtimeState) {
|
|
739
|
+
this.data.runtimeState = runtimeState;
|
|
740
|
+
}
|
|
741
|
+
const allEntries = this.jsonlFile.read();
|
|
742
|
+
const compactIdx = allEntries.findLastIndex(
|
|
743
|
+
(e) => isSystemEntry(e) && e.subtype === "compact" /* Compact */
|
|
744
|
+
);
|
|
745
|
+
if (compactIdx === -1) {
|
|
746
|
+
this.data.conversation = allEntries;
|
|
747
|
+
} else {
|
|
748
|
+
const RECENT_TOKEN_BUDGET2 = 2e4;
|
|
749
|
+
let budget = RECENT_TOKEN_BUDGET2;
|
|
750
|
+
let cutIdx = 0;
|
|
751
|
+
for (let i = compactIdx - 1; i >= 0; i--) {
|
|
752
|
+
const entry = allEntries[i];
|
|
753
|
+
const content = getEntryContent(entry);
|
|
754
|
+
budget -= Math.ceil(String(content).length / 4);
|
|
755
|
+
if (budget <= 0) {
|
|
756
|
+
cutIdx = i + 1;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
this.data.conversation = allEntries.slice(cutIdx);
|
|
761
|
+
}
|
|
762
|
+
this.logger.info({
|
|
763
|
+
entryCount: this.data.conversation.length,
|
|
764
|
+
totalEntries: allEntries.length
|
|
765
|
+
}, "ControllerAgentState restored");
|
|
766
|
+
}
|
|
767
|
+
/** Override append to ensure JSONL write-per-entry. */
|
|
768
|
+
append(path5, entry) {
|
|
769
|
+
super.append(path5, entry);
|
|
770
|
+
if (path5 === "conversation") {
|
|
771
|
+
this.persistAppend(path5, entry);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// src/services/task/task-pool-state.ts
|
|
777
|
+
import { join as join2 } from "path";
|
|
778
|
+
var TaskPoolState = class extends BaseWayangState {
|
|
779
|
+
constructor(sessionDir, logger) {
|
|
780
|
+
const jsonFile = new JSONFileHelper(join2(sessionDir, "tasks.json"));
|
|
781
|
+
super(
|
|
782
|
+
{
|
|
783
|
+
tasks: {
|
|
784
|
+
pending: [],
|
|
785
|
+
running: [],
|
|
786
|
+
history: []
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
[{ path: "tasks", helper: jsonFile }]
|
|
790
|
+
);
|
|
791
|
+
this.sessionDir = sessionDir;
|
|
792
|
+
this.logger = logger;
|
|
793
|
+
this.jsonFile = jsonFile;
|
|
794
|
+
}
|
|
795
|
+
sessionDir;
|
|
796
|
+
logger;
|
|
797
|
+
jsonFile;
|
|
798
|
+
async restore() {
|
|
799
|
+
const data = this.jsonFile.read();
|
|
800
|
+
if (data) {
|
|
801
|
+
this.data.tasks = data;
|
|
802
|
+
}
|
|
803
|
+
this.logger.info("TaskPoolState restored");
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// src/services/signal/signal-state.ts
|
|
808
|
+
import { join as join3 } from "path";
|
|
809
|
+
var SignalState = class extends BaseWayangState {
|
|
810
|
+
constructor(sessionDir, logger) {
|
|
811
|
+
const jsonlFile = new JSONLFileHelper(join3(sessionDir, "signals.jsonl"));
|
|
812
|
+
const eventLogFile = new JSONLFileHelper(join3(sessionDir, "state-events.jsonl"));
|
|
813
|
+
super(
|
|
814
|
+
{ signals: [] },
|
|
815
|
+
[{ path: "signals", helper: jsonlFile }]
|
|
816
|
+
);
|
|
817
|
+
this.sessionDir = sessionDir;
|
|
818
|
+
this.logger = logger;
|
|
819
|
+
this.jsonlFile = jsonlFile;
|
|
820
|
+
this.eventLogFile = eventLogFile;
|
|
821
|
+
this.on("signals", (event) => this.recordStateEvent(event));
|
|
822
|
+
}
|
|
823
|
+
sessionDir;
|
|
824
|
+
logger;
|
|
825
|
+
jsonlFile;
|
|
826
|
+
eventLogFile;
|
|
827
|
+
/** In-memory cache of event log entries. */
|
|
828
|
+
eventBuffer = [];
|
|
829
|
+
/** When true, skip recording events (used during restore to avoid re-writing). */
|
|
830
|
+
restoring = false;
|
|
831
|
+
/** Record a state change event to the event log */
|
|
832
|
+
recordStateEvent(event) {
|
|
833
|
+
if (this.restoring) return;
|
|
834
|
+
const logEntry = {
|
|
835
|
+
type: event.type,
|
|
836
|
+
path: event.path,
|
|
837
|
+
timestamp: Date.now(),
|
|
838
|
+
prev: event.prev,
|
|
839
|
+
data: event.data
|
|
840
|
+
};
|
|
841
|
+
if (event.path === "signals") {
|
|
842
|
+
if (event.type === "append" && Array.isArray(event.data) && event.data.length > 0) {
|
|
843
|
+
const sig = event.data[event.data.length - 1];
|
|
844
|
+
if (sig?.id) {
|
|
845
|
+
logEntry.signalId = sig.id;
|
|
846
|
+
}
|
|
847
|
+
} else if ((event.type === "set" || event.type === "update") && Array.isArray(event.data)) {
|
|
848
|
+
const sig = event.data.find((m) => m?.id);
|
|
849
|
+
if (sig?.id) {
|
|
850
|
+
logEntry.signalId = sig.id;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
this.eventBuffer.push(logEntry);
|
|
855
|
+
this.eventLogFile.write(logEntry);
|
|
856
|
+
this.logger.debug({ eventType: event.type, path: event.path }, "State event recorded");
|
|
857
|
+
}
|
|
858
|
+
/** Get all state change events from the in-memory buffer. */
|
|
859
|
+
getStateEvents() {
|
|
860
|
+
return this.eventBuffer;
|
|
861
|
+
}
|
|
862
|
+
/** Get events filtered by signal ID */
|
|
863
|
+
getEventsBySignalId(signalId) {
|
|
864
|
+
return this.eventBuffer.filter((e) => e.signalId === signalId);
|
|
865
|
+
}
|
|
866
|
+
/** Get events filtered by type */
|
|
867
|
+
getEventsByType(type) {
|
|
868
|
+
return this.eventBuffer.filter((e) => e.type === type);
|
|
869
|
+
}
|
|
870
|
+
async restore() {
|
|
871
|
+
this.restoring = true;
|
|
872
|
+
try {
|
|
873
|
+
const signals = this.jsonlFile.read();
|
|
874
|
+
const sigMap = /* @__PURE__ */ new Map();
|
|
875
|
+
for (const sig of signals) {
|
|
876
|
+
sigMap.set(sig.id, { ...sig });
|
|
877
|
+
}
|
|
878
|
+
const events = this.eventLogFile.read();
|
|
879
|
+
this.eventBuffer = events;
|
|
880
|
+
for (const event of events) {
|
|
881
|
+
if (event.type === "set" && event.path === "signals" && Array.isArray(event.data)) {
|
|
882
|
+
for (const sig of event.data) {
|
|
883
|
+
if (sig?.id && sigMap.has(sig.id)) {
|
|
884
|
+
sigMap.get(sig.id).status = sig.status;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
this.data.signals = Array.from(sigMap.values());
|
|
890
|
+
this.logger.info({
|
|
891
|
+
signalCount: this.data.signals.length,
|
|
892
|
+
eventCount: events.length
|
|
893
|
+
}, "SignalState restored");
|
|
894
|
+
} finally {
|
|
895
|
+
this.restoring = false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/** Override append to ensure JSONL write-per-entry. */
|
|
899
|
+
append(path5, entry) {
|
|
900
|
+
super.append(path5, entry);
|
|
901
|
+
if (path5 === "signals") {
|
|
902
|
+
this.persistAppend(path5, entry);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// src/services/session/session-manager.ts
|
|
908
|
+
import * as path from "path";
|
|
909
|
+
|
|
910
|
+
// src/services/session/session-state.ts
|
|
911
|
+
import { join as join4 } from "path";
|
|
912
|
+
var SessionState = class extends BaseWayangState {
|
|
913
|
+
jsonFile;
|
|
914
|
+
constructor(sessionDir) {
|
|
915
|
+
const jsonFile = new JSONFileHelper(join4(sessionDir, "meta.json"));
|
|
916
|
+
super(
|
|
917
|
+
{
|
|
918
|
+
meta: {
|
|
919
|
+
sessionId: "",
|
|
920
|
+
workspace: "",
|
|
921
|
+
firstInput: null,
|
|
922
|
+
createdAt: 0,
|
|
923
|
+
lastActiveAt: 0
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
[{ path: "meta", helper: jsonFile }]
|
|
927
|
+
);
|
|
928
|
+
this.jsonFile = jsonFile;
|
|
929
|
+
}
|
|
930
|
+
async restore() {
|
|
931
|
+
const data = this.jsonFile.read();
|
|
932
|
+
if (data) {
|
|
933
|
+
this.data.meta = data;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
// src/services/session/session-manager.ts
|
|
939
|
+
var SessionManager = class _SessionManager {
|
|
940
|
+
sessionId;
|
|
941
|
+
sessionDir;
|
|
942
|
+
isResume;
|
|
943
|
+
state;
|
|
944
|
+
constructor(sessionId, sessionDir, isResume) {
|
|
945
|
+
this.sessionId = sessionId;
|
|
946
|
+
this.sessionDir = sessionDir;
|
|
947
|
+
this.isResume = isResume;
|
|
948
|
+
this.state = new SessionState(sessionDir);
|
|
949
|
+
}
|
|
950
|
+
/** Create a new session: generates ID, initializes meta via WayangState (auto mkdir + write). */
|
|
951
|
+
static create(homeDir2, workspaceDir2) {
|
|
952
|
+
const sessionId = `${formatTimestamp()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
953
|
+
const sessionDir = path.join(homeDir2, "sessions", sessionId);
|
|
954
|
+
const mgr = new _SessionManager(sessionId, sessionDir, false);
|
|
955
|
+
mgr.state.set("meta", {
|
|
956
|
+
sessionId,
|
|
957
|
+
workspace: workspaceDir2,
|
|
958
|
+
firstInput: null,
|
|
959
|
+
createdAt: Date.now(),
|
|
960
|
+
lastActiveAt: Date.now()
|
|
961
|
+
});
|
|
962
|
+
return mgr;
|
|
963
|
+
}
|
|
964
|
+
/** Resume an existing session. State is restored in restore(). */
|
|
965
|
+
static resume(sessionId, sessionDir) {
|
|
966
|
+
return new _SessionManager(sessionId, sessionDir, true);
|
|
967
|
+
}
|
|
968
|
+
/** Restore state from disk (resume only, no-op for new sessions). */
|
|
969
|
+
async restore() {
|
|
970
|
+
if (!this.isResume) return;
|
|
971
|
+
await this.state.restore();
|
|
972
|
+
}
|
|
973
|
+
/** Workspace directory of this session. */
|
|
974
|
+
get workspace() {
|
|
975
|
+
return this.state.get("meta.workspace");
|
|
976
|
+
}
|
|
977
|
+
/** Whether firstInput has already been recorded. */
|
|
978
|
+
get hasFirstInput() {
|
|
979
|
+
return !!this.state.get("meta.firstInput");
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Notify that a user input occurred.
|
|
983
|
+
* Records firstInput (idempotent) and updates lastActiveAt.
|
|
984
|
+
*/
|
|
985
|
+
onUserInput(text) {
|
|
986
|
+
if (!this.hasFirstInput) {
|
|
987
|
+
this.state.set("meta.firstInput", text);
|
|
988
|
+
}
|
|
989
|
+
this.state.set("meta.lastActiveAt", Date.now());
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// src/services/agents/base-agent.ts
|
|
994
|
+
import { streamText, stepCountIs, hasToolCall } from "ai";
|
|
995
|
+
|
|
996
|
+
// src/services/agents/model-factory.ts
|
|
997
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
998
|
+
function createModel(provider) {
|
|
999
|
+
const openai = createOpenAI({ baseURL: provider.endpoint, apiKey: provider.apiKey });
|
|
1000
|
+
return openai.chat(provider.modelName);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/services/agents/base-agent.ts
|
|
1004
|
+
var BaseAgent = class {
|
|
1005
|
+
id;
|
|
1006
|
+
abortController;
|
|
1007
|
+
model;
|
|
1008
|
+
hooks = {};
|
|
1009
|
+
constructor(provider) {
|
|
1010
|
+
this.id = generateId("agent");
|
|
1011
|
+
this.abortController = new AbortController();
|
|
1012
|
+
this.model = createModel(provider);
|
|
1013
|
+
}
|
|
1014
|
+
/** Set lifecycle hooks. */
|
|
1015
|
+
setHooks(hooks) {
|
|
1016
|
+
this.hooks = hooks;
|
|
1017
|
+
}
|
|
1018
|
+
/** Core streaming primitive — yields text chunks, returns final result.
|
|
1019
|
+
* Catches abort signal internally so callers see a clean completion. */
|
|
1020
|
+
async *streamLoop(opts) {
|
|
1021
|
+
const step = 0;
|
|
1022
|
+
let stream;
|
|
1023
|
+
const stopConditions = [];
|
|
1024
|
+
if (opts.maxSteps) stopConditions.push(stepCountIs(opts.maxSteps));
|
|
1025
|
+
if (opts.stopTools?.length) {
|
|
1026
|
+
for (const name of opts.stopTools) {
|
|
1027
|
+
stopConditions.push(hasToolCall(name));
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
let startTime;
|
|
1031
|
+
try {
|
|
1032
|
+
this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
|
|
1033
|
+
startTime = Date.now();
|
|
1034
|
+
stream = streamText({
|
|
1035
|
+
model: this.model,
|
|
1036
|
+
system: opts.system,
|
|
1037
|
+
messages: opts.messages,
|
|
1038
|
+
tools: opts.tools,
|
|
1039
|
+
toolChoice: opts.toolChoice,
|
|
1040
|
+
stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
|
|
1041
|
+
abortSignal: this.abortController.signal,
|
|
1042
|
+
onStepFinish: opts.onStep
|
|
1043
|
+
});
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
if (this.abortController.signal.aborted) {
|
|
1046
|
+
return { text: "", toolResults: [], usage: void 0 };
|
|
1047
|
+
}
|
|
1048
|
+
throw err;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
for await (const chunk of stream.textStream) {
|
|
1052
|
+
yield chunk;
|
|
1053
|
+
}
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
if (this.abortController.signal.aborted) {
|
|
1056
|
+
return { text: "", toolResults: [], usage: void 0 };
|
|
1057
|
+
}
|
|
1058
|
+
throw err;
|
|
1059
|
+
}
|
|
1060
|
+
const duration = Date.now() - startTime;
|
|
1061
|
+
const text = await stream.text;
|
|
1062
|
+
const reasoningText = await stream.reasoningText;
|
|
1063
|
+
const toolResults = await stream.toolResults;
|
|
1064
|
+
const rawUsage = await stream.usage;
|
|
1065
|
+
const usage = rawUsage ? { inputTokens: rawUsage.inputTokens ?? 0, outputTokens: rawUsage.outputTokens ?? 0 } : void 0;
|
|
1066
|
+
this.hooks.afterLLM?.({ step, usage, durationMs: duration });
|
|
1067
|
+
return {
|
|
1068
|
+
text,
|
|
1069
|
+
reasoningText,
|
|
1070
|
+
toolResults: toolResults?.map((tr) => ({
|
|
1071
|
+
toolCallId: tr.toolCallId,
|
|
1072
|
+
result: tr.result,
|
|
1073
|
+
isError: tr.isError
|
|
1074
|
+
})),
|
|
1075
|
+
usage
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/** Batch wrapper — iterates streamLoop and returns the final result. */
|
|
1079
|
+
async collectLoop(opts) {
|
|
1080
|
+
const gen = this.streamLoop(opts);
|
|
1081
|
+
let result;
|
|
1082
|
+
while (true) {
|
|
1083
|
+
const { done, value } = await gen.next();
|
|
1084
|
+
if (done) {
|
|
1085
|
+
result = value;
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return result;
|
|
1090
|
+
}
|
|
1091
|
+
abort() {
|
|
1092
|
+
this.abortController.abort();
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// src/services/agents/utils/conversation-to-sdk-messages.ts
|
|
1097
|
+
function conversationToSdkMessages(entries) {
|
|
1098
|
+
const messages = [];
|
|
1099
|
+
for (const entry of entries) {
|
|
1100
|
+
switch (entry.type) {
|
|
1101
|
+
case "user" /* User */:
|
|
1102
|
+
messages.push({ role: "user", content: entry.message.content });
|
|
1103
|
+
break;
|
|
1104
|
+
case "assistant" /* Assistant */: {
|
|
1105
|
+
const hasSkipReply = entry.toolCalls?.some((tc) => tc.toolName === "skip_reply");
|
|
1106
|
+
const effectiveContent = hasSkipReply ? "" : entry.message.content;
|
|
1107
|
+
const parts = [];
|
|
1108
|
+
if (effectiveContent) {
|
|
1109
|
+
parts.push({ type: "text", text: effectiveContent });
|
|
1110
|
+
}
|
|
1111
|
+
if (entry.toolCalls?.length) {
|
|
1112
|
+
for (const tc of entry.toolCalls) {
|
|
1113
|
+
parts.push({
|
|
1114
|
+
type: "tool-call",
|
|
1115
|
+
toolCallId: tc.toolCallId,
|
|
1116
|
+
toolName: tc.toolName,
|
|
1117
|
+
input: parseJsonSafe(tc.arguments)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (parts.length === 1 && parts[0].type === "text") {
|
|
1122
|
+
messages.push({ role: "assistant", content: parts[0].text });
|
|
1123
|
+
} else if (parts.length > 0) {
|
|
1124
|
+
messages.push({ role: "assistant", content: parts });
|
|
1125
|
+
}
|
|
1126
|
+
if (entry.toolResults?.length) {
|
|
1127
|
+
messages.push({
|
|
1128
|
+
role: "tool",
|
|
1129
|
+
content: entry.toolResults.map((tr) => ({
|
|
1130
|
+
type: "tool-result",
|
|
1131
|
+
toolCallId: tr.toolCallId,
|
|
1132
|
+
toolName: tr.toolName,
|
|
1133
|
+
output: tr.output
|
|
1134
|
+
}))
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
if (entry.toolCalls?.length) {
|
|
1138
|
+
const resultIds = new Set(entry.toolResults?.map((tr) => tr.toolCallId) ?? []);
|
|
1139
|
+
const orphanCalls = entry.toolCalls.filter((tc) => !resultIds.has(tc.toolCallId));
|
|
1140
|
+
if (orphanCalls.length > 0) {
|
|
1141
|
+
messages.push({
|
|
1142
|
+
role: "tool",
|
|
1143
|
+
content: orphanCalls.map((tc) => ({
|
|
1144
|
+
type: "tool-result",
|
|
1145
|
+
toolCallId: tc.toolCallId,
|
|
1146
|
+
toolName: tc.toolName,
|
|
1147
|
+
output: { type: "text", value: "[No result \u2014 step was interrupted]" }
|
|
1148
|
+
}))
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
case "system" /* System */:
|
|
1155
|
+
break;
|
|
1156
|
+
case "signal" /* Signal */: {
|
|
1157
|
+
const sig = entry;
|
|
1158
|
+
const label = sig.subtype.replace("worker_", "").toUpperCase();
|
|
1159
|
+
const taskTag = sig.taskId ? ` task=${sig.taskId}` : "";
|
|
1160
|
+
messages.push({
|
|
1161
|
+
role: "system",
|
|
1162
|
+
content: `[WORKER PROGRESS SIGNAL: ${label}${taskTag}] ${sig.content}`
|
|
1163
|
+
});
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return messages;
|
|
1169
|
+
}
|
|
1170
|
+
function parseJsonSafe(text) {
|
|
1171
|
+
if (!text) return {};
|
|
1172
|
+
try {
|
|
1173
|
+
return JSON.parse(text);
|
|
1174
|
+
} catch {
|
|
1175
|
+
return text;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/services/agents/utils/estimate-tokens.ts
|
|
1180
|
+
function estimateTokens(text) {
|
|
1181
|
+
if (!text) return 0;
|
|
1182
|
+
return Math.ceil(text.length / 4);
|
|
1183
|
+
}
|
|
1184
|
+
function estimateConversationTokens(entries) {
|
|
1185
|
+
let total = 0;
|
|
1186
|
+
for (const entry of entries) {
|
|
1187
|
+
if (entry.message?.content) {
|
|
1188
|
+
total += estimateTokens(entry.message.content);
|
|
1189
|
+
}
|
|
1190
|
+
if (entry.arguments) {
|
|
1191
|
+
total += estimateTokens(typeof entry.arguments === "string" ? entry.arguments : JSON.stringify(entry.arguments));
|
|
1192
|
+
}
|
|
1193
|
+
if (entry.result) {
|
|
1194
|
+
total += estimateTokens(entry.result);
|
|
1195
|
+
}
|
|
1196
|
+
if (entry.content) {
|
|
1197
|
+
total += estimateTokens(entry.content);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return total;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/utils/time.ts
|
|
1204
|
+
function nowISO() {
|
|
1205
|
+
const now = /* @__PURE__ */ new Date();
|
|
1206
|
+
const off = -now.getTimezoneOffset();
|
|
1207
|
+
const sign = off >= 0 ? "+" : "-";
|
|
1208
|
+
const absOff = Math.abs(off);
|
|
1209
|
+
const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
|
|
1210
|
+
const mm = String(absOff % 60).padStart(2, "0");
|
|
1211
|
+
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
1212
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}${sign}${hh}:${mm}`;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/services/agents/context-manager.ts
|
|
1216
|
+
var DEFAULT_MAX_TOKENS = 1e5;
|
|
1217
|
+
var COMPACT_THRESHOLD_RATIO = 0.9;
|
|
1218
|
+
var RECENT_TOKEN_BUDGET = 2e4;
|
|
1219
|
+
var COMPACT_MAX_RETRIES = 2;
|
|
1220
|
+
var ContextManager = class {
|
|
1221
|
+
constructor(state, staticPrompt, dynamicContextFn, maxTokens) {
|
|
1222
|
+
this.state = state;
|
|
1223
|
+
this.staticPrompt = staticPrompt;
|
|
1224
|
+
this.dynamicContextFn = dynamicContextFn;
|
|
1225
|
+
this.maxTokens = maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
1226
|
+
}
|
|
1227
|
+
state;
|
|
1228
|
+
staticPrompt;
|
|
1229
|
+
dynamicContextFn;
|
|
1230
|
+
maxTokens;
|
|
1231
|
+
/**
|
|
1232
|
+
* Cached compact state — updated by compact() and restore(), read by getMessages().
|
|
1233
|
+
* Null means no compaction has occurred.
|
|
1234
|
+
*/
|
|
1235
|
+
compactSummary = null;
|
|
1236
|
+
/**
|
|
1237
|
+
* Index into the conversation array: entries from this index onward are the
|
|
1238
|
+
* "recent" entries that should be sent to the LLM alongside the summary.
|
|
1239
|
+
* Updated together with compactSummary.
|
|
1240
|
+
*/
|
|
1241
|
+
compactStartIdx = 0;
|
|
1242
|
+
// --- Public API ---
|
|
1243
|
+
getSystemPrompt() {
|
|
1244
|
+
const parts = [this.staticPrompt];
|
|
1245
|
+
if (this.compactSummary) {
|
|
1246
|
+
parts.push(`
|
|
1247
|
+
[Conversation Summary]
|
|
1248
|
+
${this.compactSummary}`);
|
|
1249
|
+
}
|
|
1250
|
+
parts.push("\n" + this.dynamicContextFn());
|
|
1251
|
+
return parts.join("\n\n");
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Build messages for the LLM.
|
|
1255
|
+
*
|
|
1256
|
+
* If no compaction has occurred, converts the full conversation.
|
|
1257
|
+
* If compacted, returns: [summary system msg] + [recent entries (compactStartIdx..)].
|
|
1258
|
+
* No backward scanning — uses cached compactSummary and compactStartIdx.
|
|
1259
|
+
*/
|
|
1260
|
+
getMessages() {
|
|
1261
|
+
const conversation = this.state.get("conversation");
|
|
1262
|
+
if (!this.compactSummary) {
|
|
1263
|
+
return conversationToSdkMessages(conversation);
|
|
1264
|
+
}
|
|
1265
|
+
const recentEntries = conversation.slice(this.compactStartIdx);
|
|
1266
|
+
return [
|
|
1267
|
+
{ role: "system", content: `[Conversation Summary]
|
|
1268
|
+
${this.compactSummary}` },
|
|
1269
|
+
...conversationToSdkMessages(recentEntries)
|
|
1270
|
+
];
|
|
1271
|
+
}
|
|
1272
|
+
/** Check if context exceeds the compaction threshold. */
|
|
1273
|
+
isFull() {
|
|
1274
|
+
const promptTokens = estimateTokens(this.getSystemPrompt());
|
|
1275
|
+
const convTokens = estimateConversationTokens(this.getMessages());
|
|
1276
|
+
return promptTokens + convTokens > this.maxTokens * COMPACT_THRESHOLD_RATIO;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Compact the conversation by summarizing all current entries.
|
|
1280
|
+
*
|
|
1281
|
+
* Appends a compact marker entry to the conversation (and JSONL file).
|
|
1282
|
+
* Does NOT modify or remove any existing entries — storage is append-only.
|
|
1283
|
+
*
|
|
1284
|
+
* The summarizer receives ALL entries and returns a summary.
|
|
1285
|
+
* After compaction, getMessages() returns [summary] + entries appended after the marker.
|
|
1286
|
+
*/
|
|
1287
|
+
async compact(summarizer) {
|
|
1288
|
+
const conversation = this.state.get("conversation");
|
|
1289
|
+
if (conversation.length < 4) return;
|
|
1290
|
+
const summary = await summarizer(conversation);
|
|
1291
|
+
this.compactSummary = summary;
|
|
1292
|
+
this.compactStartIdx = conversation.length;
|
|
1293
|
+
this.state.append("conversation", {
|
|
1294
|
+
type: "system" /* System */,
|
|
1295
|
+
uuid: generateId("compact"),
|
|
1296
|
+
parentUuid: null,
|
|
1297
|
+
sessionId: this.state.get("runtimeState.session.id") ?? "",
|
|
1298
|
+
timestamp: nowISO(),
|
|
1299
|
+
subtype: "compact" /* Compact */,
|
|
1300
|
+
content: summary,
|
|
1301
|
+
compactMeta: {
|
|
1302
|
+
summarizedTokenCount: estimateConversationTokens(conversation)
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Restore compact state from persisted conversation.
|
|
1308
|
+
*
|
|
1309
|
+
* Scans for the latest compact marker, then loads recent entries within
|
|
1310
|
+
* the token budget. If loaded entries are fewer than RECENT_TOKEN_BUDGET,
|
|
1311
|
+
* continues loading past the marker (overlap with summary is acceptable).
|
|
1312
|
+
*/
|
|
1313
|
+
restoreFromEntries(entries) {
|
|
1314
|
+
const compactIdx = entries.findLastIndex(
|
|
1315
|
+
(e) => e.type === "system" /* System */ && e.subtype === "compact" /* Compact */
|
|
1316
|
+
);
|
|
1317
|
+
if (compactIdx === -1) {
|
|
1318
|
+
this.compactSummary = null;
|
|
1319
|
+
this.compactStartIdx = 0;
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const summary = entries[compactIdx].content;
|
|
1323
|
+
let budget = RECENT_TOKEN_BUDGET;
|
|
1324
|
+
let cutIdx = 0;
|
|
1325
|
+
for (let i = compactIdx - 1; i >= 0; i--) {
|
|
1326
|
+
const tokens = estimateEntryTokens(entries[i]);
|
|
1327
|
+
budget -= tokens;
|
|
1328
|
+
if (budget <= 0) {
|
|
1329
|
+
cutIdx = i + 1;
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
this.compactSummary = summary;
|
|
1334
|
+
this.compactStartIdx = compactIdx - cutIdx;
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
function estimateEntryTokens(entry) {
|
|
1338
|
+
const content = entry.message?.content ?? entry.content ?? "";
|
|
1339
|
+
let tokens = estimateTokens(String(content));
|
|
1340
|
+
if (entry.toolCalls?.length) {
|
|
1341
|
+
for (const tc of entry.toolCalls) {
|
|
1342
|
+
tokens += estimateTokens(tc.arguments ?? "");
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (entry.toolResults?.length) {
|
|
1346
|
+
for (const tr of entry.toolResults) {
|
|
1347
|
+
tokens += estimateTokens(tr.output?.value ?? "");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return tokens;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/services/agents/utils/build-assistant-entry.ts
|
|
1354
|
+
function buildAssistantEntry(event, sessionId, uuid) {
|
|
1355
|
+
const ts = nowISO();
|
|
1356
|
+
const text = event.text ?? "";
|
|
1357
|
+
const reasoning = event.reasoningText && event.reasoningText !== "[]" && event.reasoningText.length > 0 ? event.reasoningText : void 0;
|
|
1358
|
+
const toolCalls = event.toolCalls?.length ? event.toolCalls.map((tc) => ({
|
|
1359
|
+
toolCallId: tc.toolCallId,
|
|
1360
|
+
toolName: tc.toolName,
|
|
1361
|
+
arguments: (
|
|
1362
|
+
// SDK OnStepFinishEvent type incomplete
|
|
1363
|
+
typeof tc.args === "string" ? tc.args : JSON.stringify(tc.args ?? tc.input ?? {})
|
|
1364
|
+
)
|
|
1365
|
+
})) : [];
|
|
1366
|
+
const results = [];
|
|
1367
|
+
if (event.toolResults?.length) {
|
|
1368
|
+
results.push(
|
|
1369
|
+
...event.toolResults.map((tr) => ({
|
|
1370
|
+
toolCallId: tr.toolCallId,
|
|
1371
|
+
toolName: tr.toolName,
|
|
1372
|
+
output: {
|
|
1373
|
+
type: "text",
|
|
1374
|
+
value: (
|
|
1375
|
+
// SDK OnStepFinishEvent type incomplete
|
|
1376
|
+
typeof tr.output === "string" ? tr.output : JSON.stringify(tr.output ?? "")
|
|
1377
|
+
)
|
|
1378
|
+
}
|
|
1379
|
+
}))
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
const content = event.content;
|
|
1383
|
+
if (Array.isArray(content)) {
|
|
1384
|
+
for (const part of content) {
|
|
1385
|
+
if (part.type === "tool-error" && part.toolCallId) {
|
|
1386
|
+
results.push({
|
|
1387
|
+
toolCallId: part.toolCallId,
|
|
1388
|
+
toolName: part.toolName ?? "unknown",
|
|
1389
|
+
output: {
|
|
1390
|
+
type: "text",
|
|
1391
|
+
value: part.error?.message ?? part.error ?? "Tool input validation failed"
|
|
1392
|
+
},
|
|
1393
|
+
isError: true
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const toolResults = results;
|
|
1399
|
+
const usage = event.usage ? { inputTokens: event.usage.inputTokens ?? 0, outputTokens: event.usage.outputTokens ?? 0 } : void 0;
|
|
1400
|
+
return {
|
|
1401
|
+
type: "assistant" /* Assistant */,
|
|
1402
|
+
uuid: uuid ?? generateId("step"),
|
|
1403
|
+
parentUuid: null,
|
|
1404
|
+
sessionId,
|
|
1405
|
+
timestamp: ts,
|
|
1406
|
+
message: {
|
|
1407
|
+
role: "assistant",
|
|
1408
|
+
content: text,
|
|
1409
|
+
reasoning,
|
|
1410
|
+
usage
|
|
1411
|
+
},
|
|
1412
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
1413
|
+
toolResults: toolResults.length > 0 ? toolResults : void 0,
|
|
1414
|
+
finishReason: event.finishReason
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/services/agents/controller-agent.ts
|
|
1419
|
+
import { generateText } from "ai";
|
|
1420
|
+
|
|
1421
|
+
// src/services/agents/prompts/prompt-utils.ts
|
|
1422
|
+
function assemble(...sections) {
|
|
1423
|
+
return sections.filter((s) => typeof s === "string" && s.length > 0).join("\n\n");
|
|
1424
|
+
}
|
|
1425
|
+
function section(title, body) {
|
|
1426
|
+
if (!body.trim()) return "";
|
|
1427
|
+
return `# ${title}
|
|
1428
|
+
|
|
1429
|
+
${body.trim()}`;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/services/agents/prompts/controller-prompt.ts
|
|
1433
|
+
var IDENTITY = section(
|
|
1434
|
+
"Identity",
|
|
1435
|
+
`You are Wayang \u2014 an intelligent assistant that can execute real tasks (write code, run commands, manage files) through background workers.
|
|
1436
|
+
From the user's perspective, you ARE the system. Never mention "Controller", "Worker", or internal architecture \u2014 just get things done.`
|
|
1437
|
+
);
|
|
1438
|
+
var RESPONSE_STYLE = section(
|
|
1439
|
+
"Response style",
|
|
1440
|
+
`Be direct. Minimum tokens, maximum clarity.
|
|
1441
|
+
|
|
1442
|
+
## Responding to user messages
|
|
1443
|
+
Always respond to user input \u2014 never leave the user without a reply.
|
|
1444
|
+
- Answer questions immediately. No greetings, no filler, no preamble.
|
|
1445
|
+
- When the user gives a command, acknowledge briefly AND call the tool in the same turn.
|
|
1446
|
+
- Match the user's language automatically.
|
|
1447
|
+
- No emoji unless the user uses them first.
|
|
1448
|
+
- Output plain text only. Do NOT use Markdown formatting (no bold, no code blocks, no headings, no tables). Your output renders in a terminal that does not support Markdown rendering.
|
|
1449
|
+
|
|
1450
|
+
## Responding to worker signals
|
|
1451
|
+
Worker signals are NOT user messages. Do NOT respond to them the same way.
|
|
1452
|
+
See the "Handling signals" section for detailed rules. In short: stay silent on PROGRESS, speak only on COMPLETED or FAILED.
|
|
1453
|
+
|
|
1454
|
+
Examples of ideal responses to user messages:
|
|
1455
|
+
|
|
1456
|
+
User: 12 + 13 \u7B49\u4E8E\u591A\u5C11
|
|
1457
|
+
Assistant: 25
|
|
1458
|
+
|
|
1459
|
+
User: \u5E2E\u6211\u5728 src/ \u4E0B\u521B\u5EFA\u4E00\u4E2A hello.ts
|
|
1460
|
+
Assistant: \u597D\uFF0C\u9A6C\u4E0A\u521B\u5EFA\u3002
|
|
1461
|
+
[calls add_task]
|
|
1462
|
+
|
|
1463
|
+
User: \u4EFB\u52A1\u8FDB\u5C55\u5982\u4F55\uFF1F
|
|
1464
|
+
Assistant: \u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8FD8\u6CA1\u5B8C\u6210\u3002
|
|
1465
|
+
|
|
1466
|
+
User: hi
|
|
1467
|
+
Assistant: \u6709\u4EC0\u4E48\u9700\u8981\u5E2E\u5FD9\u7684\uFF1F`
|
|
1468
|
+
);
|
|
1469
|
+
var TOOL_USAGE = section(
|
|
1470
|
+
"Task delegation",
|
|
1471
|
+
`## When to create a task
|
|
1472
|
+
|
|
1473
|
+
Use add_task when the request requires **execution** \u2014 writing code, running commands, file operations, complex content generation, etc.
|
|
1474
|
+
|
|
1475
|
+
Answer directly (no task needed) for:
|
|
1476
|
+
- Simple questions, calculations, explanations
|
|
1477
|
+
- Clarification requests
|
|
1478
|
+
- Checking task status
|
|
1479
|
+
|
|
1480
|
+
## Choosing a worker type
|
|
1481
|
+
|
|
1482
|
+
Use the \`workerType\` parameter to choose the right worker:
|
|
1483
|
+
- \`puppet\` (default) \u2014 built-in worker for general tasks using shell commands and file I/O.
|
|
1484
|
+
- Configured worker IDs (e.g. \`claude-code\`) \u2014 specialized workers for specific domains.
|
|
1485
|
+
Check the "Available workers" section below for current options.
|
|
1486
|
+
|
|
1487
|
+
## How to write a good task description
|
|
1488
|
+
|
|
1489
|
+
The worker only sees the task description \u2014 it has NO access to your conversation.
|
|
1490
|
+
Write it as a **self-contained instruction**:
|
|
1491
|
+
1. What to do (specific, unambiguous)
|
|
1492
|
+
2. All context/data the worker needs
|
|
1493
|
+
3. Expected output (file path, format, etc.)
|
|
1494
|
+
4. Constraints (language, length, style)
|
|
1495
|
+
|
|
1496
|
+
Bad: "Write the essay the user asked for"
|
|
1497
|
+
Good: "Write a 200-word Chinese prose essay about autumn. Save to sanwen.txt in UTF-8."
|
|
1498
|
+
|
|
1499
|
+
## After creating a task
|
|
1500
|
+
|
|
1501
|
+
Give the user a brief one-line acknowledgement, then STOP. Do not speculate about results.`
|
|
1502
|
+
);
|
|
1503
|
+
var SIGNAL_HANDLING = section(
|
|
1504
|
+
"Handling signals",
|
|
1505
|
+
`Worker signals are delivered as messages with a special prefix. They are NOT from the user.
|
|
1506
|
+
|
|
1507
|
+
Format: [WORKER SIGNAL: TYPE task=TASK_ID] content
|
|
1508
|
+
|
|
1509
|
+
## [WORKER SIGNAL: PROGRESS ...] \u2014 task is still running
|
|
1510
|
+
|
|
1511
|
+
Call \`skip_reply()\` IMMEDIATELY as your ONLY action. Do not output any text before, after, or alongside it.
|
|
1512
|
+
|
|
1513
|
+
\u2705 Correct response to ANY progress signal:
|
|
1514
|
+
\u2192 call skip_reply() (nothing else)
|
|
1515
|
+
|
|
1516
|
+
\u274C Wrong responses:
|
|
1517
|
+
\u2192 "\u6B63\u5728\u5199\u4F5C\u4E2D" then call skip_reply() (text before tool)
|
|
1518
|
+
\u2192 call skip_reply() then "\u8BF7\u7A0D\u5019" (text after tool)
|
|
1519
|
+
\u2192 "\u5C0F\u8BF4\u5DF2\u5B8C\u6210\uFF01\u4FDD\u5B58\u5728 novel.txt" (fabricating result)
|
|
1520
|
+
\u2192 call list_tasks() (polling)
|
|
1521
|
+
|
|
1522
|
+
The ONLY exception: speak up if the progress indicates an unexpected error or problem.
|
|
1523
|
+
|
|
1524
|
+
## [WORKER SIGNAL: COMPLETED ...] \u2014 task is finished
|
|
1525
|
+
|
|
1526
|
+
NOW report the result to the user. Summarize concisely.
|
|
1527
|
+
If the result says "(max steps reached)" or is empty, be honest \u2014 the task did not fully complete.
|
|
1528
|
+
|
|
1529
|
+
## [WORKER SIGNAL: FAILED ...] \u2014 task failed
|
|
1530
|
+
|
|
1531
|
+
Explain the error. Suggest a retry or alternative.
|
|
1532
|
+
|
|
1533
|
+
CRITICAL RULES:
|
|
1534
|
+
- PROGRESS \u2260 COMPLETED. Even if the progress text says "\u5B8C\u6210" or "done", it is still just a progress update. Only [WORKER SIGNAL: COMPLETED] means the task actually finished.
|
|
1535
|
+
- Do NOT call list_tasks or get_task_detail after receiving a signal. The signal already contains the information.
|
|
1536
|
+
- When handling PROGRESS, skip_reply must be the ONLY tool call with NO text output. The system will stop execution after skip_reply.`
|
|
1537
|
+
);
|
|
1538
|
+
var HARD_CONSTRAINTS = section(
|
|
1539
|
+
"Hard constraints",
|
|
1540
|
+
`- NEVER execute commands or write files yourself. Delegate via add_task.
|
|
1541
|
+
- NEVER poll or busy-wait for task completion.
|
|
1542
|
+
- NEVER fabricate results. Only report what you actually received.
|
|
1543
|
+
- NEVER ignore errors \u2014 always surface them.
|
|
1544
|
+
- NEVER mention internal concepts (Controller, Worker, signals) to the user. You are just "Wayang".`
|
|
1545
|
+
);
|
|
1546
|
+
function buildEnvironment() {
|
|
1547
|
+
const now = /* @__PURE__ */ new Date();
|
|
1548
|
+
const date = now.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
1549
|
+
const weekday = now.toLocaleDateString("en-US", { weekday: "long" });
|
|
1550
|
+
return section("Environment", `- Date: ${date} (${weekday})`);
|
|
1551
|
+
}
|
|
1552
|
+
function buildWorkerList(workers) {
|
|
1553
|
+
const lines = [`- \`puppet\` \u2014 ${PUPPET_DEFAULTS.description} [${PUPPET_DEFAULTS.capabilities.join(", ")}]`];
|
|
1554
|
+
if (workers) {
|
|
1555
|
+
for (const [id, config] of Object.entries(workers)) {
|
|
1556
|
+
const caps = config.capabilities?.length ? ` [${config.capabilities.join(", ")}]` : "";
|
|
1557
|
+
lines.push(`- \`${id}\` \u2014 ${config.description}${caps}`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return section("Available workers", lines.join("\n"));
|
|
1561
|
+
}
|
|
1562
|
+
function buildControllerSystemPrompt(ctx) {
|
|
1563
|
+
return assemble(
|
|
1564
|
+
IDENTITY,
|
|
1565
|
+
RESPONSE_STYLE,
|
|
1566
|
+
TOOL_USAGE,
|
|
1567
|
+
SIGNAL_HANDLING,
|
|
1568
|
+
HARD_CONSTRAINTS,
|
|
1569
|
+
buildWorkerList(ctx?.workers),
|
|
1570
|
+
buildEnvironment()
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/services/agents/prompts/worker-prompt.ts
|
|
1575
|
+
var IDENTITY2 = section(
|
|
1576
|
+
"Identity",
|
|
1577
|
+
`You are a Wayang Worker \u2014 an autonomous task executor in a multi-agent system.
|
|
1578
|
+
You receive a single task description as your first message. Your job is to complete it using the available tools, then report the outcome.`
|
|
1579
|
+
);
|
|
1580
|
+
var LANGUAGE_RULES = section(
|
|
1581
|
+
"Language",
|
|
1582
|
+
`- Match the language of the task description. If the task is in Chinese, think and output in Chinese. If in English, use English.
|
|
1583
|
+
- Internal tool arguments (file content, code, commands) should use the language appropriate for the content \u2014 code comments in English, prose in the task's language.
|
|
1584
|
+
- Progress reports and done/fail summaries should match the task's language.`
|
|
1585
|
+
);
|
|
1586
|
+
var TOOL_USAGE2 = section(
|
|
1587
|
+
"Tool usage",
|
|
1588
|
+
`## Mandatory termination
|
|
1589
|
+
|
|
1590
|
+
Every run MUST end with exactly one call to \`done\` or \`fail\`. No exceptions.
|
|
1591
|
+
- Call \`done(summary)\` when the task is completed successfully.
|
|
1592
|
+
- Call \`fail(error)\` when you cannot complete the task.
|
|
1593
|
+
- Never end without calling one of these \u2014 the system cannot detect completion otherwise.`
|
|
1594
|
+
);
|
|
1595
|
+
var EXECUTION_STRATEGY = section(
|
|
1596
|
+
"Execution strategy",
|
|
1597
|
+
`## Planning
|
|
1598
|
+
|
|
1599
|
+
Before acting, briefly plan your approach (you have a step budget \u2014 use it wisely):
|
|
1600
|
+
1. Understand the task requirements
|
|
1601
|
+
2. Break it into concrete steps
|
|
1602
|
+
3. Execute each step, verifying the result before moving on
|
|
1603
|
+
|
|
1604
|
+
## File operations
|
|
1605
|
+
|
|
1606
|
+
- Always verify write results: after write_file, use read_file to confirm content is correct.
|
|
1607
|
+
- Use absolute or workspace-relative paths. Do not guess paths \u2014 use bash(ls) or read_file to discover them.
|
|
1608
|
+
- When creating files, include ALL the content in a single write_file call. Never write an empty file intending to fill it later.
|
|
1609
|
+
|
|
1610
|
+
## Shell commands
|
|
1611
|
+
|
|
1612
|
+
- Prefer simple, well-known commands. Avoid complex pipelines when individual commands suffice.
|
|
1613
|
+
- Always check command output for errors before proceeding.
|
|
1614
|
+
- If a command fails, analyze stderr and try an alternative approach.
|
|
1615
|
+
- Long-running commands (builds, installs) may hit the 30s timeout \u2014 break them into smaller steps if needed.
|
|
1616
|
+
|
|
1617
|
+
## Error recovery
|
|
1618
|
+
|
|
1619
|
+
- If a tool call fails, do NOT repeat the same call blindly. Analyze the error and adjust.
|
|
1620
|
+
- After 2 failed attempts at the same approach, try a different strategy.
|
|
1621
|
+
- If the task is fundamentally impossible (missing dependencies, permission denied, etc.), call fail() with a clear explanation.`
|
|
1622
|
+
);
|
|
1623
|
+
var QUALITY_RULES = section(
|
|
1624
|
+
"Quality rules",
|
|
1625
|
+
`- Produce complete, working output. No placeholders, TODOs, or "fill in later" stubs.
|
|
1626
|
+
- When writing code: ensure it compiles/runs. When writing prose: meet the requested length and quality.
|
|
1627
|
+
- When writing files: include the FULL content. Never write partial content expecting to append later.
|
|
1628
|
+
- Verify your work before calling done(). Read back files you wrote. Run code you generated.
|
|
1629
|
+
- Output plain text only. Do NOT use Markdown formatting (no bold, no code blocks, no headings, no tables). Your output renders in a terminal.`
|
|
1630
|
+
);
|
|
1631
|
+
var HARD_CONSTRAINTS2 = section(
|
|
1632
|
+
"Hard constraints",
|
|
1633
|
+
`- MUST call done() or fail() to terminate. Never just stop.
|
|
1634
|
+
- NEVER write empty files. If you call write_file, the content parameter must contain the actual content.
|
|
1635
|
+
- NEVER exceed your step budget silently \u2014 if you're running out of steps, call done() with partial results or fail() explaining the limitation.
|
|
1636
|
+
- NEVER run destructive commands (rm -rf /, DROP DATABASE, etc.) without explicit instruction in the task.
|
|
1637
|
+
- ALL file operations (read_file, write_file) are sandboxed to the workspace directory. Paths outside will be rejected. Use relative paths or paths under the workspace shown in the Environment section.
|
|
1638
|
+
- bash commands run with cwd set to the workspace. Do NOT cd out of it or write to paths outside it.`
|
|
1639
|
+
);
|
|
1640
|
+
var PROGRESS_REPORTING = section(
|
|
1641
|
+
"Progress reporting",
|
|
1642
|
+
`Use update_progress sparingly \u2014 only at moments the user would genuinely want to know about.
|
|
1643
|
+
|
|
1644
|
+
## GOOD times to report:
|
|
1645
|
+
- Task has multiple phases: report when entering the next phase (e.g., "Dependencies installed, starting build")
|
|
1646
|
+
- A long-running step begins (> 5s expected): "Running tests..."
|
|
1647
|
+
- You recovered from an error: "write_file failed, retrying with corrected path"
|
|
1648
|
+
- A sub-deliverable is ready: "Database schema created, now seeding data"
|
|
1649
|
+
|
|
1650
|
+
## BAD times to report (do NOT):
|
|
1651
|
+
- Every individual tool call ("Reading file X", "Writing file Y")
|
|
1652
|
+
- Trivial progress that adds no information ("Working on it...", "Almost done")
|
|
1653
|
+
- Restating the task description
|
|
1654
|
+
- Right before calling done() or fail(). The done/fail summary IS the final report \u2014 do not duplicate it as a progress message
|
|
1655
|
+
|
|
1656
|
+
## Summary quality
|
|
1657
|
+
The summary in done() is what the user sees as the final result. Make it specific:
|
|
1658
|
+
|
|
1659
|
+
Bad: "Task completed successfully"
|
|
1660
|
+
Good: "Created sanwen.txt (218 words, prose about autumn rain)"
|
|
1661
|
+
|
|
1662
|
+
Bad: "Failed"
|
|
1663
|
+
Good: "Failed: npm install timed out after 30s, network may be unreachable"`
|
|
1664
|
+
);
|
|
1665
|
+
function buildEnvironment2(ctx) {
|
|
1666
|
+
const now = /* @__PURE__ */ new Date();
|
|
1667
|
+
const date = now.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
1668
|
+
const lines = [];
|
|
1669
|
+
lines.push(`- Date: ${date}`);
|
|
1670
|
+
lines.push(`- Task ID: ${ctx.taskId}`);
|
|
1671
|
+
lines.push(`- Workspace: ${ctx.workspaceDir}`);
|
|
1672
|
+
return section("Environment", lines.join("\n"));
|
|
1673
|
+
}
|
|
1674
|
+
function buildWorkerSystemPrompt(ctx) {
|
|
1675
|
+
return assemble(
|
|
1676
|
+
IDENTITY2,
|
|
1677
|
+
LANGUAGE_RULES,
|
|
1678
|
+
TOOL_USAGE2,
|
|
1679
|
+
EXECUTION_STRATEGY,
|
|
1680
|
+
QUALITY_RULES,
|
|
1681
|
+
HARD_CONSTRAINTS2,
|
|
1682
|
+
PROGRESS_REPORTING,
|
|
1683
|
+
ctx ? buildEnvironment2(ctx) : void 0
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// src/services/agents/prompts/third-party-prompt.ts
|
|
1688
|
+
var RULES = [
|
|
1689
|
+
"<Rules>",
|
|
1690
|
+
"Output plain text only. No Markdown formatting.",
|
|
1691
|
+
"Report meaningful progress as you work (entering a new phase, completing a sub-step, recovering from an error).",
|
|
1692
|
+
"Do NOT output a final summary right before completing \u2014 the system already captures the result separately.",
|
|
1693
|
+
"Match the language of the task description.",
|
|
1694
|
+
"</Rules>"
|
|
1695
|
+
].join("\n");
|
|
1696
|
+
function buildThirdPartyPrompt() {
|
|
1697
|
+
return `${RULES}
|
|
1698
|
+
|
|
1699
|
+
---
|
|
1700
|
+
|
|
1701
|
+
`;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// src/services/agents/prompts/summarizer.ts
|
|
1705
|
+
var SUMMARIZER_SYSTEM_PROMPT = `You are a conversation summarizer for a multi-agent orchestration system. Create a concise but complete summary of the conversation entries below.
|
|
1706
|
+
|
|
1707
|
+
Requirements:
|
|
1708
|
+
- Preserve all key decisions, task assignments, and outcomes
|
|
1709
|
+
- Preserve task IDs, worker IDs, and file paths mentioned
|
|
1710
|
+
- Note any errors or failures and their resolution status
|
|
1711
|
+
- Keep the summary under 500 words
|
|
1712
|
+
- Use bullet points for clarity`;
|
|
1713
|
+
function buildSummarizerPrompt(entryTexts) {
|
|
1714
|
+
return `Summarize the following ${entryTexts.length} conversation entries. Focus on preserving actionable context \u2014 decisions, task IDs, outcomes, and any unresolved issues.
|
|
1715
|
+
|
|
1716
|
+
${entryTexts.join("\n")}`;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/services/agents/controller-agent.ts
|
|
1720
|
+
var ControllerAgent = class extends BaseAgent {
|
|
1721
|
+
state;
|
|
1722
|
+
contextManager;
|
|
1723
|
+
tools;
|
|
1724
|
+
logger;
|
|
1725
|
+
constructor(state, provider, tools, logger, workerConfigs) {
|
|
1726
|
+
super(provider);
|
|
1727
|
+
this.state = state;
|
|
1728
|
+
this.tools = tools;
|
|
1729
|
+
this.logger = logger;
|
|
1730
|
+
this.contextManager = new ContextManager(
|
|
1731
|
+
state,
|
|
1732
|
+
buildControllerSystemPrompt({ workers: workerConfigs }),
|
|
1733
|
+
() => ""
|
|
1734
|
+
);
|
|
1735
|
+
this.setHooks({
|
|
1736
|
+
beforeLLM: ({ messages }) => {
|
|
1737
|
+
this.logger.debug({ messages }, "LLM call start");
|
|
1738
|
+
},
|
|
1739
|
+
afterLLM: ({ step, usage, durationMs }) => {
|
|
1740
|
+
this.logger.info({ agent: "controller", step, usage, durationMs }, "LLM call done");
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
/** Run the controller agent — streams text chunks while updating streamingEntries in state. */
|
|
1745
|
+
async run(signals) {
|
|
1746
|
+
this.state.set("dynamicState.busy", true);
|
|
1747
|
+
try {
|
|
1748
|
+
return await this._run(signals);
|
|
1749
|
+
} finally {
|
|
1750
|
+
this.state.set("dynamicState.busy", false);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async _run(signals) {
|
|
1754
|
+
const ts = nowISO();
|
|
1755
|
+
for (const sig of signals) {
|
|
1756
|
+
if (sig.type === "input") {
|
|
1757
|
+
const text = sig.payload.text;
|
|
1758
|
+
this.state.append("conversation", {
|
|
1759
|
+
type: "user" /* User */,
|
|
1760
|
+
uuid: generateId("u"),
|
|
1761
|
+
parentUuid: null,
|
|
1762
|
+
sessionId: "controller",
|
|
1763
|
+
timestamp: ts,
|
|
1764
|
+
message: { role: "user", content: text }
|
|
1765
|
+
});
|
|
1766
|
+
} else if (sig.type === "completed") {
|
|
1767
|
+
this.state.append("conversation", {
|
|
1768
|
+
type: "signal" /* Signal */,
|
|
1769
|
+
uuid: generateId("sig"),
|
|
1770
|
+
parentUuid: null,
|
|
1771
|
+
sessionId: "controller",
|
|
1772
|
+
timestamp: ts,
|
|
1773
|
+
subtype: "worker_completed" /* WorkerCompleted */,
|
|
1774
|
+
workerId: sig.payload.workerId,
|
|
1775
|
+
workerType: sig.payload.workerType,
|
|
1776
|
+
emoji: sig.payload.emoji,
|
|
1777
|
+
taskId: sig.payload.taskId,
|
|
1778
|
+
taskTitle: sig.payload.taskTitle,
|
|
1779
|
+
content: sig.payload.summary ?? JSON.stringify(sig.payload)
|
|
1780
|
+
});
|
|
1781
|
+
} else if (sig.type === "failed") {
|
|
1782
|
+
this.state.append("conversation", {
|
|
1783
|
+
type: "signal" /* Signal */,
|
|
1784
|
+
uuid: generateId("sig"),
|
|
1785
|
+
parentUuid: null,
|
|
1786
|
+
sessionId: "controller",
|
|
1787
|
+
timestamp: ts,
|
|
1788
|
+
subtype: "worker_failed" /* WorkerFailed */,
|
|
1789
|
+
workerId: sig.payload.workerId,
|
|
1790
|
+
workerType: sig.payload.workerType,
|
|
1791
|
+
emoji: sig.payload.emoji,
|
|
1792
|
+
taskId: sig.payload.taskId,
|
|
1793
|
+
taskTitle: sig.payload.taskTitle,
|
|
1794
|
+
content: sig.payload.error
|
|
1795
|
+
});
|
|
1796
|
+
} else if (sig.type === "progress") {
|
|
1797
|
+
this.state.append("conversation", {
|
|
1798
|
+
type: "signal" /* Signal */,
|
|
1799
|
+
uuid: generateId("sig"),
|
|
1800
|
+
parentUuid: null,
|
|
1801
|
+
sessionId: "controller",
|
|
1802
|
+
timestamp: ts,
|
|
1803
|
+
subtype: "worker_progress" /* WorkerProgress */,
|
|
1804
|
+
workerId: sig.payload.workerId,
|
|
1805
|
+
workerType: sig.payload.workerType,
|
|
1806
|
+
emoji: sig.payload.emoji,
|
|
1807
|
+
taskId: sig.payload.taskId,
|
|
1808
|
+
taskTitle: sig.payload.taskTitle,
|
|
1809
|
+
content: sig.payload.message
|
|
1810
|
+
});
|
|
1811
|
+
} else {
|
|
1812
|
+
this.state.append("conversation", {
|
|
1813
|
+
type: "signal" /* Signal */,
|
|
1814
|
+
uuid: generateId("sig"),
|
|
1815
|
+
parentUuid: null,
|
|
1816
|
+
sessionId: "controller",
|
|
1817
|
+
timestamp: ts,
|
|
1818
|
+
subtype: "worker_failed" /* WorkerFailed */,
|
|
1819
|
+
workerId: sig.payload.workerId,
|
|
1820
|
+
workerType: sig.payload.workerType,
|
|
1821
|
+
emoji: sig.payload.emoji,
|
|
1822
|
+
taskId: sig.payload.taskId,
|
|
1823
|
+
content: JSON.stringify(sig.payload)
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
this.state.set("dynamicState.streamingEntries", []);
|
|
1828
|
+
let streamingEntry = this.createStreamingEntry();
|
|
1829
|
+
const gen = this.streamLoop({
|
|
1830
|
+
system: this.contextManager.getSystemPrompt(),
|
|
1831
|
+
messages: this.contextManager.getMessages(),
|
|
1832
|
+
tools: this.tools,
|
|
1833
|
+
maxSteps: 50,
|
|
1834
|
+
stopTools: ["skip_reply"],
|
|
1835
|
+
onStep: (event) => {
|
|
1836
|
+
const streamingEntries = this.state.get("dynamicState.streamingEntries");
|
|
1837
|
+
const streamingUuid = streamingEntries?.[0]?.uuid;
|
|
1838
|
+
const entry = buildAssistantEntry(event, this.id, streamingUuid);
|
|
1839
|
+
this.state.append("conversation", entry);
|
|
1840
|
+
this.state.set("dynamicState.streamingEntries", []);
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
let fullText = "";
|
|
1844
|
+
for await (const chunk of gen) {
|
|
1845
|
+
if (chunk) {
|
|
1846
|
+
const current = this.state.get("dynamicState.streamingEntries");
|
|
1847
|
+
if (!current || current.length === 0) {
|
|
1848
|
+
streamingEntry = this.createStreamingEntry();
|
|
1849
|
+
}
|
|
1850
|
+
streamingEntry.message.content += chunk;
|
|
1851
|
+
fullText += chunk;
|
|
1852
|
+
this.state.set("dynamicState.streamingEntries", [streamingEntry]);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return { text: fullText };
|
|
1856
|
+
}
|
|
1857
|
+
/** Create a fresh streaming entry placeholder. */
|
|
1858
|
+
createStreamingEntry() {
|
|
1859
|
+
return {
|
|
1860
|
+
type: "assistant" /* Assistant */,
|
|
1861
|
+
uuid: generateId("s"),
|
|
1862
|
+
parentUuid: null,
|
|
1863
|
+
sessionId: "controller",
|
|
1864
|
+
timestamp: nowISO(),
|
|
1865
|
+
message: { role: "assistant", content: "" }
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
/** Check if context needs compaction. */
|
|
1869
|
+
needsCompaction() {
|
|
1870
|
+
return this.contextManager.isFull();
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Run compaction — uses the LLM to summarize conversation entries.
|
|
1874
|
+
*
|
|
1875
|
+
* Retries up to COMPACT_MAX_RETRIES times on failure.
|
|
1876
|
+
* Falls back to truncating half of the history in memory (file preserved).
|
|
1877
|
+
*/
|
|
1878
|
+
async performCompaction() {
|
|
1879
|
+
try {
|
|
1880
|
+
await this.contextManager.compact(async (entries) => {
|
|
1881
|
+
const entryTexts = entries.map((e, i) => {
|
|
1882
|
+
const content = e.message?.content ?? e.content ?? "";
|
|
1883
|
+
return `[${i}] (${e.type}) ${String(content).slice(0, 300)}`;
|
|
1884
|
+
});
|
|
1885
|
+
let lastError;
|
|
1886
|
+
for (let attempt = 0; attempt <= COMPACT_MAX_RETRIES; attempt++) {
|
|
1887
|
+
try {
|
|
1888
|
+
const { text } = await generateText({
|
|
1889
|
+
model: this.model,
|
|
1890
|
+
// WayangModel opaque boundary
|
|
1891
|
+
system: SUMMARIZER_SYSTEM_PROMPT,
|
|
1892
|
+
prompt: buildSummarizerPrompt(entryTexts)
|
|
1893
|
+
});
|
|
1894
|
+
return text;
|
|
1895
|
+
} catch (err) {
|
|
1896
|
+
lastError = err;
|
|
1897
|
+
this.logger.warn({ attempt: attempt + 1, error: String(err) }, "Compaction summarizer failed");
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
throw lastError;
|
|
1901
|
+
});
|
|
1902
|
+
} catch (err) {
|
|
1903
|
+
this.logger.warn({ error: String(err) }, "Compaction failed, falling back to half-truncation");
|
|
1904
|
+
const conversation = this.state.get("conversation");
|
|
1905
|
+
if (conversation.length > 4) {
|
|
1906
|
+
const half = Math.floor(conversation.length / 2);
|
|
1907
|
+
this.state.set("conversation", conversation.slice(-half));
|
|
1908
|
+
this.logger.info({ kept: half, total: conversation.length }, "History truncated (half) as compaction fallback");
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
// src/services/tools/add-task.ts
|
|
1915
|
+
import { z } from "zod";
|
|
1916
|
+
|
|
1917
|
+
// src/services/tools/common.ts
|
|
1918
|
+
import { tool } from "ai";
|
|
1919
|
+
var MAX_RESULT_CHARS = 8e3;
|
|
1920
|
+
function truncate(result) {
|
|
1921
|
+
if (result.length <= MAX_RESULT_CHARS) return result;
|
|
1922
|
+
return result.slice(0, MAX_RESULT_CHARS) + "\n...(truncated)";
|
|
1923
|
+
}
|
|
1924
|
+
function safeExecute(name, fn) {
|
|
1925
|
+
return async (args, _options) => {
|
|
1926
|
+
try {
|
|
1927
|
+
const result = await fn(args);
|
|
1928
|
+
return truncate(typeof result === "string" ? result : JSON.stringify(result));
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
return `[ERROR] ${name}: ${err.message ?? String(err)}`;
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
function defineTool(def) {
|
|
1935
|
+
const { parameters, ...rest } = def;
|
|
1936
|
+
return tool({ ...rest, inputSchema: parameters });
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/services/tools/add-task.ts
|
|
1940
|
+
function addTaskTool(deps) {
|
|
1941
|
+
return defineTool({
|
|
1942
|
+
description: "Add a new task to the queue. A Worker will execute it automatically.",
|
|
1943
|
+
parameters: z.object({
|
|
1944
|
+
title: z.string().max(10).describe("Brief task title, max 10 characters"),
|
|
1945
|
+
description: z.string().describe("Detailed task description for the Worker"),
|
|
1946
|
+
priority: z.enum(["normal", "high"]).default("normal").describe("Priority level"),
|
|
1947
|
+
workerType: z.string().optional().describe('Worker type: "puppet" (default, built-in LLM) or a configured worker ID (e.g. "claude-code")')
|
|
1948
|
+
}),
|
|
1949
|
+
execute: safeExecute("add_task", async ({ title, description, priority, workerType }) => {
|
|
1950
|
+
const resolvedType = workerType || "puppet";
|
|
1951
|
+
if (deps.validateWorkerType) {
|
|
1952
|
+
const err = deps.validateWorkerType(resolvedType);
|
|
1953
|
+
if (err) return err;
|
|
1954
|
+
}
|
|
1955
|
+
const task = {
|
|
1956
|
+
id: generateId("t"),
|
|
1957
|
+
title,
|
|
1958
|
+
description,
|
|
1959
|
+
priority,
|
|
1960
|
+
workerType: resolvedType,
|
|
1961
|
+
status: "pending",
|
|
1962
|
+
createdAt: Date.now()
|
|
1963
|
+
};
|
|
1964
|
+
deps.addTask(task);
|
|
1965
|
+
return `Task ${task.id} added (priority: ${priority}, worker: ${task.workerType}). You will be notified when the task completes.`;
|
|
1966
|
+
})
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/services/tools/list-tasks.ts
|
|
1971
|
+
import { z as z2 } from "zod";
|
|
1972
|
+
function listTasksTool(deps) {
|
|
1973
|
+
return defineTool({
|
|
1974
|
+
description: "View the task list",
|
|
1975
|
+
parameters: z2.object({
|
|
1976
|
+
status: z2.enum(["pending", "running", "completed", "failed", "cancelled"]).optional().describe("Filter by status")
|
|
1977
|
+
}),
|
|
1978
|
+
execute: safeExecute("list_tasks", async ({ status }) => {
|
|
1979
|
+
const tasks = deps.listTasks(status);
|
|
1980
|
+
if (tasks.length === 0) return status ? `No ${status} tasks` : "No tasks";
|
|
1981
|
+
return tasks.map(
|
|
1982
|
+
(t) => `[${t.status}] ${t.id}: ${t.description}${t.result ? ` \u2192 ${t.result}` : ""}${t.error ? ` \u2717 ${t.error}` : ""}`
|
|
1983
|
+
).join("\n");
|
|
1984
|
+
})
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// src/services/tools/cancel-task.ts
|
|
1989
|
+
import { z as z3 } from "zod";
|
|
1990
|
+
function cancelTaskTool(deps) {
|
|
1991
|
+
return defineTool({
|
|
1992
|
+
description: "Cancel a running or pending task",
|
|
1993
|
+
parameters: z3.object({
|
|
1994
|
+
taskId: z3.string().describe("Task ID to cancel")
|
|
1995
|
+
}),
|
|
1996
|
+
execute: safeExecute("cancel_task", async ({ taskId }) => {
|
|
1997
|
+
if (deps.abortWorker) {
|
|
1998
|
+
deps.abortWorker(taskId);
|
|
1999
|
+
}
|
|
2000
|
+
const cancelled = deps.cancelTask(taskId);
|
|
2001
|
+
if (!cancelled) return `Task ${taskId} not found or already completed`;
|
|
2002
|
+
return `Task ${taskId} cancelled`;
|
|
2003
|
+
})
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// src/services/tools/get-task-detail.ts
|
|
2008
|
+
import { z as z4 } from "zod";
|
|
2009
|
+
function getTaskDetailTool(deps) {
|
|
2010
|
+
return defineTool({
|
|
2011
|
+
description: "Get detailed information about a specific task",
|
|
2012
|
+
parameters: z4.object({
|
|
2013
|
+
taskId: z4.string().describe("Task ID")
|
|
2014
|
+
}),
|
|
2015
|
+
execute: safeExecute("get_task_detail", async ({ taskId }) => {
|
|
2016
|
+
const task = deps.getTask(taskId);
|
|
2017
|
+
if (!task) return `Task ${taskId} not found`;
|
|
2018
|
+
const lines = [
|
|
2019
|
+
`ID: ${task.id}`,
|
|
2020
|
+
`Status: ${task.status}`,
|
|
2021
|
+
`Priority: ${task.priority}`,
|
|
2022
|
+
`Description: ${task.description}`,
|
|
2023
|
+
`Created: ${new Date(task.createdAt).toISOString()}`
|
|
2024
|
+
];
|
|
2025
|
+
if (task.startedAt) lines.push(`Started: ${new Date(task.startedAt).toISOString()}`);
|
|
2026
|
+
if (task.completedAt) lines.push(`Completed: ${new Date(task.completedAt).toISOString()}`);
|
|
2027
|
+
if (task.result) lines.push(`Result: ${task.result}`);
|
|
2028
|
+
if (task.error) lines.push(`Error: ${task.error}`);
|
|
2029
|
+
if (task.workerSessionId) lines.push(`Worker: ${task.workerSessionId}`);
|
|
2030
|
+
if (deps.getWorkerConversation) {
|
|
2031
|
+
const entries = deps.getWorkerConversation(taskId);
|
|
2032
|
+
if (entries.length > 0) {
|
|
2033
|
+
lines.push(`
|
|
2034
|
+
Conversation (${entries.length} entries):`);
|
|
2035
|
+
for (const entry of entries.slice(-10)) {
|
|
2036
|
+
lines.push(` [${entry.type}] ${entry.message?.content ?? entry.content ?? ""}`.slice(0, 200));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return lines.join("\n");
|
|
2041
|
+
})
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/services/tools/update-task.ts
|
|
2046
|
+
import { z as z5 } from "zod";
|
|
2047
|
+
function updateTaskTool(deps) {
|
|
2048
|
+
return defineTool({
|
|
2049
|
+
description: "Update a pending task description or priority",
|
|
2050
|
+
parameters: z5.object({
|
|
2051
|
+
taskId: z5.string().describe("Task ID to update"),
|
|
2052
|
+
description: z5.string().optional().describe("New task description"),
|
|
2053
|
+
priority: z5.enum(["normal", "high"]).optional().describe("New priority")
|
|
2054
|
+
}),
|
|
2055
|
+
execute: safeExecute("update_task", async ({ taskId, description, priority }) => {
|
|
2056
|
+
if (!description && !priority) {
|
|
2057
|
+
return "Nothing to update \u2014 provide description or priority";
|
|
2058
|
+
}
|
|
2059
|
+
const updated = deps.updateTask(taskId, { description, priority });
|
|
2060
|
+
if (!updated) return `Task ${taskId} not found or not in pending status`;
|
|
2061
|
+
return `Task ${taskId} updated`;
|
|
2062
|
+
})
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// src/services/tools/query-signals.ts
|
|
2067
|
+
import { z as z6 } from "zod";
|
|
2068
|
+
function querySignalsTool(deps) {
|
|
2069
|
+
return defineTool({
|
|
2070
|
+
description: "Query signal history by status, source, or type",
|
|
2071
|
+
parameters: z6.object({
|
|
2072
|
+
status: z6.enum(["unread", "read", "discarded"]).optional().describe("Filter by status"),
|
|
2073
|
+
source: z6.enum(["user", "worker", "system"]).optional().describe("Filter by source"),
|
|
2074
|
+
type: z6.enum(["input", "completed", "failed", "progress", "cancelled"]).optional().describe("Filter by type")
|
|
2075
|
+
}),
|
|
2076
|
+
execute: safeExecute("query_signals", async (filter) => {
|
|
2077
|
+
const signals = deps.querySignals(filter);
|
|
2078
|
+
if (signals.length === 0) return "No matching signals";
|
|
2079
|
+
return signals.map(
|
|
2080
|
+
(s) => `[${s.status}] ${s.source}/${s.type} ${s.id}: ${JSON.stringify(s.payload).slice(0, 150)}`
|
|
2081
|
+
).join("\n");
|
|
2082
|
+
})
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/services/tools/skip-reply.ts
|
|
2087
|
+
import { z as z7 } from "zod";
|
|
2088
|
+
function skipReplyTool() {
|
|
2089
|
+
return defineTool({
|
|
2090
|
+
description: "Acknowledge a signal without responding to the user. Call this when you receive a PROGRESS signal and have nothing meaningful to say. The user will not see anything.",
|
|
2091
|
+
parameters: z7.object({}),
|
|
2092
|
+
execute: safeExecute("skip_reply", async () => {
|
|
2093
|
+
return "(skipped)";
|
|
2094
|
+
})
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// src/services/tools/bash.ts
|
|
2099
|
+
import { z as z8 } from "zod";
|
|
2100
|
+
import { execSync } from "child_process";
|
|
2101
|
+
function bashTool(deps) {
|
|
2102
|
+
return defineTool({
|
|
2103
|
+
description: "Execute a shell command and return its output",
|
|
2104
|
+
parameters: z8.object({
|
|
2105
|
+
command: z8.string().describe("Shell command to execute")
|
|
2106
|
+
}),
|
|
2107
|
+
execute: safeExecute("bash", async ({ command }) => {
|
|
2108
|
+
try {
|
|
2109
|
+
const output = execSync(command, {
|
|
2110
|
+
cwd: deps.cwd,
|
|
2111
|
+
timeout: 3e4,
|
|
2112
|
+
maxBuffer: 1024 * 1024,
|
|
2113
|
+
encoding: "utf-8"
|
|
2114
|
+
});
|
|
2115
|
+
return output || "(no output)";
|
|
2116
|
+
} catch (err) {
|
|
2117
|
+
const stderr = err.stderr?.toString() || "";
|
|
2118
|
+
return `Exit code ${err.status ?? 1}
|
|
2119
|
+
${stderr}`;
|
|
2120
|
+
}
|
|
2121
|
+
})
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/services/tools/read-file.ts
|
|
2126
|
+
import { z as z9 } from "zod";
|
|
2127
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2128
|
+
import { resolve, relative } from "path";
|
|
2129
|
+
function validatePath(resolved, workspace) {
|
|
2130
|
+
const rel = relative(workspace, resolved);
|
|
2131
|
+
if (rel.startsWith("..") || resolve(workspace, rel) !== resolved) {
|
|
2132
|
+
return `Path escapes workspace: ${resolved} (workspace: ${workspace})`;
|
|
2133
|
+
}
|
|
2134
|
+
return null;
|
|
2135
|
+
}
|
|
2136
|
+
function readFileTool(deps = {}) {
|
|
2137
|
+
const workspace = deps.cwd ?? process.cwd();
|
|
2138
|
+
return defineTool({
|
|
2139
|
+
description: "Read file content. Path must be within the workspace.",
|
|
2140
|
+
parameters: z9.object({
|
|
2141
|
+
path: z9.string().describe("File path (relative to workspace)")
|
|
2142
|
+
}),
|
|
2143
|
+
execute: safeExecute("read_file", async ({ path: path5 }) => {
|
|
2144
|
+
const resolved = resolve(workspace, path5);
|
|
2145
|
+
const err = validatePath(resolved, workspace);
|
|
2146
|
+
if (err) return `[ERROR] read_file: ${err}`;
|
|
2147
|
+
return readFileSync3(resolved, "utf-8");
|
|
2148
|
+
})
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/services/tools/write-file.ts
|
|
2153
|
+
import { z as z10 } from "zod";
|
|
2154
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
2155
|
+
import { dirname as dirname3, resolve as resolve2, relative as relative2 } from "path";
|
|
2156
|
+
function validatePath2(resolved, workspace) {
|
|
2157
|
+
const rel = relative2(workspace, resolved);
|
|
2158
|
+
if (rel.startsWith("..") || resolve2(workspace, rel) !== resolved) {
|
|
2159
|
+
return `Path escapes workspace: ${resolved} (workspace: ${workspace})`;
|
|
2160
|
+
}
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
function writeFileTool(deps = {}) {
|
|
2164
|
+
const workspace = deps.cwd ?? process.cwd();
|
|
2165
|
+
return defineTool({
|
|
2166
|
+
description: "Write content to a file, creating parent directories as needed. Path must be within the workspace.",
|
|
2167
|
+
parameters: z10.object({
|
|
2168
|
+
path: z10.string().describe("File path (relative to workspace)"),
|
|
2169
|
+
content: z10.string().describe("File content")
|
|
2170
|
+
}),
|
|
2171
|
+
execute: safeExecute("write_file", async ({ path: path5, content }) => {
|
|
2172
|
+
const resolved = resolve2(workspace, path5);
|
|
2173
|
+
const err = validatePath2(resolved, workspace);
|
|
2174
|
+
if (err) return `[ERROR] write_file: ${err}`;
|
|
2175
|
+
mkdirSync3(dirname3(resolved), { recursive: true });
|
|
2176
|
+
writeFileSync3(resolved, content, "utf-8");
|
|
2177
|
+
return `Written ${content.length} chars to ${resolved}`;
|
|
2178
|
+
})
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/services/tools/done.ts
|
|
2183
|
+
import { z as z11 } from "zod";
|
|
2184
|
+
function doneTool(deps) {
|
|
2185
|
+
return defineTool({
|
|
2186
|
+
description: "Report task completion. Worker stops executing after this call.",
|
|
2187
|
+
parameters: z11.object({
|
|
2188
|
+
summary: z11.string().describe("Task completion summary")
|
|
2189
|
+
}),
|
|
2190
|
+
execute: safeExecute("done", async ({ summary }) => {
|
|
2191
|
+
deps.onComplete(summary);
|
|
2192
|
+
return `Task completed: ${summary}`;
|
|
2193
|
+
})
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// src/services/tools/fail.ts
|
|
2198
|
+
import { z as z12 } from "zod";
|
|
2199
|
+
function failTool(deps) {
|
|
2200
|
+
return defineTool({
|
|
2201
|
+
description: "Report task failure. Worker stops executing after this call.",
|
|
2202
|
+
parameters: z12.object({
|
|
2203
|
+
error: z12.string().describe("Error description")
|
|
2204
|
+
}),
|
|
2205
|
+
execute: safeExecute("fail", async ({ error }) => {
|
|
2206
|
+
deps.onFail(error);
|
|
2207
|
+
return `Task failed: ${error}`;
|
|
2208
|
+
})
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// src/services/tools/update-progress.ts
|
|
2213
|
+
import { z as z13 } from "zod";
|
|
2214
|
+
function updateProgressTool(deps) {
|
|
2215
|
+
return defineTool({
|
|
2216
|
+
description: "Report progress for the current task (replaces automatic step-based progress)",
|
|
2217
|
+
parameters: z13.object({
|
|
2218
|
+
message: z13.string().describe("Progress description"),
|
|
2219
|
+
percent: z13.number().min(0).max(100).optional().describe("Completion percentage (0-100)")
|
|
2220
|
+
}),
|
|
2221
|
+
execute: safeExecute("update_progress", async ({ message, percent }) => {
|
|
2222
|
+
deps.reportProgress(message, percent);
|
|
2223
|
+
const suffix = percent !== void 0 ? ` (${percent}%)` : "";
|
|
2224
|
+
return `Progress reported: ${message}${suffix}`;
|
|
2225
|
+
})
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// src/services/tools/index.ts
|
|
2230
|
+
function createControllerTools(deps) {
|
|
2231
|
+
return {
|
|
2232
|
+
add_task: addTaskTool({ addTask: deps.addTask, validateWorkerType: deps.validateWorkerType }),
|
|
2233
|
+
list_tasks: listTasksTool({ listTasks: deps.listTasks }),
|
|
2234
|
+
cancel_task: cancelTaskTool({
|
|
2235
|
+
cancelTask: deps.cancelTask,
|
|
2236
|
+
abortWorker: deps.abortWorker
|
|
2237
|
+
}),
|
|
2238
|
+
get_task_detail: getTaskDetailTool({
|
|
2239
|
+
getTask: deps.getTask
|
|
2240
|
+
}),
|
|
2241
|
+
update_task: updateTaskTool({ updateTask: deps.updateTask }),
|
|
2242
|
+
query_signals: querySignalsTool({ querySignals: deps.queryMessages }),
|
|
2243
|
+
skip_reply: skipReplyTool()
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function createWorkerTools(deps) {
|
|
2247
|
+
return {
|
|
2248
|
+
bash: bashTool({ cwd: deps.cwd }),
|
|
2249
|
+
read_file: readFileTool({ cwd: deps.cwd }),
|
|
2250
|
+
write_file: writeFileTool({ cwd: deps.cwd }),
|
|
2251
|
+
list_tasks: listTasksTool({ listTasks: deps.listTasks }),
|
|
2252
|
+
update_progress: updateProgressTool({ reportProgress: deps.reportProgress }),
|
|
2253
|
+
done: doneTool({ onComplete: deps.onComplete }),
|
|
2254
|
+
fail: failTool({ onFail: deps.onFail })
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// src/services/agents/worker-state.ts
|
|
2259
|
+
import { join as join6 } from "path";
|
|
2260
|
+
var WorkerState = class extends BaseWayangState {
|
|
2261
|
+
constructor(sessionDir, workerId, logger) {
|
|
2262
|
+
const workerDir = join6(sessionDir, "workers", workerId);
|
|
2263
|
+
const jsonFile = new JSONFileHelper(join6(workerDir, "runtime-state.json"));
|
|
2264
|
+
const jsonlFile = new JSONLFileHelper(join6(workerDir, "conversation.jsonl"));
|
|
2265
|
+
const initialData = {
|
|
2266
|
+
runtimeState: {
|
|
2267
|
+
session: { id: workerId, startedAt: Date.now() },
|
|
2268
|
+
task: { id: "", description: "" }
|
|
2269
|
+
},
|
|
2270
|
+
conversation: []
|
|
2271
|
+
};
|
|
2272
|
+
super(initialData, [
|
|
2273
|
+
{ path: "runtimeState", helper: jsonFile },
|
|
2274
|
+
{ path: "conversation", helper: jsonlFile }
|
|
2275
|
+
]);
|
|
2276
|
+
this.logger = logger;
|
|
2277
|
+
this.jsonFile = jsonFile;
|
|
2278
|
+
this.jsonlFile = jsonlFile;
|
|
2279
|
+
}
|
|
2280
|
+
logger;
|
|
2281
|
+
jsonFile;
|
|
2282
|
+
jsonlFile;
|
|
2283
|
+
async restore() {
|
|
2284
|
+
const runtimeState = this.jsonFile.read();
|
|
2285
|
+
if (runtimeState) {
|
|
2286
|
+
this.data.runtimeState = runtimeState;
|
|
2287
|
+
}
|
|
2288
|
+
const conversation = this.jsonlFile.read();
|
|
2289
|
+
if (conversation.length > 0) {
|
|
2290
|
+
this.data.conversation = conversation;
|
|
2291
|
+
}
|
|
2292
|
+
this.logger.info("WorkerState restored");
|
|
2293
|
+
}
|
|
2294
|
+
/** Override append to ensure JSONL write-per-entry. */
|
|
2295
|
+
append(path5, entry) {
|
|
2296
|
+
super.append(path5, entry);
|
|
2297
|
+
if (path5 === "conversation") {
|
|
2298
|
+
this.persistAppend(path5, entry);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
|
|
2303
|
+
// src/services/agents/constants.ts
|
|
2304
|
+
var WORKER_AGENT_MAX_STEP = 1e3;
|
|
2305
|
+
|
|
2306
|
+
// src/services/agents/worker-agent.ts
|
|
2307
|
+
var WorkerAgent = class extends BaseAgent {
|
|
2308
|
+
state;
|
|
2309
|
+
logger;
|
|
2310
|
+
_terminalResult = null;
|
|
2311
|
+
workspaceDir;
|
|
2312
|
+
contextManager;
|
|
2313
|
+
constructor(provider, sessionDir, workspaceDir2, logger) {
|
|
2314
|
+
super(provider);
|
|
2315
|
+
this.logger = logger;
|
|
2316
|
+
this.workspaceDir = workspaceDir2;
|
|
2317
|
+
this.state = new WorkerState(sessionDir, this.id, logger);
|
|
2318
|
+
this.contextManager = new ContextManager(
|
|
2319
|
+
this.state,
|
|
2320
|
+
buildWorkerSystemPrompt(),
|
|
2321
|
+
() => ""
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
async run(task, tools, onProgress) {
|
|
2325
|
+
this.state.set("runtimeState.task", { id: task.id, description: task.description });
|
|
2326
|
+
this.contextManager = new ContextManager(
|
|
2327
|
+
this.state,
|
|
2328
|
+
buildWorkerSystemPrompt({ taskId: task.id, workspaceDir: this.workspaceDir }),
|
|
2329
|
+
() => ""
|
|
2330
|
+
);
|
|
2331
|
+
this._terminalResult = null;
|
|
2332
|
+
this.state.append("conversation", {
|
|
2333
|
+
type: "user" /* User */,
|
|
2334
|
+
uuid: generateId("u"),
|
|
2335
|
+
parentUuid: null,
|
|
2336
|
+
sessionId: this.id,
|
|
2337
|
+
timestamp: nowISO(),
|
|
2338
|
+
message: { role: "user", content: task.description }
|
|
2339
|
+
});
|
|
2340
|
+
const result = await this.collectLoop({
|
|
2341
|
+
system: this.contextManager.getSystemPrompt(),
|
|
2342
|
+
messages: this.contextManager.getMessages(),
|
|
2343
|
+
tools,
|
|
2344
|
+
toolChoice: "required",
|
|
2345
|
+
maxSteps: WORKER_AGENT_MAX_STEP,
|
|
2346
|
+
stopTools: ["done", "fail"],
|
|
2347
|
+
onStep: (event) => {
|
|
2348
|
+
const entry = buildAssistantEntry(event, this.id);
|
|
2349
|
+
this.state.append("conversation", entry);
|
|
2350
|
+
if (event.text && onProgress) {
|
|
2351
|
+
onProgress(event.text.slice(0, 200));
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
});
|
|
2355
|
+
if (this._terminalResult) {
|
|
2356
|
+
return this._terminalResult;
|
|
2357
|
+
}
|
|
2358
|
+
if (this.abortController.signal.aborted) {
|
|
2359
|
+
return { status: "failed", error: "Aborted" };
|
|
2360
|
+
}
|
|
2361
|
+
return { status: "completed", summary: result.text || "(max steps reached)" };
|
|
2362
|
+
}
|
|
2363
|
+
/** Called by done tool callback — saves result. Loop stops via stopWhen. */
|
|
2364
|
+
complete(summary) {
|
|
2365
|
+
this._terminalResult = { status: "completed", summary };
|
|
2366
|
+
}
|
|
2367
|
+
/** Called by fail tool callback — saves result. Loop stops via stopWhen. */
|
|
2368
|
+
fail(error) {
|
|
2369
|
+
this._terminalResult = { status: "failed", error };
|
|
2370
|
+
}
|
|
2371
|
+
/** Get state for UI rendering. */
|
|
2372
|
+
getState() {
|
|
2373
|
+
return this.state;
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
|
|
2377
|
+
// src/services/agents/claude-code-worker.ts
|
|
2378
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2379
|
+
var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
2380
|
+
id;
|
|
2381
|
+
config;
|
|
2382
|
+
state;
|
|
2383
|
+
workspaceDir;
|
|
2384
|
+
logger;
|
|
2385
|
+
abortController = new AbortController();
|
|
2386
|
+
_terminalResult = null;
|
|
2387
|
+
/**
|
|
2388
|
+
* Progress debounce mechanism.
|
|
2389
|
+
*
|
|
2390
|
+
* Claude Code SDK streams assistant messages before the final result message.
|
|
2391
|
+
* The last assistant message often contains a summary that duplicates the result.
|
|
2392
|
+
* To avoid showing the user two near-identical signals (progress + completed):
|
|
2393
|
+
* 1. Each assistant text is buffered for PROGRESS_DEBOUNCE_MS
|
|
2394
|
+
* 2. If a result message arrives before the timer fires, the pending progress is discarded
|
|
2395
|
+
* 3. If the timer fires (no result yet), the progress is emitted normally
|
|
2396
|
+
*/
|
|
2397
|
+
static PROGRESS_DEBOUNCE_MS = 500;
|
|
2398
|
+
_progressTimer = null;
|
|
2399
|
+
_pendingProgress = null;
|
|
2400
|
+
constructor(config, sessionDir, workspaceDir2, logger) {
|
|
2401
|
+
this.id = generateId("cw");
|
|
2402
|
+
this.config = config;
|
|
2403
|
+
this.workspaceDir = workspaceDir2;
|
|
2404
|
+
this.logger = logger;
|
|
2405
|
+
this.state = new WorkerState(sessionDir, this.id, logger);
|
|
2406
|
+
}
|
|
2407
|
+
async run(task, _tools, onProgress) {
|
|
2408
|
+
this.state.set("runtimeState.task", { id: task.id, description: task.description });
|
|
2409
|
+
this.state.append("conversation", {
|
|
2410
|
+
type: "user" /* User */,
|
|
2411
|
+
uuid: generateId("u"),
|
|
2412
|
+
parentUuid: null,
|
|
2413
|
+
sessionId: this.id,
|
|
2414
|
+
timestamp: nowISO(),
|
|
2415
|
+
message: { role: "user", content: task.description }
|
|
2416
|
+
});
|
|
2417
|
+
try {
|
|
2418
|
+
const options = {
|
|
2419
|
+
cwd: this.workspaceDir,
|
|
2420
|
+
maxTurns: this.config.maxTurns ?? 10,
|
|
2421
|
+
permissionMode: "bypassPermissions",
|
|
2422
|
+
abortController: this.abortController
|
|
2423
|
+
};
|
|
2424
|
+
if (this.config.cliPath) {
|
|
2425
|
+
options.pathToClaudeCodeExecutable = this.config.cliPath;
|
|
2426
|
+
}
|
|
2427
|
+
const stream = query({
|
|
2428
|
+
prompt: `${buildThirdPartyPrompt()}${task.description}`,
|
|
2429
|
+
options
|
|
2430
|
+
});
|
|
2431
|
+
for await (const message of stream) {
|
|
2432
|
+
if (this.abortController.signal.aborted) break;
|
|
2433
|
+
this.processStreamMessage(message, onProgress);
|
|
2434
|
+
}
|
|
2435
|
+
} catch (err) {
|
|
2436
|
+
this.logger.error({ err, taskId: task.id }, "ClaudeCodeWorker query error");
|
|
2437
|
+
if (!this._terminalResult) {
|
|
2438
|
+
this._terminalResult = {
|
|
2439
|
+
status: "failed",
|
|
2440
|
+
error: err.message ?? String(err)
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
if (!this._terminalResult) {
|
|
2445
|
+
this._terminalResult = {
|
|
2446
|
+
status: "failed",
|
|
2447
|
+
error: this.abortController.signal.aborted ? "Aborted" : "No result received from Claude Code"
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
this.state.append("conversation", {
|
|
2451
|
+
type: "system" /* System */,
|
|
2452
|
+
uuid: generateId("s"),
|
|
2453
|
+
parentUuid: null,
|
|
2454
|
+
sessionId: this.id,
|
|
2455
|
+
timestamp: nowISO(),
|
|
2456
|
+
subtype: this._terminalResult.status === "completed" ? "compact" : "error",
|
|
2457
|
+
content: this._terminalResult.summary ?? this._terminalResult.error ?? ""
|
|
2458
|
+
});
|
|
2459
|
+
return this._terminalResult;
|
|
2460
|
+
}
|
|
2461
|
+
abort() {
|
|
2462
|
+
this.abortController.abort();
|
|
2463
|
+
}
|
|
2464
|
+
/** Get state for UI rendering. */
|
|
2465
|
+
getState() {
|
|
2466
|
+
return this.state;
|
|
2467
|
+
}
|
|
2468
|
+
// --- Stream message processing ---
|
|
2469
|
+
processStreamMessage(message, onProgress) {
|
|
2470
|
+
if (message.type === "assistant") {
|
|
2471
|
+
this.handleAssistantMessage(message, onProgress);
|
|
2472
|
+
} else if (message.type === "result") {
|
|
2473
|
+
this.handleResultMessage(message);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
handleAssistantMessage(msg, onProgress) {
|
|
2477
|
+
const textParts = [];
|
|
2478
|
+
for (const block of msg.message.content) {
|
|
2479
|
+
if (block.type === "text") {
|
|
2480
|
+
textParts.push(block.text);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
const text = textParts.join("\n");
|
|
2484
|
+
if (!text) return;
|
|
2485
|
+
this.state.append("conversation", {
|
|
2486
|
+
type: "assistant" /* Assistant */,
|
|
2487
|
+
uuid: generateId("a"),
|
|
2488
|
+
parentUuid: null,
|
|
2489
|
+
sessionId: this.id,
|
|
2490
|
+
timestamp: nowISO(),
|
|
2491
|
+
message: {
|
|
2492
|
+
role: "assistant",
|
|
2493
|
+
content: text,
|
|
2494
|
+
usage: {
|
|
2495
|
+
inputTokens: msg.message.usage?.input_tokens ?? 0,
|
|
2496
|
+
outputTokens: msg.message.usage?.output_tokens ?? 0
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
if (onProgress) {
|
|
2501
|
+
this._pendingProgress = text.slice(0, 200);
|
|
2502
|
+
if (this._progressTimer) clearTimeout(this._progressTimer);
|
|
2503
|
+
this._progressTimer = setTimeout(() => {
|
|
2504
|
+
if (this._pendingProgress) {
|
|
2505
|
+
onProgress(this._pendingProgress);
|
|
2506
|
+
this._pendingProgress = null;
|
|
2507
|
+
}
|
|
2508
|
+
this._progressTimer = null;
|
|
2509
|
+
}, _ClaudeCodeWorker.PROGRESS_DEBOUNCE_MS);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
handleResultMessage(msg) {
|
|
2513
|
+
if (this._progressTimer) {
|
|
2514
|
+
clearTimeout(this._progressTimer);
|
|
2515
|
+
this._progressTimer = null;
|
|
2516
|
+
this._pendingProgress = null;
|
|
2517
|
+
}
|
|
2518
|
+
if (msg.subtype === "success") {
|
|
2519
|
+
this._terminalResult = {
|
|
2520
|
+
status: "completed",
|
|
2521
|
+
summary: msg.result
|
|
2522
|
+
};
|
|
2523
|
+
} else {
|
|
2524
|
+
this._terminalResult = {
|
|
2525
|
+
status: "failed",
|
|
2526
|
+
error: msg.errors.join("\n") || `Claude Code error: ${msg.subtype}`
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
const prev = this.state.get("runtimeState");
|
|
2530
|
+
this.state.set("runtimeState", {
|
|
2531
|
+
...prev,
|
|
2532
|
+
usage: {
|
|
2533
|
+
inputTokens: msg.usage.input_tokens,
|
|
2534
|
+
outputTokens: msg.usage.output_tokens,
|
|
2535
|
+
costUsd: msg.total_cost_usd,
|
|
2536
|
+
turns: msg.num_turns,
|
|
2537
|
+
durationMs: msg.duration_ms
|
|
2538
|
+
}
|
|
2539
|
+
});
|
|
2540
|
+
this.logger.info(
|
|
2541
|
+
{
|
|
2542
|
+
subtype: msg.subtype,
|
|
2543
|
+
turns: msg.num_turns,
|
|
2544
|
+
costUsd: msg.total_cost_usd,
|
|
2545
|
+
durationMs: msg.duration_ms
|
|
2546
|
+
},
|
|
2547
|
+
"ClaudeCodeWorker result"
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2552
|
+
// src/services/worker-factory.ts
|
|
2553
|
+
var PUPPET_WORKER_TYPE = "puppet";
|
|
2554
|
+
var WorkerFactory = class {
|
|
2555
|
+
/**
|
|
2556
|
+
* Create a worker instance for the given type.
|
|
2557
|
+
*
|
|
2558
|
+
* @throws Error if the worker type is unknown or unsupported.
|
|
2559
|
+
*/
|
|
2560
|
+
create(workerType, deps) {
|
|
2561
|
+
const type = workerType ?? PUPPET_WORKER_TYPE;
|
|
2562
|
+
if (type === PUPPET_WORKER_TYPE) {
|
|
2563
|
+
return new WorkerAgent(deps.workerProvider, deps.sessionDir, deps.workspaceDir, deps.logger);
|
|
2564
|
+
}
|
|
2565
|
+
const config = deps.workerConfigs?.[type];
|
|
2566
|
+
if (!config) {
|
|
2567
|
+
throw new Error(`Unknown worker type: "${type}". Available: ${Object.keys(deps.workerConfigs ?? {}).join(", ") || "(none)"}`);
|
|
2568
|
+
}
|
|
2569
|
+
if (config.enable === false) {
|
|
2570
|
+
throw new Error(`Worker type "${type}" is disabled`);
|
|
2571
|
+
}
|
|
2572
|
+
switch (config.type) {
|
|
2573
|
+
case "claude-code":
|
|
2574
|
+
return new ClaudeCodeWorker(config, deps.sessionDir, deps.workspaceDir, deps.logger);
|
|
2575
|
+
default:
|
|
2576
|
+
throw new Error(`Unsupported worker type: "${config.type}"`);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
/** Check whether a worker type is known (puppet or configured). */
|
|
2580
|
+
isKnownType(workerType, workerConfigs) {
|
|
2581
|
+
if (!workerType || workerType === PUPPET_WORKER_TYPE) return true;
|
|
2582
|
+
return !!workerConfigs?.[workerType];
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
// src/services/supervisor.ts
|
|
2587
|
+
var Supervisor = class {
|
|
2588
|
+
ctx;
|
|
2589
|
+
eventBus;
|
|
2590
|
+
controllerState;
|
|
2591
|
+
taskPool;
|
|
2592
|
+
signalQueue;
|
|
2593
|
+
scheduler;
|
|
2594
|
+
controllerAgent;
|
|
2595
|
+
sessionManager;
|
|
2596
|
+
workerFactory;
|
|
2597
|
+
workers = /* @__PURE__ */ new Map();
|
|
2598
|
+
/** Reverse mapping: workerId → taskId, for reliable abort lookup. */
|
|
2599
|
+
workerTaskMap = /* @__PURE__ */ new Map();
|
|
2600
|
+
controllerProvider;
|
|
2601
|
+
workerProvider;
|
|
2602
|
+
config;
|
|
2603
|
+
constructor(options) {
|
|
2604
|
+
const { config, workspaceDir: workspaceDir2, logLevel } = options;
|
|
2605
|
+
if (options.resume) {
|
|
2606
|
+
this.sessionManager = SessionManager.resume(options.resume.sessionId, options.resume.sessionDir);
|
|
2607
|
+
} else {
|
|
2608
|
+
if (!options.homeDir) throw new Error("homeDir is required for new sessions");
|
|
2609
|
+
this.sessionManager = SessionManager.create(options.homeDir, workspaceDir2);
|
|
2610
|
+
}
|
|
2611
|
+
this.ctx = new SystemContext(
|
|
2612
|
+
config,
|
|
2613
|
+
this.sessionManager.sessionId,
|
|
2614
|
+
this.sessionManager.sessionDir,
|
|
2615
|
+
workspaceDir2,
|
|
2616
|
+
logLevel
|
|
2617
|
+
);
|
|
2618
|
+
this.controllerProvider = this.ctx.controllerProvider;
|
|
2619
|
+
this.workerProvider = this.ctx.workerProvider;
|
|
2620
|
+
this.config = config;
|
|
2621
|
+
this.workerFactory = new WorkerFactory();
|
|
2622
|
+
this.ctx.logger.info(
|
|
2623
|
+
{ controllerEndpoint: this.controllerProvider.endpoint, controllerModel: this.controllerProvider.modelName },
|
|
2624
|
+
"Provider config"
|
|
2625
|
+
);
|
|
2626
|
+
this.eventBus = new EventBus();
|
|
2627
|
+
this.controllerState = new ControllerAgentState(this.ctx.sessionDir, this.ctx.logger);
|
|
2628
|
+
const taskPoolState = new TaskPoolState(this.ctx.sessionDir, this.ctx.logger);
|
|
2629
|
+
const signalState = new SignalState(this.ctx.sessionDir, this.ctx.logger);
|
|
2630
|
+
this.taskPool = new TaskPool(taskPoolState, this.eventBus, this.ctx.logger);
|
|
2631
|
+
this.signalQueue = new SignalQueue(signalState, this.ctx.logger);
|
|
2632
|
+
this.scheduler = new TaskScheduler(
|
|
2633
|
+
this.ctx.logger,
|
|
2634
|
+
this.taskPool,
|
|
2635
|
+
this.signalQueue,
|
|
2636
|
+
this.eventBus,
|
|
2637
|
+
this,
|
|
2638
|
+
// SchedulerContext
|
|
2639
|
+
this.ctx.maxConcurrency,
|
|
2640
|
+
config.workers
|
|
2641
|
+
);
|
|
2642
|
+
const controllerTools = createControllerTools({
|
|
2643
|
+
addTask: (task) => this.taskPool.add(task),
|
|
2644
|
+
validateWorkerType: (type) => {
|
|
2645
|
+
if (type === "puppet") return null;
|
|
2646
|
+
const workerConfig = config.workers?.[type];
|
|
2647
|
+
if (!workerConfig) return `Unknown worker type: "${type}". Available: puppet${Object.keys(config.workers ?? {}).map((k) => `, ${k}`).join("")}`;
|
|
2648
|
+
if (workerConfig.enable === false) return `Worker "${type}" is disabled`;
|
|
2649
|
+
return null;
|
|
2650
|
+
},
|
|
2651
|
+
listTasks: (status) => this.taskPool.list(status),
|
|
2652
|
+
getTask: (taskId) => this.taskPool.get(taskId),
|
|
2653
|
+
cancelTask: (taskId) => this.taskPool.cancel(taskId),
|
|
2654
|
+
abortWorker: (taskId) => this.abortWorkerByTaskId(taskId),
|
|
2655
|
+
updateTask: (taskId, updates) => this.taskPool.updatePending(taskId, updates),
|
|
2656
|
+
queryMessages: (filter) => this.signalQueue.query(filter)
|
|
2657
|
+
});
|
|
2658
|
+
this.controllerAgent = new ControllerAgent(
|
|
2659
|
+
this.controllerState,
|
|
2660
|
+
this.controllerProvider,
|
|
2661
|
+
controllerTools,
|
|
2662
|
+
this.ctx.logger,
|
|
2663
|
+
config.workers
|
|
2664
|
+
);
|
|
2665
|
+
this.scheduler.setSpawnFn((taskWithWorker) => {
|
|
2666
|
+
return this.spawnWorker(taskWithWorker);
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
// --- Lifecycle ---
|
|
2670
|
+
async restore() {
|
|
2671
|
+
await Promise.all([
|
|
2672
|
+
this.sessionManager.restore(),
|
|
2673
|
+
this.controllerState.restore(),
|
|
2674
|
+
this.taskPool.restore(),
|
|
2675
|
+
this.signalQueue.restore()
|
|
2676
|
+
]);
|
|
2677
|
+
this.recoverCrashedWorkers();
|
|
2678
|
+
this.ctx.logger.info("Supervisor restored");
|
|
2679
|
+
}
|
|
2680
|
+
async start() {
|
|
2681
|
+
this.controllerState.set("runtimeState.session", {
|
|
2682
|
+
id: this.ctx.sessionId,
|
|
2683
|
+
startedAt: this.ctx.startedAt
|
|
2684
|
+
});
|
|
2685
|
+
this.scheduler.start();
|
|
2686
|
+
this.ctx.logger.info("Supervisor started");
|
|
2687
|
+
}
|
|
2688
|
+
// --- Worker management ---
|
|
2689
|
+
/**
|
|
2690
|
+
* Crash recovery: detect stale workers and orphaned tasks.
|
|
2691
|
+
* On resume, running tasks from a crashed session are marked failed,
|
|
2692
|
+
* and stale activeWorkers entries are cleared.
|
|
2693
|
+
*/
|
|
2694
|
+
recoverCrashedWorkers() {
|
|
2695
|
+
const activeWorkers = this.controllerState.get("runtimeState.activeWorkers");
|
|
2696
|
+
if (activeWorkers.length > 0) {
|
|
2697
|
+
this.ctx.logger.info({ count: activeWorkers.length }, "Clearing stale active workers from crash");
|
|
2698
|
+
this.controllerState.set("runtimeState.activeWorkers", []);
|
|
2699
|
+
}
|
|
2700
|
+
const running = this.taskPool.list("running");
|
|
2701
|
+
for (const task of running) {
|
|
2702
|
+
this.ctx.logger.info({ taskId: task.id }, "Marking crashed running task as failed");
|
|
2703
|
+
this.taskPool.fail(task.id, "Session crashed \u2014 task interrupted");
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
/** Abort a worker that is running a specific task. */
|
|
2707
|
+
abortWorkerByTaskId(taskId) {
|
|
2708
|
+
for (const [workerId, tId] of this.workerTaskMap) {
|
|
2709
|
+
if (tId === taskId) {
|
|
2710
|
+
const worker = this.workers.get(workerId);
|
|
2711
|
+
worker?.abort();
|
|
2712
|
+
this.ctx.logger.info({ workerId, taskId }, "Worker aborted");
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
this.ctx.logger.warn({ taskId }, "No running worker found for task abort");
|
|
2717
|
+
}
|
|
2718
|
+
async spawnWorker(taskWithWorker) {
|
|
2719
|
+
const { workerId, ...task } = taskWithWorker;
|
|
2720
|
+
const workerType = task.workerType ?? "puppet";
|
|
2721
|
+
const worker = this.workerFactory.create(workerType, {
|
|
2722
|
+
workerProvider: this.workerProvider,
|
|
2723
|
+
sessionDir: this.ctx.sessionDir,
|
|
2724
|
+
workspaceDir: this.ctx.workspaceDir,
|
|
2725
|
+
logger: this.ctx.logger,
|
|
2726
|
+
workerConfigs: this.config.workers
|
|
2727
|
+
});
|
|
2728
|
+
this.workers.set(workerId, worker);
|
|
2729
|
+
this.workerTaskMap.set(workerId, task.id);
|
|
2730
|
+
const workerMeta = getWorkerMeta(workerType, this.config.workers);
|
|
2731
|
+
const tools = workerType === "puppet" ? createWorkerTools({
|
|
2732
|
+
listTasks: (status) => this.taskPool.list(status),
|
|
2733
|
+
cwd: this.ctx.workspaceDir,
|
|
2734
|
+
reportProgress: (msg, _percent) => {
|
|
2735
|
+
this.signalQueue.enqueue({
|
|
2736
|
+
source: "worker",
|
|
2737
|
+
type: "progress",
|
|
2738
|
+
payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: workerMeta.emoji, message: msg }
|
|
2739
|
+
});
|
|
2740
|
+
},
|
|
2741
|
+
onComplete: (summary) => {
|
|
2742
|
+
worker.complete(summary);
|
|
2743
|
+
},
|
|
2744
|
+
onFail: (error) => {
|
|
2745
|
+
worker.fail(error);
|
|
2746
|
+
}
|
|
2747
|
+
}) : {};
|
|
2748
|
+
this.ctx.logger.info({ workerId, taskId: task.id, workerType }, "Worker starting");
|
|
2749
|
+
return worker.run(
|
|
2750
|
+
{ ...task, workerId },
|
|
2751
|
+
tools,
|
|
2752
|
+
(msg) => {
|
|
2753
|
+
this.signalQueue.enqueue({
|
|
2754
|
+
source: "worker",
|
|
2755
|
+
type: "progress",
|
|
2756
|
+
payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: workerMeta.emoji, message: msg }
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
registerWorker(worker) {
|
|
2762
|
+
this.workers.set(worker.id, worker);
|
|
2763
|
+
}
|
|
2764
|
+
getWorker(id) {
|
|
2765
|
+
return this.workers.get(id);
|
|
2766
|
+
}
|
|
2767
|
+
getWorkerState(id) {
|
|
2768
|
+
return this.workers.get(id)?.getState() ?? null;
|
|
2769
|
+
}
|
|
2770
|
+
removeWorker(id) {
|
|
2771
|
+
setTimeout(() => {
|
|
2772
|
+
this.workers.delete(id);
|
|
2773
|
+
this.workerTaskMap.delete(id);
|
|
2774
|
+
}, 0);
|
|
2775
|
+
}
|
|
2776
|
+
// --- SchedulerContext implementation ---
|
|
2777
|
+
addActiveWorker(info) {
|
|
2778
|
+
this.controllerState.append("runtimeState.activeWorkers", info);
|
|
2779
|
+
}
|
|
2780
|
+
removeActiveWorker(workerId) {
|
|
2781
|
+
const workers = this.controllerState.get("runtimeState.activeWorkers");
|
|
2782
|
+
const idx = workers.findIndex((w) => w.workerId === workerId);
|
|
2783
|
+
if (idx !== -1) {
|
|
2784
|
+
this.controllerState.remove("runtimeState.activeWorkers", idx);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
// --- Shutdown ---
|
|
2788
|
+
async shutdown() {
|
|
2789
|
+
for (const worker of this.workers.values()) {
|
|
2790
|
+
worker.abort();
|
|
2791
|
+
}
|
|
2792
|
+
const running = this.taskPool.list("running");
|
|
2793
|
+
for (const task of running) {
|
|
2794
|
+
this.taskPool.cancel(task.id);
|
|
2795
|
+
}
|
|
2796
|
+
this.ctx.abortController.abort();
|
|
2797
|
+
this.workers.clear();
|
|
2798
|
+
this.workerTaskMap.clear();
|
|
2799
|
+
this.ctx.logger.info("Supervisor shutdown complete");
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
|
|
2803
|
+
// src/services/controller-loop.ts
|
|
2804
|
+
async function mainControllerLoop(supervisor) {
|
|
2805
|
+
const { signalQueue, controllerAgent, ctx } = supervisor;
|
|
2806
|
+
ctx.logger.info("Main controller loop started");
|
|
2807
|
+
while (!ctx.abortController.signal.aborted) {
|
|
2808
|
+
try {
|
|
2809
|
+
if (controllerAgent.needsCompaction()) {
|
|
2810
|
+
ctx.logger.info("Context full, triggering compaction");
|
|
2811
|
+
await controllerAgent.performCompaction();
|
|
2812
|
+
}
|
|
2813
|
+
const signals = signalQueue.dequeueUnread();
|
|
2814
|
+
ctx.logger.debug("loop start with signals:" + JSON.stringify(signals));
|
|
2815
|
+
if (signals.length > 0) {
|
|
2816
|
+
ctx.logger.info({ count: signals.length, sigIds: signals.map((s) => s.id), sigTypes: signals.map((s) => `${s.source}/${s.type}`) }, "Processing signals");
|
|
2817
|
+
await controllerAgent.run(signals);
|
|
2818
|
+
ctx.logger.debug("loop end");
|
|
2819
|
+
}
|
|
2820
|
+
await signalQueue.waitForSignal();
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
if (ctx.abortController.signal.aborted) break;
|
|
2823
|
+
const friendly = formatLlmError(err);
|
|
2824
|
+
ctx.logger.error({ error: friendly, statusCode: err.statusCode ?? err.lastError?.statusCode }, "Controller loop error");
|
|
2825
|
+
controllerAgent.state.set("dynamicState.streamingEntries", []);
|
|
2826
|
+
controllerAgent.state.append("conversation", {
|
|
2827
|
+
type: "system" /* System */,
|
|
2828
|
+
uuid: generateId("err"),
|
|
2829
|
+
parentUuid: null,
|
|
2830
|
+
sessionId: "controller",
|
|
2831
|
+
timestamp: nowISO(),
|
|
2832
|
+
subtype: "error" /* Error */,
|
|
2833
|
+
content: friendly
|
|
2834
|
+
});
|
|
2835
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
ctx.logger.info("Main controller loop stopped");
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
// src/infra/session-helpers.ts
|
|
2842
|
+
import * as fs from "fs";
|
|
2843
|
+
import * as path2 from "path";
|
|
2844
|
+
function listSessions(homeDir2) {
|
|
2845
|
+
const sessionsDir = path2.join(homeDir2, "sessions");
|
|
2846
|
+
if (!fs.existsSync(sessionsDir)) return [];
|
|
2847
|
+
return fs.readdirSync(sessionsDir).filter((name) => fs.existsSync(path2.join(sessionsDir, name, "meta.json"))).map((name) => {
|
|
2848
|
+
try {
|
|
2849
|
+
return JSON.parse(fs.readFileSync(path2.join(sessionsDir, name, "meta.json"), "utf-8"));
|
|
2850
|
+
} catch {
|
|
2851
|
+
return null;
|
|
2852
|
+
}
|
|
2853
|
+
}).filter((m) => m !== null).sort((a, b) => b.createdAt - a.createdAt);
|
|
2854
|
+
}
|
|
2855
|
+
function getSession(homeDir2, sessionId) {
|
|
2856
|
+
const sessionDir = path2.join(homeDir2, "sessions", sessionId);
|
|
2857
|
+
const metaPath = path2.join(sessionDir, "meta.json");
|
|
2858
|
+
if (!fs.existsSync(metaPath)) return null;
|
|
2859
|
+
try {
|
|
2860
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
2861
|
+
return { sessionId, sessionDir, meta };
|
|
2862
|
+
} catch {
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// src/tui/render.ts
|
|
2868
|
+
import React7 from "react";
|
|
2869
|
+
import { render } from "ink";
|
|
2870
|
+
|
|
2871
|
+
// src/tui/app.tsx
|
|
2872
|
+
import { useRef as useRef2 } from "react";
|
|
2873
|
+
|
|
2874
|
+
// src/tui/hooks/use-memoized-fn.ts
|
|
2875
|
+
import { useRef, useCallback } from "react";
|
|
2876
|
+
function useMemoizedFn(fn) {
|
|
2877
|
+
const fnRef = useRef(fn);
|
|
2878
|
+
fnRef.current = fn;
|
|
2879
|
+
const memoized = useCallback((...args) => fnRef.current(...args), []);
|
|
2880
|
+
return memoized;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// src/tui/app.tsx
|
|
2884
|
+
import { Box as Box12, useApp, useInput as useInput3 } from "ink";
|
|
2885
|
+
|
|
2886
|
+
// src/tui/providers/supervisor-provider.tsx
|
|
2887
|
+
import { createContext, useContext } from "react";
|
|
2888
|
+
import { jsx } from "react/jsx-runtime";
|
|
2889
|
+
var SupervisorContext = createContext(null);
|
|
2890
|
+
function SupervisorProvider({ supervisor, children }) {
|
|
2891
|
+
return /* @__PURE__ */ jsx(SupervisorContext.Provider, { value: supervisor, children });
|
|
2892
|
+
}
|
|
2893
|
+
function useSupervisor() {
|
|
2894
|
+
const ctx = useContext(SupervisorContext);
|
|
2895
|
+
if (!ctx) throw new Error("useSupervisor must be used within SupervisorProvider");
|
|
2896
|
+
return ctx;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// src/tui/providers/route-provider.tsx
|
|
2900
|
+
import { createContext as createContext2, useContext as useContext2, useState } from "react";
|
|
2901
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
2902
|
+
var RouteContext = createContext2({
|
|
2903
|
+
route: { page: "controller" },
|
|
2904
|
+
navigate: () => {
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
2907
|
+
function RouteProvider({ children }) {
|
|
2908
|
+
const [route, setRoute] = useState({ page: "controller" });
|
|
2909
|
+
const navigate = useMemoizedFn((r) => setRoute(r));
|
|
2910
|
+
return /* @__PURE__ */ jsx2(RouteContext.Provider, { value: { route, navigate }, children });
|
|
2911
|
+
}
|
|
2912
|
+
function useRouter() {
|
|
2913
|
+
return useContext2(RouteContext);
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
// src/tui/pages/controller-page.tsx
|
|
2917
|
+
import { useState as useState7, useMemo as useMemo4 } from "react";
|
|
2918
|
+
import { Box as Box10, useStdout } from "ink";
|
|
2919
|
+
|
|
2920
|
+
// src/tui/components/chat-area/chat-area.tsx
|
|
2921
|
+
import React3, { useMemo } from "react";
|
|
2922
|
+
import { Box as Box5, Static, Text as Text6 } from "ink";
|
|
2923
|
+
|
|
2924
|
+
// src/tui/theme.ts
|
|
2925
|
+
var baseToken = {
|
|
2926
|
+
/** Semantic color palette — named by usage, not by hue. */
|
|
2927
|
+
color: {
|
|
2928
|
+
/** Strong emphasis — important labels, highlights. */
|
|
2929
|
+
textEmphasis: "yellow",
|
|
2930
|
+
/** Default body text — AI responses, general content. */
|
|
2931
|
+
textNormal: "white",
|
|
2932
|
+
/** Subtle text — meta entries, dimmed labels, timestamps. */
|
|
2933
|
+
textSubtle: "gray",
|
|
2934
|
+
/** Accent — interactive elements, assistant prefix. */
|
|
2935
|
+
accent: "green",
|
|
2936
|
+
/** Error / danger states. */
|
|
2937
|
+
error: "red",
|
|
2938
|
+
/** Borders and structural lines. */
|
|
2939
|
+
border: "gray"
|
|
2940
|
+
},
|
|
2941
|
+
/** Background fills (hex). */
|
|
2942
|
+
bg: {
|
|
2943
|
+
/** Highlighted row surface. */
|
|
2944
|
+
surface: "#444444"
|
|
2945
|
+
},
|
|
2946
|
+
/** Typographic / icon characters. */
|
|
2947
|
+
icon: {
|
|
2948
|
+
arrowRight: "\u2192",
|
|
2949
|
+
arrowLeft: "\u2190",
|
|
2950
|
+
/** Status / notification flag. */
|
|
2951
|
+
flag: "\u2691",
|
|
2952
|
+
/** Decorative dot. */
|
|
2953
|
+
dot: "\xB7"
|
|
2954
|
+
},
|
|
2955
|
+
/** Numeric spacing scale (terminal rows). */
|
|
2956
|
+
space: {
|
|
2957
|
+
xs: 0,
|
|
2958
|
+
sm: 1,
|
|
2959
|
+
md: 2
|
|
2960
|
+
},
|
|
2961
|
+
/** Animation timing (ms). */
|
|
2962
|
+
timing: {
|
|
2963
|
+
slow: 1e3,
|
|
2964
|
+
normal: 500,
|
|
2965
|
+
fast: 100
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
var theme = {
|
|
2969
|
+
baseToken,
|
|
2970
|
+
user: {
|
|
2971
|
+
prefix: "\u2192",
|
|
2972
|
+
bgHex: baseToken.bg.surface,
|
|
2973
|
+
prefixColor: baseToken.color.textEmphasis
|
|
2974
|
+
},
|
|
2975
|
+
assistant: {
|
|
2976
|
+
prefix: "\u2192",
|
|
2977
|
+
prefixColor: baseToken.color.accent
|
|
2978
|
+
},
|
|
2979
|
+
toolUse: {
|
|
2980
|
+
callIcon: baseToken.icon.arrowRight,
|
|
2981
|
+
resultIcon: baseToken.icon.arrowLeft,
|
|
2982
|
+
color: baseToken.color.textSubtle
|
|
2983
|
+
},
|
|
2984
|
+
signal: {
|
|
2985
|
+
prefix: baseToken.icon.flag,
|
|
2986
|
+
color: baseToken.color.textSubtle
|
|
2987
|
+
},
|
|
2988
|
+
meta: {
|
|
2989
|
+
prefix: baseToken.icon.dot,
|
|
2990
|
+
color: baseToken.color.textSubtle
|
|
2991
|
+
},
|
|
2992
|
+
spinner: {
|
|
2993
|
+
frames: ["\u25CF", "\u25C6", "\u2726", "\u203B"],
|
|
2994
|
+
interval: baseToken.timing.normal
|
|
2995
|
+
},
|
|
2996
|
+
spacing: {
|
|
2997
|
+
blockGap: baseToken.space.sm
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
|
|
3001
|
+
// src/tui/components/chat-area/user-row.tsx
|
|
3002
|
+
import { Box, Text } from "ink";
|
|
3003
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
3004
|
+
function UserRow({ item, width }) {
|
|
3005
|
+
const dim = !item.read;
|
|
3006
|
+
const text = item.content;
|
|
3007
|
+
const padded = text.length < width ? text + " ".repeat(4) : text;
|
|
3008
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
3009
|
+
/* @__PURE__ */ jsxs(
|
|
3010
|
+
Text,
|
|
3011
|
+
{
|
|
3012
|
+
backgroundColor: theme.user.bgHex,
|
|
3013
|
+
color: theme.user.prefixColor,
|
|
3014
|
+
bold: true,
|
|
3015
|
+
children: [
|
|
3016
|
+
theme.user.prefix,
|
|
3017
|
+
" "
|
|
3018
|
+
]
|
|
3019
|
+
}
|
|
3020
|
+
),
|
|
3021
|
+
/* @__PURE__ */ jsx3(
|
|
3022
|
+
Text,
|
|
3023
|
+
{
|
|
3024
|
+
backgroundColor: theme.user.bgHex,
|
|
3025
|
+
color: dim ? theme.baseToken.color.textSubtle : theme.baseToken.color.textNormal,
|
|
3026
|
+
children: padded
|
|
3027
|
+
}
|
|
3028
|
+
)
|
|
3029
|
+
] });
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// src/tui/components/chat-area/assistant-step-row.tsx
|
|
3033
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
3034
|
+
|
|
3035
|
+
// src/tui/components/chat-area/constants.ts
|
|
3036
|
+
var TOOL_DETAIL_MAX_LENGTH = 80;
|
|
3037
|
+
var LONG_MSG_MAX_LENGTH = 120;
|
|
3038
|
+
|
|
3039
|
+
// src/tui/components/chat-area/assistant-step-row.tsx
|
|
3040
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
3041
|
+
function AssistantStepRow({ item }) {
|
|
3042
|
+
const sections = item.sections ?? [];
|
|
3043
|
+
if (sections.length === 0) {
|
|
3044
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3045
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.assistant.prefixColor, bold: true, children: [
|
|
3046
|
+
theme.assistant.prefix,
|
|
3047
|
+
" "
|
|
3048
|
+
] }),
|
|
3049
|
+
/* @__PURE__ */ jsx4(Text2, { children: item.content })
|
|
3050
|
+
] });
|
|
3051
|
+
}
|
|
3052
|
+
return /* @__PURE__ */ jsx4(Box2, { flexDirection: "column", children: sections.map((sec) => {
|
|
3053
|
+
const key = `${sec.kind}-${sec.id}`;
|
|
3054
|
+
switch (sec.kind) {
|
|
3055
|
+
case "reasoning":
|
|
3056
|
+
return /* @__PURE__ */ jsx4(ReasoningRow, { section: sec }, key);
|
|
3057
|
+
case "content":
|
|
3058
|
+
return /* @__PURE__ */ jsx4(ContentRow, { section: sec }, key);
|
|
3059
|
+
case "tool_call":
|
|
3060
|
+
return /* @__PURE__ */ jsx4(ToolCallRow, { section: sec }, key);
|
|
3061
|
+
case "tool_result":
|
|
3062
|
+
return /* @__PURE__ */ jsx4(ToolResultRow, { section: sec }, key);
|
|
3063
|
+
}
|
|
3064
|
+
}) });
|
|
3065
|
+
}
|
|
3066
|
+
function ReasoningRow({ section: section2 }) {
|
|
3067
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3068
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.meta.color, children: [
|
|
3069
|
+
theme.meta.prefix,
|
|
3070
|
+
" "
|
|
3071
|
+
] }),
|
|
3072
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
3073
|
+
"thinking: ",
|
|
3074
|
+
section2.text.slice(0, LONG_MSG_MAX_LENGTH)
|
|
3075
|
+
] })
|
|
3076
|
+
] });
|
|
3077
|
+
}
|
|
3078
|
+
function ContentRow({ section: section2 }) {
|
|
3079
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3080
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.assistant.prefixColor, bold: true, children: [
|
|
3081
|
+
theme.assistant.prefix,
|
|
3082
|
+
" "
|
|
3083
|
+
] }),
|
|
3084
|
+
/* @__PURE__ */ jsx4(Text2, { children: section2.text })
|
|
3085
|
+
] });
|
|
3086
|
+
}
|
|
3087
|
+
function ToolCallRow({ section: section2 }) {
|
|
3088
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3089
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.meta.color, children: [
|
|
3090
|
+
theme.meta.prefix,
|
|
3091
|
+
" "
|
|
3092
|
+
] }),
|
|
3093
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.toolUse.color, children: [
|
|
3094
|
+
theme.toolUse.callIcon,
|
|
3095
|
+
" ",
|
|
3096
|
+
section2.toolName,
|
|
3097
|
+
": ",
|
|
3098
|
+
section2.text.slice(0, TOOL_DETAIL_MAX_LENGTH)
|
|
3099
|
+
] })
|
|
3100
|
+
] });
|
|
3101
|
+
}
|
|
3102
|
+
function ToolResultRow({ section: section2 }) {
|
|
3103
|
+
const color = section2.isError ? theme.baseToken.color.error : theme.toolUse.color;
|
|
3104
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3105
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.meta.color, children: [
|
|
3106
|
+
theme.meta.prefix,
|
|
3107
|
+
" "
|
|
3108
|
+
] }),
|
|
3109
|
+
/* @__PURE__ */ jsxs2(Text2, { color, children: [
|
|
3110
|
+
theme.toolUse.resultIcon,
|
|
3111
|
+
" ",
|
|
3112
|
+
section2.toolName,
|
|
3113
|
+
": ",
|
|
3114
|
+
section2.text.slice(0, TOOL_DETAIL_MAX_LENGTH)
|
|
3115
|
+
] })
|
|
3116
|
+
] });
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/tui/components/chat-area/signal-row.tsx
|
|
3120
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
3121
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
3122
|
+
var MAX_CONTENT_LINES = 2;
|
|
3123
|
+
function trimContent(text, maxLines) {
|
|
3124
|
+
const lines = text.split("\n").map((l) => l.trimEnd()).filter((l) => l.length > 0);
|
|
3125
|
+
const trimmed = lines.slice(0, maxLines).join("\n");
|
|
3126
|
+
return lines.length > maxLines ? trimmed + "..." : trimmed;
|
|
3127
|
+
}
|
|
3128
|
+
function SignalRow({ item }) {
|
|
3129
|
+
const dim = !item.read;
|
|
3130
|
+
const label = item.subtype?.replace("worker_", "") ?? "signal";
|
|
3131
|
+
const color = dim ? theme.baseToken.color.textSubtle : theme.signal.color;
|
|
3132
|
+
const contextParts = [];
|
|
3133
|
+
if (item.taskTitle) contextParts.push(item.taskTitle);
|
|
3134
|
+
if (item.workerId) contextParts.push(`w:${item.workerId.slice(-4)}`);
|
|
3135
|
+
const context = contextParts.length > 0 ? `[${contextParts.join(" \xB7 ")}] ` : "";
|
|
3136
|
+
const content = trimContent(item.content, MAX_CONTENT_LINES);
|
|
3137
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
3138
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
3139
|
+
/* @__PURE__ */ jsxs3(Text3, { color: theme.meta.color, children: [
|
|
3140
|
+
theme.signal.prefix,
|
|
3141
|
+
" "
|
|
3142
|
+
] }),
|
|
3143
|
+
item.emoji && /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
3144
|
+
item.emoji,
|
|
3145
|
+
" "
|
|
3146
|
+
] }),
|
|
3147
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: dim, color, children: [
|
|
3148
|
+
context,
|
|
3149
|
+
label
|
|
3150
|
+
] })
|
|
3151
|
+
] }),
|
|
3152
|
+
content && /* @__PURE__ */ jsx5(Box3, { marginLeft: 3, children: /* @__PURE__ */ jsx5(Text3, { dimColor: dim, color, children: content }) })
|
|
3153
|
+
] });
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// src/tui/components/chat-area/system-row.tsx
|
|
3157
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
3158
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3159
|
+
function SystemRow({ item }) {
|
|
3160
|
+
const isError = item.subtype === "error";
|
|
3161
|
+
const isCompact = item.subtype === "compact";
|
|
3162
|
+
if (isCompact) {
|
|
3163
|
+
return /* @__PURE__ */ jsx6(Box4, { children: /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: "\u224B Conversation has been compacted" }) });
|
|
3164
|
+
}
|
|
3165
|
+
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
3166
|
+
/* @__PURE__ */ jsxs4(Text4, { color: isError ? theme.baseToken.color.error : theme.meta.color, children: [
|
|
3167
|
+
isError ? "\u2718" : theme.meta.prefix,
|
|
3168
|
+
" "
|
|
3169
|
+
] }),
|
|
3170
|
+
/* @__PURE__ */ jsx6(Text4, { color: isError ? theme.baseToken.color.error : void 0, dimColor: !isError, children: item.content })
|
|
3171
|
+
] });
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// src/tui/components/chat-area/item-row.tsx
|
|
3175
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
3176
|
+
function ItemRow({ item, contentWidth }) {
|
|
3177
|
+
switch (item.role) {
|
|
3178
|
+
case "user":
|
|
3179
|
+
return /* @__PURE__ */ jsx7(UserRow, { item, width: contentWidth });
|
|
3180
|
+
case "assistant":
|
|
3181
|
+
return /* @__PURE__ */ jsx7(AssistantStepRow, { item });
|
|
3182
|
+
case "signal":
|
|
3183
|
+
return /* @__PURE__ */ jsx7(SignalRow, { item });
|
|
3184
|
+
case "system":
|
|
3185
|
+
return /* @__PURE__ */ jsx7(SystemRow, { item });
|
|
3186
|
+
default:
|
|
3187
|
+
return null;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// src/tui/components/chat-area/streaming-spinner.tsx
|
|
3192
|
+
import { useState as useState2, useEffect } from "react";
|
|
3193
|
+
import { Text as Text5 } from "ink";
|
|
3194
|
+
import { jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3195
|
+
function StreamingSpinner() {
|
|
3196
|
+
const [frame, setFrame] = useState2(0);
|
|
3197
|
+
useEffect(() => {
|
|
3198
|
+
const timer = setInterval(() => {
|
|
3199
|
+
setFrame((f) => (f + 1) % theme.spinner.frames.length);
|
|
3200
|
+
}, theme.spinner.interval);
|
|
3201
|
+
return () => clearInterval(timer);
|
|
3202
|
+
}, []);
|
|
3203
|
+
return /* @__PURE__ */ jsxs5(Text5, { color: theme.assistant.prefixColor, bold: true, children: [
|
|
3204
|
+
theme.spinner.frames[frame],
|
|
3205
|
+
" "
|
|
3206
|
+
] });
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// src/tui/components/chat-area/chat-area.tsx
|
|
3210
|
+
import { Fragment, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3211
|
+
function shouldShowGap(prev, _current) {
|
|
3212
|
+
if (!prev) return false;
|
|
3213
|
+
return true;
|
|
3214
|
+
}
|
|
3215
|
+
var ChatArea = React3.memo(function ChatArea2({ items, busy, streamingItems, contentWidth }) {
|
|
3216
|
+
const width = contentWidth ?? 78;
|
|
3217
|
+
const { staticItems, dynamicItems } = useMemo(() => {
|
|
3218
|
+
const read = items.filter((i) => i.read);
|
|
3219
|
+
const unread = items.filter((i) => !i.read);
|
|
3220
|
+
return { staticItems: read, dynamicItems: unread };
|
|
3221
|
+
}, [items]);
|
|
3222
|
+
return /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
3223
|
+
/* @__PURE__ */ jsx8(Static, { items: staticItems, children: (item, idx) => /* @__PURE__ */ jsx8(
|
|
3224
|
+
Box5,
|
|
3225
|
+
{
|
|
3226
|
+
flexDirection: "column",
|
|
3227
|
+
paddingX: 1,
|
|
3228
|
+
marginTop: shouldShowGap(idx > 0 ? staticItems[idx - 1] : null, item) ? theme.spacing.blockGap : 0,
|
|
3229
|
+
children: /* @__PURE__ */ jsx8(ItemRow, { item, contentWidth: width })
|
|
3230
|
+
},
|
|
3231
|
+
item.id
|
|
3232
|
+
) }, "chat"),
|
|
3233
|
+
(streamingItems?.length ?? 0) > 0 && /* @__PURE__ */ jsxs6(Box5, { paddingX: 1, marginTop: items.length > 0 ? theme.spacing.blockGap : 0, children: [
|
|
3234
|
+
/* @__PURE__ */ jsx8(StreamingSpinner, {}),
|
|
3235
|
+
/* @__PURE__ */ jsx8(Text6, { children: streamingItems[0].content })
|
|
3236
|
+
] }),
|
|
3237
|
+
dynamicItems.length > 0 && /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", paddingX: 3, marginTop: 1, marginBottom: 1, children: [
|
|
3238
|
+
/* @__PURE__ */ jsx8(Box5, { justifyContent: "flex-start", children: /* @__PURE__ */ jsx8(Text6, { color: theme.baseToken.color.textSubtle, dimColor: true, children: "\xB7\xB7\xB7 waiting for process \xB7\xB7\xB7" }) }),
|
|
3239
|
+
dynamicItems.map((item) => /* @__PURE__ */ jsx8(Box5, { flexDirection: "column", marginTop: shouldShowGap(null, item) ? theme.spacing.blockGap : 0, children: /* @__PURE__ */ jsx8(ItemRow, { item, contentWidth: width }) }, item.id))
|
|
3240
|
+
] })
|
|
3241
|
+
] });
|
|
3242
|
+
});
|
|
3243
|
+
|
|
3244
|
+
// src/tui/components/input-area.tsx
|
|
3245
|
+
import { useState as useState3, useMemo as useMemo2 } from "react";
|
|
3246
|
+
|
|
3247
|
+
// src/tui/hooks/use-slash-commands.ts
|
|
3248
|
+
var SLASH_COMMANDS = [
|
|
3249
|
+
{ name: "tasks", args: "[status]", description: "List tasks" },
|
|
3250
|
+
{ name: "workers", description: "List active workers" },
|
|
3251
|
+
{ name: "worker", args: "[id]", description: "View worker detail" },
|
|
3252
|
+
{ name: "cancel", args: "<id>", description: "Cancel a task" },
|
|
3253
|
+
{ name: "compact", description: "Compact context" },
|
|
3254
|
+
{ name: "exit", description: "Quit" },
|
|
3255
|
+
{ name: "help", description: "Show help" }
|
|
3256
|
+
];
|
|
3257
|
+
function useSlashCommands(supervisor, ctx) {
|
|
3258
|
+
const handleCommand = useMemoizedFn((input) => {
|
|
3259
|
+
if (!input.startsWith("/")) return { handled: false };
|
|
3260
|
+
const parts = input.slice(1).split(/\s+/);
|
|
3261
|
+
const cmd = parts[0]?.toLowerCase();
|
|
3262
|
+
switch (cmd) {
|
|
3263
|
+
case "exit":
|
|
3264
|
+
case "quit":
|
|
3265
|
+
return { handled: true, output: "__EXIT__" };
|
|
3266
|
+
case "tasks": {
|
|
3267
|
+
const status = parts[1];
|
|
3268
|
+
const tasks = supervisor.taskPool.list(status);
|
|
3269
|
+
if (tasks.length === 0) return { handled: true, output: "No tasks" };
|
|
3270
|
+
const lines = tasks.map(
|
|
3271
|
+
(t) => `[${t.status}] ${t.id}: ${t.description}${t.result ? ` \u2192 ${t.result}` : ""}`
|
|
3272
|
+
);
|
|
3273
|
+
return { handled: true, output: lines.join("\n") };
|
|
3274
|
+
}
|
|
3275
|
+
case "workers": {
|
|
3276
|
+
return { handled: true, action: "workers" };
|
|
3277
|
+
}
|
|
3278
|
+
case "worker": {
|
|
3279
|
+
const workerId = parts[1];
|
|
3280
|
+
if (!workerId) {
|
|
3281
|
+
const activeWorkers = supervisor.controllerState.get("runtimeState.activeWorkers");
|
|
3282
|
+
if (!activeWorkers || activeWorkers.length === 0) {
|
|
3283
|
+
return { handled: true, output: "No active workers. Usage: /worker <id>" };
|
|
3284
|
+
}
|
|
3285
|
+
const lines = activeWorkers.map(
|
|
3286
|
+
(w) => `${w.workerId} \u2192 task ${w.taskId}`
|
|
3287
|
+
);
|
|
3288
|
+
return { handled: true, output: lines.join("\n") };
|
|
3289
|
+
}
|
|
3290
|
+
return { handled: true, navigate: { page: "worker", workerId } };
|
|
3291
|
+
}
|
|
3292
|
+
case "cancel": {
|
|
3293
|
+
const taskId = parts[1];
|
|
3294
|
+
if (!taskId) {
|
|
3295
|
+
return { handled: true, output: "Usage: /cancel <taskId>" };
|
|
3296
|
+
}
|
|
3297
|
+
supervisor.taskPool.cancel(taskId);
|
|
3298
|
+
supervisor.abortWorkerByTaskId(taskId);
|
|
3299
|
+
return { handled: true, output: `Task ${taskId} cancelled` };
|
|
3300
|
+
}
|
|
3301
|
+
case "compact":
|
|
3302
|
+
return { handled: true, action: "compact", output: "Compacting context..." };
|
|
3303
|
+
case "help":
|
|
3304
|
+
return {
|
|
3305
|
+
handled: true,
|
|
3306
|
+
output: [
|
|
3307
|
+
"Commands:",
|
|
3308
|
+
" /tasks [status] \u2014 List tasks",
|
|
3309
|
+
" /workers \u2014 List active workers",
|
|
3310
|
+
" /worker [id] \u2014 View worker detail or list all",
|
|
3311
|
+
" /cancel <id> \u2014 Cancel a task and abort its worker",
|
|
3312
|
+
" /compact \u2014 Compact conversation context",
|
|
3313
|
+
" /exit \u2014 Quit",
|
|
3314
|
+
" /help \u2014 Show this help"
|
|
3315
|
+
].join("\n")
|
|
3316
|
+
};
|
|
3317
|
+
default:
|
|
3318
|
+
return { handled: true, output: `Unknown command: /${cmd}. Type /help for commands.` };
|
|
3319
|
+
}
|
|
3320
|
+
});
|
|
3321
|
+
return { handleCommand };
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// src/tui/components/input-area.tsx
|
|
3325
|
+
import { Box as Box6, Text as Text7, useInput } from "ink";
|
|
3326
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3327
|
+
function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
3328
|
+
const [input, setInput] = useState3("");
|
|
3329
|
+
const [cursor, setCursor] = useState3(0);
|
|
3330
|
+
const [selectedIndex, setSelectedIndex] = useState3(0);
|
|
3331
|
+
const suggestions = useMemo2(() => {
|
|
3332
|
+
if (!input.startsWith("/") || input.includes(" ")) return [];
|
|
3333
|
+
const typed = input.slice(1).toLowerCase();
|
|
3334
|
+
return SLASH_COMMANDS.filter((c) => c.name.startsWith(typed));
|
|
3335
|
+
}, [input]);
|
|
3336
|
+
const showSuggestions = suggestions.length > 0;
|
|
3337
|
+
const clampedIndex = showSuggestions ? Math.min(selectedIndex, suggestions.length - 1) : 0;
|
|
3338
|
+
const reset = useMemoizedFn(() => {
|
|
3339
|
+
setInput("");
|
|
3340
|
+
setCursor(0);
|
|
3341
|
+
setSelectedIndex(0);
|
|
3342
|
+
});
|
|
3343
|
+
const applySuggestion = useMemoizedFn((index) => {
|
|
3344
|
+
const cmd = suggestions[index];
|
|
3345
|
+
if (!cmd) return;
|
|
3346
|
+
const text = `/${cmd.name}`;
|
|
3347
|
+
const filled = "args" in cmd ? `${text} ` : text;
|
|
3348
|
+
setInput(filled);
|
|
3349
|
+
setCursor(filled.length);
|
|
3350
|
+
setSelectedIndex(0);
|
|
3351
|
+
});
|
|
3352
|
+
useInput((ch, key) => {
|
|
3353
|
+
if (key.return) {
|
|
3354
|
+
if (showSuggestions) {
|
|
3355
|
+
const cmd = suggestions[clampedIndex];
|
|
3356
|
+
if (cmd) {
|
|
3357
|
+
if ("args" in cmd) {
|
|
3358
|
+
applySuggestion(clampedIndex);
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
const text = `/${cmd.name}`;
|
|
3362
|
+
if (text === "/exit" || text === "/quit") {
|
|
3363
|
+
onExit();
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
onSubmit(text);
|
|
3367
|
+
reset();
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
const trimmed = input.trim();
|
|
3372
|
+
if (!trimmed) return;
|
|
3373
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
3374
|
+
onExit();
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
onSubmit(trimmed);
|
|
3378
|
+
reset();
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
if (key.tab) {
|
|
3382
|
+
if (showSuggestions) {
|
|
3383
|
+
applySuggestion(clampedIndex);
|
|
3384
|
+
}
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
if (key.upArrow) {
|
|
3388
|
+
if (showSuggestions) {
|
|
3389
|
+
setSelectedIndex((i) => (i - 1 + suggestions.length) % suggestions.length);
|
|
3390
|
+
}
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
if (key.downArrow) {
|
|
3394
|
+
if (showSuggestions) {
|
|
3395
|
+
setSelectedIndex((i) => (i + 1) % suggestions.length);
|
|
3396
|
+
}
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
if (key.escape) {
|
|
3400
|
+
if (showSuggestions) {
|
|
3401
|
+
setInput("");
|
|
3402
|
+
setCursor(0);
|
|
3403
|
+
setSelectedIndex(0);
|
|
3404
|
+
} else {
|
|
3405
|
+
onEscape?.();
|
|
3406
|
+
reset();
|
|
3407
|
+
}
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
if (key.backspace || key.delete) {
|
|
3411
|
+
if (cursor === 0) return;
|
|
3412
|
+
setInput((prev) => prev.slice(0, cursor - 1) + prev.slice(cursor));
|
|
3413
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
3414
|
+
setSelectedIndex(0);
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
if (key.leftArrow) {
|
|
3418
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
if (key.rightArrow) {
|
|
3422
|
+
setCursor((c) => Math.min(input.length, c + 1));
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
if (ch && !key.ctrl && !key.meta) {
|
|
3426
|
+
setInput((prev) => prev.slice(0, cursor) + ch + prev.slice(cursor));
|
|
3427
|
+
setCursor((c) => c + ch.length);
|
|
3428
|
+
setSelectedIndex(0);
|
|
3429
|
+
}
|
|
3430
|
+
});
|
|
3431
|
+
const before = input.slice(0, cursor);
|
|
3432
|
+
const after = input.slice(cursor);
|
|
3433
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
|
|
3434
|
+
showSuggestions && /* @__PURE__ */ jsx9(Box6, { flexDirection: "column", paddingX: 1, marginBottom: 0, children: suggestions.map((cmd, i) => {
|
|
3435
|
+
const isSelected = i === clampedIndex;
|
|
3436
|
+
const nameWithArgs = "args" in cmd ? `/${cmd.name} ${cmd.args}` : `/${cmd.name}`;
|
|
3437
|
+
return /* @__PURE__ */ jsxs7(Box6, { children: [
|
|
3438
|
+
/* @__PURE__ */ jsx9(Text7, { color: isSelected ? theme.baseToken.color.accent : void 0, children: isSelected ? "\u25B8 " : " " }),
|
|
3439
|
+
/* @__PURE__ */ jsx9(Text7, { bold: isSelected, color: isSelected ? theme.baseToken.color.accent : theme.baseToken.color.textNormal, children: nameWithArgs.padEnd(20) }),
|
|
3440
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
3441
|
+
" ",
|
|
3442
|
+
cmd.description
|
|
3443
|
+
] })
|
|
3444
|
+
] }, cmd.name);
|
|
3445
|
+
}) }),
|
|
3446
|
+
/* @__PURE__ */ jsxs7(
|
|
3447
|
+
Box6,
|
|
3448
|
+
{
|
|
3449
|
+
borderStyle: "single",
|
|
3450
|
+
borderColor: "gray",
|
|
3451
|
+
borderLeft: false,
|
|
3452
|
+
borderRight: false,
|
|
3453
|
+
paddingX: 1,
|
|
3454
|
+
children: [
|
|
3455
|
+
/* @__PURE__ */ jsx9(Text7, { color: theme.user.prefixColor, bold: true, children: theme.user.prefix }),
|
|
3456
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
3457
|
+
" ",
|
|
3458
|
+
before
|
|
3459
|
+
] }),
|
|
3460
|
+
/* @__PURE__ */ jsx9(Text7, { color: "white", inverse: true, children: after.length > 0 ? after[0] : " " }),
|
|
3461
|
+
/* @__PURE__ */ jsx9(Text7, { children: after.slice(1) }),
|
|
3462
|
+
/* @__PURE__ */ jsx9(Text7, { dimColor: true, children: busy ? " (thinking...)" : "" })
|
|
3463
|
+
]
|
|
3464
|
+
}
|
|
3465
|
+
)
|
|
3466
|
+
] });
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
// src/tui/components/status-bar.tsx
|
|
3470
|
+
import React5, { useMemo as useMemo3, useState as useState4, useEffect as useEffect2 } from "react";
|
|
3471
|
+
import { Box as Box7, Text as Text8 } from "ink";
|
|
3472
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3473
|
+
function StatusBar({
|
|
3474
|
+
activeWorkers,
|
|
3475
|
+
pendingTasks,
|
|
3476
|
+
runningTasks
|
|
3477
|
+
}) {
|
|
3478
|
+
const [now, setNow] = useState4(() => Date.now());
|
|
3479
|
+
useEffect2(() => {
|
|
3480
|
+
if (activeWorkers.length === 0) return;
|
|
3481
|
+
const timer = setInterval(() => setNow(Date.now()), 1e3);
|
|
3482
|
+
return () => clearInterval(timer);
|
|
3483
|
+
}, [activeWorkers.length]);
|
|
3484
|
+
const workerRows = useMemo3(
|
|
3485
|
+
() => {
|
|
3486
|
+
return activeWorkers.map((w) => {
|
|
3487
|
+
const task = runningTasks.find((t) => t.id === w.taskId);
|
|
3488
|
+
const elapsed = Math.round((now - w.startedAt) / 1e3);
|
|
3489
|
+
const label = task ? task.title || task.description.slice(0, 10) : w.taskId.slice(0, 8);
|
|
3490
|
+
return { workerId: w.workerId, label, elapsed };
|
|
3491
|
+
});
|
|
3492
|
+
},
|
|
3493
|
+
[activeWorkers, runningTasks, now]
|
|
3494
|
+
);
|
|
3495
|
+
if (activeWorkers.length === 0 && pendingTasks.length === 0) {
|
|
3496
|
+
return null;
|
|
3497
|
+
}
|
|
3498
|
+
const parts = [];
|
|
3499
|
+
if (pendingTasks.length > 0) {
|
|
3500
|
+
parts.push(`pending tasks:${pendingTasks.length}`);
|
|
3501
|
+
}
|
|
3502
|
+
if (activeWorkers.length > 0) {
|
|
3503
|
+
parts.push(`active workers:${activeWorkers.length}`);
|
|
3504
|
+
}
|
|
3505
|
+
return /* @__PURE__ */ jsxs8(Box7, { paddingX: 1, marginTop: 0, children: [
|
|
3506
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
3507
|
+
" ",
|
|
3508
|
+
parts.join(" \u2502 "),
|
|
3509
|
+
" "
|
|
3510
|
+
] }),
|
|
3511
|
+
workerRows.map((r, i) => /* @__PURE__ */ jsxs8(React5.Fragment, { children: [
|
|
3512
|
+
i > 0 && /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: " \xB7 " }),
|
|
3513
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, color: "blue", children: [
|
|
3514
|
+
r.label,
|
|
3515
|
+
" (",
|
|
3516
|
+
r.elapsed,
|
|
3517
|
+
"s)"
|
|
3518
|
+
] })
|
|
3519
|
+
] }, r.workerId))
|
|
3520
|
+
] });
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
// src/tui/components/overlay-panel.tsx
|
|
3524
|
+
import { Box as Box8, Text as Text9 } from "ink";
|
|
3525
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3526
|
+
function OverlayPanel({ overlay }) {
|
|
3527
|
+
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginTop: 1, children: [
|
|
3528
|
+
overlay.kind === "text" ? /* @__PURE__ */ jsx11(Box8, { paddingX: 1, borderStyle: "single", borderColor: "gray", flexDirection: "column", children: /* @__PURE__ */ jsx11(Text9, { color: "gray", children: overlay.content }) }) : overlay.render(),
|
|
3529
|
+
overlay.hint && /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
3530
|
+
" ",
|
|
3531
|
+
overlay.hint
|
|
3532
|
+
] })
|
|
3533
|
+
] });
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// src/tui/components/worker-list-overlay.tsx
|
|
3537
|
+
import { useState as useState5 } from "react";
|
|
3538
|
+
import { Box as Box9, Text as Text10, useInput as useInput2 } from "ink";
|
|
3539
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3540
|
+
function WorkerListOverlay({ workers, onSelect, onDismiss }) {
|
|
3541
|
+
const [selectedIndex, setSelectedIndex] = useState5(0);
|
|
3542
|
+
useInput2((_ch, key) => {
|
|
3543
|
+
if (key.escape) {
|
|
3544
|
+
onDismiss();
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
if (key.return && workers.length > 0) {
|
|
3548
|
+
onSelect(workers[selectedIndex].workerId);
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
if (key.upArrow) {
|
|
3552
|
+
setSelectedIndex((i) => (i - 1 + workers.length) % workers.length);
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
if (key.downArrow) {
|
|
3556
|
+
setSelectedIndex((i) => (i + 1) % workers.length);
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
});
|
|
3560
|
+
if (workers.length === 0) {
|
|
3561
|
+
return /* @__PURE__ */ jsx12(Box9, { paddingX: 1, borderStyle: "single", borderColor: "gray", children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "No active workers" }) });
|
|
3562
|
+
}
|
|
3563
|
+
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", paddingX: 1, borderStyle: "single", borderColor: "gray", children: [
|
|
3564
|
+
/* @__PURE__ */ jsxs10(Text10, { bold: true, dimColor: true, children: [
|
|
3565
|
+
"Workers (",
|
|
3566
|
+
workers.length,
|
|
3567
|
+
")"
|
|
3568
|
+
] }),
|
|
3569
|
+
workers.map((w, i) => {
|
|
3570
|
+
const isSelected = i === selectedIndex;
|
|
3571
|
+
const elapsed = Math.round((Date.now() - w.startedAt) / 1e3);
|
|
3572
|
+
const mins = Math.floor(elapsed / 60);
|
|
3573
|
+
const secs = elapsed % 60;
|
|
3574
|
+
const timeStr = mins > 0 ? `${mins}m${secs}s` : `${secs}s`;
|
|
3575
|
+
return /* @__PURE__ */ jsxs10(Box9, { children: [
|
|
3576
|
+
/* @__PURE__ */ jsx12(Text10, { color: isSelected ? theme.baseToken.color.accent : void 0, children: isSelected ? "\u25B8 " : " " }),
|
|
3577
|
+
/* @__PURE__ */ jsxs10(Text10, { bold: isSelected, color: isSelected ? theme.baseToken.color.accent : theme.baseToken.color.textNormal, children: [
|
|
3578
|
+
w.emoji,
|
|
3579
|
+
" ",
|
|
3580
|
+
w.taskTitle
|
|
3581
|
+
] }),
|
|
3582
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
3583
|
+
" ",
|
|
3584
|
+
w.workerType,
|
|
3585
|
+
" \xB7 ",
|
|
3586
|
+
w.taskId,
|
|
3587
|
+
" \xB7 ",
|
|
3588
|
+
timeStr
|
|
3589
|
+
] })
|
|
3590
|
+
] }, w.workerId);
|
|
3591
|
+
})
|
|
3592
|
+
] });
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
// src/tui/hooks/use-wayang-state.ts
|
|
3596
|
+
import { useState as useState6, useEffect as useEffect3 } from "react";
|
|
3597
|
+
function useWayangState(state, path5) {
|
|
3598
|
+
const [value, setValue] = useState6(() => state.get(path5));
|
|
3599
|
+
useEffect3(() => {
|
|
3600
|
+
const unSub = state.on(path5, (_event) => {
|
|
3601
|
+
setValue(state.get(path5));
|
|
3602
|
+
});
|
|
3603
|
+
setValue(state.get(path5));
|
|
3604
|
+
return unSub;
|
|
3605
|
+
}, [state, path5]);
|
|
3606
|
+
return value;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
// src/tui/types/display-item.ts
|
|
3610
|
+
function entryToDisplayItem(entry) {
|
|
3611
|
+
const base = {
|
|
3612
|
+
id: entry.uuid,
|
|
3613
|
+
timestamp: new Date(entry.timestamp).getTime(),
|
|
3614
|
+
read: true
|
|
3615
|
+
};
|
|
3616
|
+
switch (entry.type) {
|
|
3617
|
+
case "user" /* User */:
|
|
3618
|
+
return { ...base, role: "user", content: entry.message.content };
|
|
3619
|
+
case "assistant" /* Assistant */: {
|
|
3620
|
+
const hasSkipReply = entry.toolCalls?.some((tc) => tc.toolName === "skip_reply");
|
|
3621
|
+
if (hasSkipReply) return null;
|
|
3622
|
+
const sections = [];
|
|
3623
|
+
if (entry.message.reasoning) {
|
|
3624
|
+
sections.push({ kind: "reasoning", id: "reasoning", text: entry.message.reasoning });
|
|
3625
|
+
}
|
|
3626
|
+
if (entry.message.content) {
|
|
3627
|
+
sections.push({ kind: "content", id: "content", text: entry.message.content });
|
|
3628
|
+
}
|
|
3629
|
+
for (const tc of entry.toolCalls ?? []) {
|
|
3630
|
+
sections.push({
|
|
3631
|
+
kind: "tool_call",
|
|
3632
|
+
id: tc.toolCallId,
|
|
3633
|
+
text: tc.arguments,
|
|
3634
|
+
toolName: tc.toolName
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
for (const tr of entry.toolResults ?? []) {
|
|
3638
|
+
sections.push({
|
|
3639
|
+
kind: "tool_result",
|
|
3640
|
+
id: tr.toolCallId,
|
|
3641
|
+
text: tr.output.value,
|
|
3642
|
+
toolName: tr.toolName,
|
|
3643
|
+
isError: tr.isError
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
return {
|
|
3647
|
+
...base,
|
|
3648
|
+
role: "assistant",
|
|
3649
|
+
content: entry.message.content,
|
|
3650
|
+
sections
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
case "signal" /* Signal */:
|
|
3654
|
+
return { ...base, role: "signal", subtype: entry.subtype, content: entry.content, taskId: entry.taskId, taskTitle: entry.taskTitle, workerId: entry.workerId, workerType: entry.workerType, emoji: entry.emoji };
|
|
3655
|
+
case "system" /* System */:
|
|
3656
|
+
return { ...base, role: "system", subtype: entry.subtype, content: entry.content };
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
function signalToDisplayItem(signal) {
|
|
3660
|
+
const base = {
|
|
3661
|
+
id: signal.id,
|
|
3662
|
+
timestamp: signal.timestamp,
|
|
3663
|
+
read: false
|
|
3664
|
+
};
|
|
3665
|
+
if (signal.type === "input") {
|
|
3666
|
+
return {
|
|
3667
|
+
...base,
|
|
3668
|
+
role: "user",
|
|
3669
|
+
content: signal.payload.text
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
const subtype = signal.type === "completed" ? "worker_completed" /* WorkerCompleted */ : signal.type === "failed" ? "worker_failed" /* WorkerFailed */ : "worker_progress" /* WorkerProgress */;
|
|
3673
|
+
const content = getSignalField(signal, "summary") ?? getSignalField(signal, "error") ?? getSignalField(signal, "message") ?? "";
|
|
3674
|
+
const taskId = getSignalField(signal, "taskId");
|
|
3675
|
+
const taskTitle = getSignalField(signal, "taskTitle");
|
|
3676
|
+
return {
|
|
3677
|
+
...base,
|
|
3678
|
+
role: "signal",
|
|
3679
|
+
subtype,
|
|
3680
|
+
content,
|
|
3681
|
+
taskId,
|
|
3682
|
+
workerId: getSignalField(signal, "workerId"),
|
|
3683
|
+
workerType: getSignalField(signal, "workerType"),
|
|
3684
|
+
emoji: getSignalField(signal, "emoji"),
|
|
3685
|
+
taskTitle
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
// src/tui/pages/controller-page.tsx
|
|
3690
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3691
|
+
function ControllerPage({ onExit }) {
|
|
3692
|
+
const supervisor = useSupervisor();
|
|
3693
|
+
const { navigate } = useRouter();
|
|
3694
|
+
const { stdout } = useStdout();
|
|
3695
|
+
const terminalCols = stdout?.columns ?? 80;
|
|
3696
|
+
const chatContentWidth = terminalCols - 4;
|
|
3697
|
+
const conversation = useWayangState(
|
|
3698
|
+
supervisor.controllerState,
|
|
3699
|
+
"conversation"
|
|
3700
|
+
);
|
|
3701
|
+
const streamingEntries = useWayangState(
|
|
3702
|
+
supervisor.controllerState,
|
|
3703
|
+
"dynamicState.streamingEntries"
|
|
3704
|
+
);
|
|
3705
|
+
const signals = useWayangState(
|
|
3706
|
+
supervisor.signalQueue.stateRef,
|
|
3707
|
+
"signals"
|
|
3708
|
+
);
|
|
3709
|
+
const activeWorkers = useWayangState(
|
|
3710
|
+
supervisor.controllerState,
|
|
3711
|
+
"runtimeState.activeWorkers"
|
|
3712
|
+
);
|
|
3713
|
+
const pendingTasks = useWayangState(
|
|
3714
|
+
supervisor.taskPool.stateRef,
|
|
3715
|
+
"tasks.pending"
|
|
3716
|
+
);
|
|
3717
|
+
const runningTasks = useWayangState(
|
|
3718
|
+
supervisor.taskPool.stateRef,
|
|
3719
|
+
"tasks.running"
|
|
3720
|
+
);
|
|
3721
|
+
const agentBusy = useWayangState(
|
|
3722
|
+
supervisor.controllerState,
|
|
3723
|
+
"dynamicState.busy"
|
|
3724
|
+
);
|
|
3725
|
+
const displayItems = useMemo4(() => {
|
|
3726
|
+
const readItems = (conversation ?? []).map(entryToDisplayItem).filter((d) => d !== null);
|
|
3727
|
+
const unreadSignals = (signals ?? []).filter((s) => s.status === "unread").map(signalToDisplayItem);
|
|
3728
|
+
return [...readItems, ...unreadSignals];
|
|
3729
|
+
}, [conversation, signals]);
|
|
3730
|
+
const streamingItems = useMemo4(() => {
|
|
3731
|
+
const entries = streamingEntries ?? [];
|
|
3732
|
+
if (entries.length === 0) return [];
|
|
3733
|
+
const conversationIds = new Set((conversation ?? []).map((e) => e.uuid));
|
|
3734
|
+
return entries.filter((e) => !conversationIds.has(e.uuid)).map(entryToDisplayItem).filter((d) => d !== null);
|
|
3735
|
+
}, [streamingEntries, conversation]);
|
|
3736
|
+
const [overlay, setOverlay] = useState7(null);
|
|
3737
|
+
const busy = agentBusy ?? false;
|
|
3738
|
+
const dismissOverlay = useMemoizedFn(() => setOverlay(null));
|
|
3739
|
+
const slashCtx = useMemo4(() => ({
|
|
3740
|
+
navigate
|
|
3741
|
+
}), [navigate]);
|
|
3742
|
+
const { handleCommand } = useSlashCommands(supervisor, slashCtx);
|
|
3743
|
+
const handleSubmit = useMemoizedFn((input) => {
|
|
3744
|
+
const cmdResult = handleCommand(input);
|
|
3745
|
+
if (cmdResult.handled) {
|
|
3746
|
+
if (cmdResult.output === "__EXIT__") {
|
|
3747
|
+
onExit();
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
if (cmdResult.navigate) {
|
|
3751
|
+
navigate(cmdResult.navigate);
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
if (cmdResult.action === "workers") {
|
|
3755
|
+
setOverlay({
|
|
3756
|
+
kind: "component",
|
|
3757
|
+
render: () => /* @__PURE__ */ jsx13(
|
|
3758
|
+
WorkerListOverlay,
|
|
3759
|
+
{
|
|
3760
|
+
workers: activeWorkers ?? [],
|
|
3761
|
+
onSelect: (workerId) => {
|
|
3762
|
+
dismissOverlay();
|
|
3763
|
+
navigate({ page: "worker", workerId });
|
|
3764
|
+
},
|
|
3765
|
+
onDismiss: dismissOverlay
|
|
3766
|
+
}
|
|
3767
|
+
),
|
|
3768
|
+
hint: "\u2191\u2193 select \xB7 enter to view \xB7 esc to dismiss"
|
|
3769
|
+
});
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
if (cmdResult.action === "compact") {
|
|
3773
|
+
setOverlay(cmdResult.output ? { kind: "text", content: cmdResult.output } : null);
|
|
3774
|
+
supervisor.controllerAgent.performCompaction().then(() => setOverlay(null)).catch(() => setOverlay({ kind: "text", content: "Compaction failed", hint: "(esc to dismiss)" }));
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3777
|
+
if (cmdResult.output) {
|
|
3778
|
+
setOverlay({ kind: "text", content: cmdResult.output, hint: "(esc to dismiss)" });
|
|
3779
|
+
}
|
|
3780
|
+
return;
|
|
3781
|
+
}
|
|
3782
|
+
setOverlay(null);
|
|
3783
|
+
supervisor.signalQueue.enqueue({
|
|
3784
|
+
source: "user",
|
|
3785
|
+
type: "input",
|
|
3786
|
+
payload: { text: input }
|
|
3787
|
+
});
|
|
3788
|
+
supervisor.sessionManager.onUserInput(input);
|
|
3789
|
+
});
|
|
3790
|
+
return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
|
|
3791
|
+
/* @__PURE__ */ jsx13(Box10, { marginBottom: 1, children: /* @__PURE__ */ jsx13(
|
|
3792
|
+
ChatArea,
|
|
3793
|
+
{
|
|
3794
|
+
items: displayItems,
|
|
3795
|
+
busy,
|
|
3796
|
+
streamingItems,
|
|
3797
|
+
contentWidth: chatContentWidth
|
|
3798
|
+
}
|
|
3799
|
+
) }),
|
|
3800
|
+
overlay && /* @__PURE__ */ jsx13(OverlayPanel, { overlay }),
|
|
3801
|
+
/* @__PURE__ */ jsx13(InputArea, { onSubmit: handleSubmit, onExit, onEscape: dismissOverlay, busy }),
|
|
3802
|
+
/* @__PURE__ */ jsx13(
|
|
3803
|
+
StatusBar,
|
|
3804
|
+
{
|
|
3805
|
+
activeWorkers: activeWorkers ?? [],
|
|
3806
|
+
pendingTasks: pendingTasks ?? [],
|
|
3807
|
+
runningTasks: runningTasks ?? []
|
|
3808
|
+
}
|
|
3809
|
+
)
|
|
3810
|
+
] });
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
// src/tui/pages/worker-detail-page.tsx
|
|
3814
|
+
import { useState as useState8, useEffect as useEffect4, useMemo as useMemo5 } from "react";
|
|
3815
|
+
import { Box as Box11, Text as Text11 } from "ink";
|
|
3816
|
+
import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3817
|
+
var MAX_VISIBLE_ENTRIES = 15;
|
|
3818
|
+
var CONTENT_MAX_LENGTH = 100;
|
|
3819
|
+
var TOOL_CONTENT_MAX_LENGTH = 80;
|
|
3820
|
+
function WorkerDetailPage({ workerId, onBack }) {
|
|
3821
|
+
const supervisor = useSupervisor();
|
|
3822
|
+
const workerState = supervisor.getWorkerState(workerId);
|
|
3823
|
+
if (!workerState) {
|
|
3824
|
+
return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", padding: 1, children: [
|
|
3825
|
+
/* @__PURE__ */ jsxs12(Text11, { color: "red", children: [
|
|
3826
|
+
"Worker ",
|
|
3827
|
+
workerId,
|
|
3828
|
+
" not found"
|
|
3829
|
+
] }),
|
|
3830
|
+
/* @__PURE__ */ jsx14(Text11, { color: "gray", children: "Press ESC to go back" })
|
|
3831
|
+
] });
|
|
3832
|
+
}
|
|
3833
|
+
return /* @__PURE__ */ jsx14(WorkerContent, { workerState, workerId });
|
|
3834
|
+
}
|
|
3835
|
+
function WorkerContent({ workerState, workerId }) {
|
|
3836
|
+
const supervisor = useSupervisor();
|
|
3837
|
+
const taskInfo = useWayangState(workerState, "runtimeState.task");
|
|
3838
|
+
const conversation = useWayangState(workerState, "conversation");
|
|
3839
|
+
const activeWorkers = useWayangState(
|
|
3840
|
+
supervisor.controllerState,
|
|
3841
|
+
"runtimeState.activeWorkers"
|
|
3842
|
+
);
|
|
3843
|
+
const workerInfo = useMemo5(
|
|
3844
|
+
() => (activeWorkers ?? []).find((w) => w.workerId === workerId),
|
|
3845
|
+
[activeWorkers, workerId]
|
|
3846
|
+
);
|
|
3847
|
+
const isRunning = !!workerInfo;
|
|
3848
|
+
const emoji = workerInfo?.emoji ?? "?";
|
|
3849
|
+
const workerType = workerInfo?.workerType ?? "puppet";
|
|
3850
|
+
const taskTitle = workerInfo?.taskTitle ?? taskInfo?.title ?? "N/A";
|
|
3851
|
+
return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", height: "100%", children: [
|
|
3852
|
+
/* @__PURE__ */ jsxs12(Box11, { borderStyle: "bold", borderColor: "yellow", paddingX: 1, children: [
|
|
3853
|
+
/* @__PURE__ */ jsxs12(Text11, { color: "yellow", bold: true, children: [
|
|
3854
|
+
emoji,
|
|
3855
|
+
" ",
|
|
3856
|
+
workerType
|
|
3857
|
+
] }),
|
|
3858
|
+
/* @__PURE__ */ jsx14(Text11, { dimColor: true, children: " \xB7 " }),
|
|
3859
|
+
/* @__PURE__ */ jsx14(Text11, { bold: true, children: taskTitle })
|
|
3860
|
+
] }),
|
|
3861
|
+
/* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", flexGrow: 1, padding: 1, children: [
|
|
3862
|
+
/* @__PURE__ */ jsx14(Text11, { bold: true, children: "Task:" }),
|
|
3863
|
+
/* @__PURE__ */ jsxs12(Text11, { children: [
|
|
3864
|
+
" ",
|
|
3865
|
+
taskInfo?.description ?? "N/A"
|
|
3866
|
+
] }),
|
|
3867
|
+
/* @__PURE__ */ jsxs12(Text11, { bold: true, children: [
|
|
3868
|
+
"Conversation (",
|
|
3869
|
+
conversation?.length ?? 0,
|
|
3870
|
+
" entries):"
|
|
3871
|
+
] }),
|
|
3872
|
+
(conversation ?? []).slice(-MAX_VISIBLE_ENTRIES).map((entry, i) => /* @__PURE__ */ jsx14(ConversationRow, { entry }, `${entry.uuid}-${i}`))
|
|
3873
|
+
] }),
|
|
3874
|
+
/* @__PURE__ */ jsxs12(Box11, { paddingX: 1, justifyContent: "space-between", children: [
|
|
3875
|
+
/* @__PURE__ */ jsx14(Text11, { dimColor: true, children: "Press Esc to go back" }),
|
|
3876
|
+
isRunning && /* @__PURE__ */ jsx14(WorkerTimer, { startedAt: workerInfo.startedAt }),
|
|
3877
|
+
!isRunning && /* @__PURE__ */ jsx14(Text11, { dimColor: true, children: "Done" })
|
|
3878
|
+
] })
|
|
3879
|
+
] });
|
|
3880
|
+
}
|
|
3881
|
+
function WorkerTimer({ startedAt }) {
|
|
3882
|
+
const [now, setNow] = useState8(Date.now());
|
|
3883
|
+
const [frame, setFrame] = useState8(0);
|
|
3884
|
+
useEffect4(() => {
|
|
3885
|
+
const timer = setInterval(() => {
|
|
3886
|
+
setNow(Date.now());
|
|
3887
|
+
setFrame((f) => (f + 1) % theme.spinner.frames.length);
|
|
3888
|
+
}, 500);
|
|
3889
|
+
return () => clearInterval(timer);
|
|
3890
|
+
}, []);
|
|
3891
|
+
const elapsed = Math.round((now - startedAt) / 1e3);
|
|
3892
|
+
const mins = Math.floor(elapsed / 60);
|
|
3893
|
+
const secs = elapsed % 60;
|
|
3894
|
+
const timeStr = mins > 0 ? `${mins}m${secs}s` : `${secs}s`;
|
|
3895
|
+
return /* @__PURE__ */ jsxs12(Text11, { children: [
|
|
3896
|
+
/* @__PURE__ */ jsxs12(Text11, { color: theme.assistant.prefixColor, bold: true, children: [
|
|
3897
|
+
theme.spinner.frames[frame],
|
|
3898
|
+
" "
|
|
3899
|
+
] }),
|
|
3900
|
+
/* @__PURE__ */ jsxs12(Text11, { dimColor: true, children: [
|
|
3901
|
+
"Running ",
|
|
3902
|
+
timeStr
|
|
3903
|
+
] })
|
|
3904
|
+
] });
|
|
3905
|
+
}
|
|
3906
|
+
function ConversationRow({ entry }) {
|
|
3907
|
+
switch (entry.type) {
|
|
3908
|
+
case "user":
|
|
3909
|
+
return /* @__PURE__ */ jsxs12(Text11, { children: [
|
|
3910
|
+
/* @__PURE__ */ jsx14(Text11, { color: "cyan", bold: true, children: "> " }),
|
|
3911
|
+
entry.message?.content?.slice(0, CONTENT_MAX_LENGTH)
|
|
3912
|
+
] });
|
|
3913
|
+
case "assistant": {
|
|
3914
|
+
const items = [];
|
|
3915
|
+
if (entry.message?.content) {
|
|
3916
|
+
items.push(
|
|
3917
|
+
/* @__PURE__ */ jsxs12(Text11, { children: [
|
|
3918
|
+
/* @__PURE__ */ jsx14(Text11, { color: "green", children: " " }),
|
|
3919
|
+
entry.message.content.slice(0, CONTENT_MAX_LENGTH)
|
|
3920
|
+
] }, "text")
|
|
3921
|
+
);
|
|
3922
|
+
}
|
|
3923
|
+
if (entry.toolCalls?.length) {
|
|
3924
|
+
for (const tc of entry.toolCalls) {
|
|
3925
|
+
items.push(
|
|
3926
|
+
/* @__PURE__ */ jsxs12(Text11, { dimColor: true, children: [
|
|
3927
|
+
" [",
|
|
3928
|
+
tc.toolName,
|
|
3929
|
+
"] ",
|
|
3930
|
+
tc.arguments.slice(0, TOOL_CONTENT_MAX_LENGTH)
|
|
3931
|
+
] }, `tc-${tc.toolCallId}`)
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
if (entry.toolResults?.length) {
|
|
3936
|
+
for (const tr of entry.toolResults) {
|
|
3937
|
+
items.push(
|
|
3938
|
+
/* @__PURE__ */ jsxs12(Text11, { dimColor: true, children: [
|
|
3939
|
+
" [",
|
|
3940
|
+
tr.toolName,
|
|
3941
|
+
"] ",
|
|
3942
|
+
tr.output.value.slice(0, TOOL_CONTENT_MAX_LENGTH)
|
|
3943
|
+
] }, `tr-${tr.toolCallId}`)
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
return items.length > 0 ? /* @__PURE__ */ jsx14(Fragment2, { children: items }) : null;
|
|
3948
|
+
}
|
|
3949
|
+
case "signal":
|
|
3950
|
+
return /* @__PURE__ */ jsxs12(Text11, { color: "yellow", children: [
|
|
3951
|
+
" [sig] ",
|
|
3952
|
+
entry.content?.slice(0, TOOL_CONTENT_MAX_LENGTH)
|
|
3953
|
+
] });
|
|
3954
|
+
case "system":
|
|
3955
|
+
return /* @__PURE__ */ jsxs12(Text11, { color: "gray", children: [
|
|
3956
|
+
" [sys] ",
|
|
3957
|
+
entry.content?.slice(0, TOOL_CONTENT_MAX_LENGTH)
|
|
3958
|
+
] });
|
|
3959
|
+
default:
|
|
3960
|
+
return null;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
// src/tui/app.tsx
|
|
3965
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3966
|
+
function App({ supervisor }) {
|
|
3967
|
+
return /* @__PURE__ */ jsx15(SupervisorProvider, { supervisor, children: /* @__PURE__ */ jsx15(RouteProvider, { children: /* @__PURE__ */ jsx15(AppContent, {}) }) });
|
|
3968
|
+
}
|
|
3969
|
+
function AppContent() {
|
|
3970
|
+
const { exit } = useApp();
|
|
3971
|
+
const supervisor = useSupervisor();
|
|
3972
|
+
const { route, navigate } = useRouter();
|
|
3973
|
+
const busy = useWayangState(supervisor.controllerState, "dynamicState.busy") ?? false;
|
|
3974
|
+
const lastCtrlC = useRef2(0);
|
|
3975
|
+
const handleExit = useMemoizedFn(() => {
|
|
3976
|
+
exit();
|
|
3977
|
+
});
|
|
3978
|
+
useInput3((_ch, key) => {
|
|
3979
|
+
if (key.escape && route.page === "worker") {
|
|
3980
|
+
navigate({ page: "controller" });
|
|
3981
|
+
}
|
|
3982
|
+
if (key.ctrl && _ch === "c") {
|
|
3983
|
+
if (busy) {
|
|
3984
|
+
supervisor.controllerAgent.abort();
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
const now = Date.now();
|
|
3988
|
+
if (lastCtrlC.current && now - lastCtrlC.current < 1e3) {
|
|
3989
|
+
supervisor.shutdown().then(() => exit());
|
|
3990
|
+
} else {
|
|
3991
|
+
lastCtrlC.current = now;
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
|
|
3996
|
+
/* @__PURE__ */ jsx15(Box12, { display: route.page === "controller" ? "flex" : "none", flexDirection: "column", children: /* @__PURE__ */ jsx15(ControllerPage, { onExit: handleExit }) }),
|
|
3997
|
+
route.page === "worker" && /* @__PURE__ */ jsx15(WorkerDetailPage, { workerId: route.workerId, onBack: () => navigate({ page: "controller" }) })
|
|
3998
|
+
] });
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// package.json
|
|
4002
|
+
var package_default = {
|
|
4003
|
+
name: "wayang-ai",
|
|
4004
|
+
version: "0.1.0",
|
|
4005
|
+
description: "Multi-agent orchestration platform",
|
|
4006
|
+
type: "module",
|
|
4007
|
+
license: "MIT",
|
|
4008
|
+
author: "pgYou",
|
|
4009
|
+
repository: {
|
|
4010
|
+
type: "git",
|
|
4011
|
+
url: "git+https://github.com/pgYou/wayang.git"
|
|
4012
|
+
},
|
|
4013
|
+
homepage: "https://github.com/pgYou/wayang#readme",
|
|
4014
|
+
bugs: {
|
|
4015
|
+
url: "https://github.com/pgYou/wayang/issues"
|
|
4016
|
+
},
|
|
4017
|
+
keywords: [
|
|
4018
|
+
"ai",
|
|
4019
|
+
"agent",
|
|
4020
|
+
"multi-agent",
|
|
4021
|
+
"orchestration",
|
|
4022
|
+
"cli",
|
|
4023
|
+
"llm",
|
|
4024
|
+
"claude"
|
|
4025
|
+
],
|
|
4026
|
+
bin: {
|
|
4027
|
+
wayang: "./dist/cli.js",
|
|
4028
|
+
waya: "./dist/cli.js"
|
|
4029
|
+
},
|
|
4030
|
+
files: [
|
|
4031
|
+
"dist",
|
|
4032
|
+
"!dist/**/*.map"
|
|
4033
|
+
],
|
|
4034
|
+
scripts: {
|
|
4035
|
+
build: "tsup",
|
|
4036
|
+
"dev:watch": "tsup --watch",
|
|
4037
|
+
dev: "tsup && node --env-file=.env.local dist/cli.js --home-dir . -w . -c ./config.local.json --verbose",
|
|
4038
|
+
start: "node dist/cli.js",
|
|
4039
|
+
prepublishOnly: "npm run build",
|
|
4040
|
+
test: "vitest run",
|
|
4041
|
+
"test:watch": "vitest",
|
|
4042
|
+
"test:coverage": "vitest run --coverage",
|
|
4043
|
+
lint: "tsc --noEmit && eslint src/"
|
|
4044
|
+
},
|
|
4045
|
+
engines: {
|
|
4046
|
+
node: ">=20"
|
|
4047
|
+
},
|
|
4048
|
+
dependencies: {
|
|
4049
|
+
"@ai-sdk/openai": "^3.0.50",
|
|
4050
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.98",
|
|
4051
|
+
"@types/react": "^18.3.28",
|
|
4052
|
+
ai: "^6.0.145",
|
|
4053
|
+
ink: "^4.4.1",
|
|
4054
|
+
"lodash-es": "^4.18.1",
|
|
4055
|
+
meow: "^14.1.0",
|
|
4056
|
+
pino: "^9.6.0",
|
|
4057
|
+
"pino-pretty": "^13.0.0",
|
|
4058
|
+
prompts: "^2.4.2",
|
|
4059
|
+
react: "^18.3.1",
|
|
4060
|
+
zod: "^4.3.6"
|
|
4061
|
+
},
|
|
4062
|
+
devDependencies: {
|
|
4063
|
+
"@eslint/js": "^9.0.0",
|
|
4064
|
+
"@types/lodash-es": "^4.17.12",
|
|
4065
|
+
"@types/node": "^22.0.0",
|
|
4066
|
+
"@types/prompts": "^2.4.9",
|
|
4067
|
+
eslint: "^9.0.0",
|
|
4068
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
4069
|
+
tsup: "^8.5.1",
|
|
4070
|
+
typescript: "^5.8.0",
|
|
4071
|
+
"typescript-eslint": "^8.58.0",
|
|
4072
|
+
vitest: "^4.1.2"
|
|
4073
|
+
}
|
|
4074
|
+
};
|
|
4075
|
+
|
|
4076
|
+
// src/tui/welcome.ts
|
|
4077
|
+
var logo3d = `
|
|
4078
|
+
\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
4079
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
4080
|
+
\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557
|
|
4081
|
+
\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
4082
|
+
\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
|
|
4083
|
+
\u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
|
|
4084
|
+
`;
|
|
4085
|
+
var pkgVersion = package_default.version;
|
|
4086
|
+
function writeWelcome(workspace) {
|
|
4087
|
+
const GREEN = "\x1B[32m";
|
|
4088
|
+
const BOLD = "\x1B[1m";
|
|
4089
|
+
const DIM = "\x1B[2m";
|
|
4090
|
+
const RESET = "\x1B[0m";
|
|
4091
|
+
process.stdout.write("\n");
|
|
4092
|
+
process.stdout.write(GREEN + logo3d + RESET);
|
|
4093
|
+
process.stdout.write("\n");
|
|
4094
|
+
process.stdout.write(` Welcome to play with ${BOLD}Wayang${RESET}
|
|
4095
|
+
`);
|
|
4096
|
+
process.stdout.write(` ${DIM}${pkgVersion} \xB7 ${workspace}${RESET}
|
|
4097
|
+
`);
|
|
4098
|
+
process.stdout.write("\n");
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
// src/tui/render.ts
|
|
4102
|
+
function renderInkUI(supervisor) {
|
|
4103
|
+
return new Promise((resolve4) => {
|
|
4104
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
4105
|
+
writeWelcome(supervisor.ctx.workspaceDir);
|
|
4106
|
+
const { waitUntilExit } = render(
|
|
4107
|
+
React7.createElement(App, { supervisor })
|
|
4108
|
+
);
|
|
4109
|
+
waitUntilExit().then(() => resolve4());
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
// src/onboard.ts
|
|
4114
|
+
import * as fs2 from "fs";
|
|
4115
|
+
import * as path3 from "path";
|
|
4116
|
+
import * as childProcess from "child_process";
|
|
4117
|
+
import prompts from "prompts";
|
|
4118
|
+
function fillApiKeys(config) {
|
|
4119
|
+
const envKey = process.env.WAYANG_LLM_API_KEY || "";
|
|
4120
|
+
for (const provider of Object.values(config.providers)) {
|
|
4121
|
+
if (!provider.apiKey && envKey) {
|
|
4122
|
+
provider.apiKey = envKey;
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
function detectClaudeCode() {
|
|
4127
|
+
try {
|
|
4128
|
+
const result = childProcess.execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
4129
|
+
return result || null;
|
|
4130
|
+
} catch {
|
|
4131
|
+
return null;
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
async function promptClaudeCodeWorker(claudeDetected) {
|
|
4135
|
+
const hint = claudeDetected ? "Claude Code detected. Configure as a worker?" : "Configure Claude Code worker? (install Claude Code CLI first)";
|
|
4136
|
+
const { enable } = await prompts({
|
|
4137
|
+
type: "confirm",
|
|
4138
|
+
name: "enable",
|
|
4139
|
+
message: hint,
|
|
4140
|
+
initial: claudeDetected
|
|
4141
|
+
});
|
|
4142
|
+
if (!enable) return void 0;
|
|
4143
|
+
const answers = await prompts([
|
|
4144
|
+
{
|
|
4145
|
+
type: "text",
|
|
4146
|
+
name: "emoji",
|
|
4147
|
+
message: "Worker emoji",
|
|
4148
|
+
initial: "\u{1F980}"
|
|
4149
|
+
},
|
|
4150
|
+
{
|
|
4151
|
+
type: "text",
|
|
4152
|
+
name: "cliPath",
|
|
4153
|
+
message: "Path to Claude Code CLI (leave empty for auto-detect)",
|
|
4154
|
+
initial: ""
|
|
4155
|
+
},
|
|
4156
|
+
{
|
|
4157
|
+
type: "number",
|
|
4158
|
+
name: "maxTurns",
|
|
4159
|
+
message: "Max turns per task",
|
|
4160
|
+
initial: 10,
|
|
4161
|
+
min: 1,
|
|
4162
|
+
max: 100
|
|
4163
|
+
}
|
|
4164
|
+
]);
|
|
4165
|
+
return {
|
|
4166
|
+
type: "claude-code",
|
|
4167
|
+
enable: true,
|
|
4168
|
+
emoji: answers.emoji || "\u{1F980}",
|
|
4169
|
+
description: "Coding assistant powered by Claude Code \u2014 excels at writing, debugging, and refactoring code",
|
|
4170
|
+
capabilities: ["code", "debug", "refactor", "git"],
|
|
4171
|
+
maxTurns: answers.maxTurns ?? 10,
|
|
4172
|
+
cliPath: answers.cliPath || void 0
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
async function buildConfigInteractive() {
|
|
4176
|
+
console.log("\n Welcome to paly with Wayang! Let's configure your setup.\n");
|
|
4177
|
+
const answers = await prompts([
|
|
4178
|
+
{
|
|
4179
|
+
type: "text",
|
|
4180
|
+
name: "endpoint",
|
|
4181
|
+
message: "API endpoint (OpenAI-compatible base URL, e.g. https://api.openai.com/v1)"
|
|
4182
|
+
},
|
|
4183
|
+
{
|
|
4184
|
+
type: "text",
|
|
4185
|
+
name: "apiKey",
|
|
4186
|
+
message: "API key"
|
|
4187
|
+
},
|
|
4188
|
+
{
|
|
4189
|
+
type: "text",
|
|
4190
|
+
name: "modelName",
|
|
4191
|
+
message: "Model name"
|
|
4192
|
+
},
|
|
4193
|
+
{
|
|
4194
|
+
type: "number",
|
|
4195
|
+
name: "maxConcurrency",
|
|
4196
|
+
message: "Max concurrent workers",
|
|
4197
|
+
initial: 3,
|
|
4198
|
+
min: 1,
|
|
4199
|
+
max: 10
|
|
4200
|
+
}
|
|
4201
|
+
]);
|
|
4202
|
+
if (!answers.apiKey) {
|
|
4203
|
+
console.error("Error: API key is required.");
|
|
4204
|
+
process.exit(1);
|
|
4205
|
+
}
|
|
4206
|
+
const config = {
|
|
4207
|
+
providers: {
|
|
4208
|
+
default: {
|
|
4209
|
+
endpoint: answers.endpoint,
|
|
4210
|
+
apiKey: answers.apiKey,
|
|
4211
|
+
modelName: answers.modelName
|
|
4212
|
+
}
|
|
4213
|
+
},
|
|
4214
|
+
controller: { provider: "default" },
|
|
4215
|
+
worker: { provider: "default", maxConcurrency: answers.maxConcurrency ?? 3 }
|
|
4216
|
+
};
|
|
4217
|
+
const claudePath = detectClaudeCode();
|
|
4218
|
+
const workerConfig = await promptClaudeCodeWorker(!!claudePath);
|
|
4219
|
+
if (workerConfig) {
|
|
4220
|
+
config.workers = { "claude-code": workerConfig };
|
|
4221
|
+
}
|
|
4222
|
+
return config;
|
|
4223
|
+
}
|
|
4224
|
+
async function loadConfig(configPath2) {
|
|
4225
|
+
let config;
|
|
4226
|
+
if (fs2.existsSync(configPath2)) {
|
|
4227
|
+
config = JSON.parse(fs2.readFileSync(configPath2, "utf-8"));
|
|
4228
|
+
} else {
|
|
4229
|
+
const envKey = process.env.WAYANG_LLM_API_KEY || "";
|
|
4230
|
+
const envEndpoint = process.env.WAYANG_ENDPOINT || "";
|
|
4231
|
+
const envModel = process.env.WAYANG_MODEL || "";
|
|
4232
|
+
if (envKey && envEndpoint && envModel) {
|
|
4233
|
+
config = {
|
|
4234
|
+
providers: { default: { endpoint: envEndpoint, apiKey: envKey, modelName: envModel } },
|
|
4235
|
+
controller: { provider: "default" },
|
|
4236
|
+
worker: { provider: "default", maxConcurrency: 3 }
|
|
4237
|
+
};
|
|
4238
|
+
} else {
|
|
4239
|
+
config = await buildConfigInteractive();
|
|
4240
|
+
}
|
|
4241
|
+
fs2.mkdirSync(path3.dirname(configPath2), { recursive: true });
|
|
4242
|
+
fs2.writeFileSync(configPath2, JSON.stringify(config, null, 2));
|
|
4243
|
+
}
|
|
4244
|
+
fillApiKeys(config);
|
|
4245
|
+
const err = validateConfig(config);
|
|
4246
|
+
if (err) {
|
|
4247
|
+
console.error(`Error: ${err}`);
|
|
4248
|
+
console.error("Run `wayang` without arguments to configure.");
|
|
4249
|
+
process.exit(1);
|
|
4250
|
+
}
|
|
4251
|
+
return config;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
// src/session-select.ts
|
|
4255
|
+
import prompts2 from "prompts";
|
|
4256
|
+
function formatSessionChoice(meta, showWorkspace) {
|
|
4257
|
+
const date = new Date(meta.createdAt).toLocaleString();
|
|
4258
|
+
const label = meta.firstInput ? `"${meta.firstInput}"` : "(no input yet)";
|
|
4259
|
+
const ws = showWorkspace ? ` [${meta.workspace}]` : "";
|
|
4260
|
+
return `${meta.sessionId} ${date} ${label}${ws}`;
|
|
4261
|
+
}
|
|
4262
|
+
async function selectSessionInteractive(homeDir2, workspaceDir2, showAll) {
|
|
4263
|
+
let sessions = listSessions(homeDir2);
|
|
4264
|
+
if (!showAll) {
|
|
4265
|
+
sessions = sessions.filter((m) => m.workspace === workspaceDir2);
|
|
4266
|
+
}
|
|
4267
|
+
if (sessions.length === 0) {
|
|
4268
|
+
const scope = showAll ? "any workspace" : `workspace ${workspaceDir2}`;
|
|
4269
|
+
console.error(`Error: No sessions found for ${scope}. Start a new session first.`);
|
|
4270
|
+
process.exit(1);
|
|
4271
|
+
}
|
|
4272
|
+
const { selected } = await prompts2({
|
|
4273
|
+
type: "select",
|
|
4274
|
+
name: "selected",
|
|
4275
|
+
message: "Select a session to resume",
|
|
4276
|
+
choices: sessions.map((m) => ({
|
|
4277
|
+
title: formatSessionChoice(m, showAll),
|
|
4278
|
+
value: m.sessionId
|
|
4279
|
+
}))
|
|
4280
|
+
});
|
|
4281
|
+
if (!selected) {
|
|
4282
|
+
process.exit(0);
|
|
4283
|
+
}
|
|
4284
|
+
const result = getSession(homeDir2, selected);
|
|
4285
|
+
if (!result) {
|
|
4286
|
+
console.error(`Error: Session "${selected}" not found.`);
|
|
4287
|
+
process.exit(1);
|
|
4288
|
+
}
|
|
4289
|
+
if (result.meta.workspace !== workspaceDir2) {
|
|
4290
|
+
const { confirm } = await prompts2({
|
|
4291
|
+
type: "confirm",
|
|
4292
|
+
name: "confirm",
|
|
4293
|
+
message: `Session workspace is ${result.meta.workspace}, current dir is ${workspaceDir2}. Switch?`,
|
|
4294
|
+
initial: true
|
|
4295
|
+
});
|
|
4296
|
+
if (!confirm) {
|
|
4297
|
+
process.exit(0);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
return result;
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
// src/bootstrap.ts
|
|
4304
|
+
function die(msg) {
|
|
4305
|
+
console.error(`Error: ${msg}`);
|
|
4306
|
+
process.exit(1);
|
|
4307
|
+
}
|
|
4308
|
+
async function bootstrap(options) {
|
|
4309
|
+
const config = await loadConfig(options.configPath);
|
|
4310
|
+
const supervisorOpts = {
|
|
4311
|
+
config,
|
|
4312
|
+
workspaceDir: options.workspaceDir,
|
|
4313
|
+
logLevel: options.logLevel,
|
|
4314
|
+
homeDir: options.homeDir
|
|
4315
|
+
};
|
|
4316
|
+
if (options.resume === void 0) {
|
|
4317
|
+
} else if (options.resume === "") {
|
|
4318
|
+
const result = await selectSessionInteractive(options.homeDir, options.workspaceDir, options.showAll ?? false);
|
|
4319
|
+
supervisorOpts.workspaceDir = result.meta.workspace;
|
|
4320
|
+
supervisorOpts.resume = { sessionId: result.sessionId, sessionDir: result.sessionDir };
|
|
4321
|
+
console.log(` Resuming session ${result.sessionId}`);
|
|
4322
|
+
} else {
|
|
4323
|
+
const existing = getSession(options.homeDir, options.resume);
|
|
4324
|
+
if (!existing) {
|
|
4325
|
+
die(`Session "${options.resume}" not found.`);
|
|
4326
|
+
}
|
|
4327
|
+
supervisorOpts.workspaceDir = existing.meta.workspace;
|
|
4328
|
+
supervisorOpts.resume = { sessionId: existing.sessionId, sessionDir: existing.sessionDir };
|
|
4329
|
+
console.log(` Resuming session ${existing.sessionId}`);
|
|
4330
|
+
}
|
|
4331
|
+
const supervisor = new Supervisor(supervisorOpts);
|
|
4332
|
+
await supervisor.restore();
|
|
4333
|
+
await supervisor.start();
|
|
4334
|
+
mainControllerLoop(supervisor).catch((err) => {
|
|
4335
|
+
supervisor.ctx.logger.error({ error: err.message }, "Controller loop crashed");
|
|
4336
|
+
});
|
|
4337
|
+
process.on("SIGTERM", () => {
|
|
4338
|
+
supervisor.shutdown().then(() => process.exit(0));
|
|
4339
|
+
});
|
|
4340
|
+
await renderInkUI(supervisor);
|
|
4341
|
+
await supervisor.shutdown();
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
// src/cli.ts
|
|
4345
|
+
var cli = meow(`
|
|
4346
|
+
Usage
|
|
4347
|
+
$ wayang New session
|
|
4348
|
+
$ wayang --resume Interactive session select
|
|
4349
|
+
$ wayang --resume <id> Resume specific session
|
|
4350
|
+
$ wayang --resume --all Show all workspaces
|
|
4351
|
+
|
|
4352
|
+
Options
|
|
4353
|
+
--home-dir <path> Root directory for sessions & config (default: $HOME)
|
|
4354
|
+
--workspace-dir, -w Working directory for tools (default: pwd)
|
|
4355
|
+
--config, -c Config file path
|
|
4356
|
+
--resume, -r Resume a previous session
|
|
4357
|
+
--all List sessions across all workspaces (with --resume)
|
|
4358
|
+
--verbose, -v Enable debug logging
|
|
4359
|
+
`, {
|
|
4360
|
+
importMeta: import.meta,
|
|
4361
|
+
flags: {
|
|
4362
|
+
homeDir: {
|
|
4363
|
+
type: "string",
|
|
4364
|
+
default: process.env.HOME || "~"
|
|
4365
|
+
},
|
|
4366
|
+
workspaceDir: {
|
|
4367
|
+
type: "string",
|
|
4368
|
+
shortFlag: "w",
|
|
4369
|
+
default: process.cwd()
|
|
4370
|
+
},
|
|
4371
|
+
config: {
|
|
4372
|
+
type: "string",
|
|
4373
|
+
shortFlag: "c"
|
|
4374
|
+
},
|
|
4375
|
+
resume: {
|
|
4376
|
+
type: "boolean",
|
|
4377
|
+
shortFlag: "r",
|
|
4378
|
+
default: false
|
|
4379
|
+
},
|
|
4380
|
+
all: {
|
|
4381
|
+
type: "boolean",
|
|
4382
|
+
default: false
|
|
4383
|
+
},
|
|
4384
|
+
verbose: {
|
|
4385
|
+
type: "boolean",
|
|
4386
|
+
shortFlag: "v",
|
|
4387
|
+
default: false
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
});
|
|
4391
|
+
var homeDir = path4.resolve(cli.flags.homeDir);
|
|
4392
|
+
var workspaceDir = path4.resolve(cli.flags.workspaceDir);
|
|
4393
|
+
var configPath = cli.flags.config ? path4.resolve(cli.flags.config) : path4.join(homeDir, ".wayang.config.json");
|
|
4394
|
+
var resumeFlag = cli.flags.resume;
|
|
4395
|
+
var resumeId = resumeFlag ? cli.input.at(0) || void 0 : void 0;
|
|
4396
|
+
bootstrap({
|
|
4397
|
+
configPath,
|
|
4398
|
+
homeDir: path4.join(homeDir, ".wayang"),
|
|
4399
|
+
workspaceDir,
|
|
4400
|
+
logLevel: cli.flags.verbose ? "debug" : "info",
|
|
4401
|
+
resume: resumeFlag ? resumeId ?? "" : void 0,
|
|
4402
|
+
showAll: cli.flags.all
|
|
4403
|
+
}).catch((err) => {
|
|
4404
|
+
console.error("Fatal:", err);
|
|
4405
|
+
process.exit(1);
|
|
4406
|
+
});
|
|
4407
|
+
//# sourceMappingURL=cli.js.map
|