@bytespell/amux 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +10 -2171
- package/dist/bin/cli.js.map +1 -1
- package/dist/chunk-226DBKL3.js +26 -0
- package/dist/chunk-226DBKL3.js.map +1 -0
- package/dist/chunk-5IPYOXBE.js +32 -0
- package/dist/chunk-5IPYOXBE.js.map +1 -0
- package/dist/chunk-KUFDA4M3.js +333 -0
- package/dist/chunk-KUFDA4M3.js.map +1 -0
- package/dist/chunk-L4DBPVMA.js +122 -0
- package/dist/chunk-L4DBPVMA.js.map +1 -0
- package/dist/chunk-OQ5K5ON2.js +319 -0
- package/dist/chunk-OQ5K5ON2.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-RSLSN7F2.js +1591 -0
- package/dist/chunk-RSLSN7F2.js.map +1 -0
- package/dist/chunk-SX7NC3ZM.js +65 -0
- package/dist/chunk-SX7NC3ZM.js.map +1 -0
- package/dist/src/agents/eventStore.js +12 -344
- package/dist/src/agents/eventStore.js.map +1 -1
- package/dist/src/agents/manager.js +7 -1740
- package/dist/src/agents/manager.js.map +1 -1
- package/dist/src/db/index.js +5 -286
- package/dist/src/db/index.js.map +1 -1
- package/dist/src/lib/logger.js +18 -0
- package/dist/src/lib/logger.js.map +1 -0
- package/dist/src/server.js +10 -2168
- package/dist/src/server.js.map +1 -1
- package/dist/src/trpc/files.js +6 -396
- package/dist/src/trpc/files.js.map +1 -1
- package/dist/src/types.js +6 -19
- package/dist/src/types.js.map +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
import {
|
|
2
|
+
endTurn,
|
|
3
|
+
getEventsForSession,
|
|
4
|
+
startTurn,
|
|
5
|
+
storeEvent
|
|
6
|
+
} from "./chunk-SX7NC3ZM.js";
|
|
7
|
+
import {
|
|
8
|
+
isSessionUpdate
|
|
9
|
+
} from "./chunk-226DBKL3.js";
|
|
10
|
+
import {
|
|
11
|
+
debug,
|
|
12
|
+
warn
|
|
13
|
+
} from "./chunk-5IPYOXBE.js";
|
|
14
|
+
import {
|
|
15
|
+
agentConfigs,
|
|
16
|
+
db,
|
|
17
|
+
sessions
|
|
18
|
+
} from "./chunk-OQ5K5ON2.js";
|
|
19
|
+
|
|
20
|
+
// src/agents/manager.ts
|
|
21
|
+
import { EventEmitter } from "events";
|
|
22
|
+
import { eq } from "drizzle-orm";
|
|
23
|
+
|
|
24
|
+
// src/agents/backends/acp.ts
|
|
25
|
+
import { randomUUID } from "crypto";
|
|
26
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
27
|
+
import { Readable, Writable } from "stream";
|
|
28
|
+
import * as fs from "fs/promises";
|
|
29
|
+
import {
|
|
30
|
+
ClientSideConnection,
|
|
31
|
+
ndJsonStream
|
|
32
|
+
} from "@agentclientprotocol/sdk";
|
|
33
|
+
|
|
34
|
+
// src/agents/process.ts
|
|
35
|
+
import { execa } from "execa";
|
|
36
|
+
import treeKill from "tree-kill";
|
|
37
|
+
function spawn(options) {
|
|
38
|
+
const { command, args = [], cwd, env, timeoutMs } = options;
|
|
39
|
+
const subprocess = execa(command, args, {
|
|
40
|
+
cwd,
|
|
41
|
+
env: { ...process.env, ...env },
|
|
42
|
+
stdin: "pipe",
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "pipe",
|
|
45
|
+
timeout: timeoutMs,
|
|
46
|
+
cleanup: true,
|
|
47
|
+
// Kill on parent exit
|
|
48
|
+
windowsHide: true
|
|
49
|
+
// Hide console window on Windows
|
|
50
|
+
});
|
|
51
|
+
const pid = subprocess.pid;
|
|
52
|
+
if (!pid) {
|
|
53
|
+
throw new Error(`Failed to spawn process: ${command}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
pid,
|
|
57
|
+
stdin: subprocess.stdin,
|
|
58
|
+
stdout: subprocess.stdout,
|
|
59
|
+
stderr: subprocess.stderr,
|
|
60
|
+
kill: (signal = "SIGTERM") => killTree(pid, signal),
|
|
61
|
+
wait: async () => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await subprocess;
|
|
64
|
+
return { exitCode: result.exitCode };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return { exitCode: err.exitCode ?? 1 };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function killTree(pid, signal) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
treeKill(pid, signal, (err) => {
|
|
74
|
+
if (err && !err.message.includes("No such process")) {
|
|
75
|
+
console.error(`[process] kill error pid=${pid}:`, err.message);
|
|
76
|
+
}
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/agents/backends/acp.ts
|
|
83
|
+
var INIT_TIMEOUT_MS = 3e4;
|
|
84
|
+
function normalizeSessionUpdate(update) {
|
|
85
|
+
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
|
86
|
+
return update;
|
|
87
|
+
}
|
|
88
|
+
const content = update.content;
|
|
89
|
+
if (!content || !Array.isArray(content)) {
|
|
90
|
+
return update;
|
|
91
|
+
}
|
|
92
|
+
const normalizedContent = content.map((item) => {
|
|
93
|
+
if (item.type !== "diff") return item;
|
|
94
|
+
if (typeof item.content === "string") return item;
|
|
95
|
+
const newText = item.newText;
|
|
96
|
+
const oldText = item.oldText;
|
|
97
|
+
const path2 = item.path;
|
|
98
|
+
if (newText === void 0) return item;
|
|
99
|
+
const filePath = path2 ?? "file";
|
|
100
|
+
const oldLines = oldText ? oldText.split("\n") : [];
|
|
101
|
+
const newLines = newText.split("\n");
|
|
102
|
+
let unifiedDiff = `Index: ${filePath}
|
|
103
|
+
`;
|
|
104
|
+
unifiedDiff += "===================================================================\n";
|
|
105
|
+
unifiedDiff += `--- ${filePath}
|
|
106
|
+
`;
|
|
107
|
+
unifiedDiff += `+++ ${filePath}
|
|
108
|
+
`;
|
|
109
|
+
unifiedDiff += `@@ -${oldLines.length > 0 ? 1 : 0},${oldLines.length} +1,${newLines.length} @@
|
|
110
|
+
`;
|
|
111
|
+
for (const line of oldLines) {
|
|
112
|
+
unifiedDiff += `-${line}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
for (const line of newLines) {
|
|
116
|
+
unifiedDiff += `+${line}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
type: "diff",
|
|
121
|
+
content: unifiedDiff
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
...update,
|
|
126
|
+
content: normalizedContent
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function withTimeout(promise, ms, operation) {
|
|
130
|
+
return Promise.race([
|
|
131
|
+
promise,
|
|
132
|
+
new Promise(
|
|
133
|
+
(_, reject) => setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms)
|
|
134
|
+
)
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
var AcpBackend = class {
|
|
138
|
+
type = "acp";
|
|
139
|
+
instances = /* @__PURE__ */ new Map();
|
|
140
|
+
onSessionIdChanged;
|
|
141
|
+
matches(config2) {
|
|
142
|
+
return config2.command !== "__mock__" && config2.command !== "__stress__" && config2.command !== "__shell__";
|
|
143
|
+
}
|
|
144
|
+
async start(sessionId, config2, cwd, existingAcpSessionId, emit) {
|
|
145
|
+
if (this.instances.has(sessionId)) {
|
|
146
|
+
await this.stop(sessionId);
|
|
147
|
+
}
|
|
148
|
+
const args = config2.args ?? [];
|
|
149
|
+
const env = config2.env ?? {};
|
|
150
|
+
debug("acp", ` Spawning: ${config2.command} ${args.join(" ")} in ${cwd}`);
|
|
151
|
+
const proc = spawn({
|
|
152
|
+
command: config2.command,
|
|
153
|
+
args,
|
|
154
|
+
cwd,
|
|
155
|
+
env
|
|
156
|
+
});
|
|
157
|
+
const instance = {
|
|
158
|
+
process: proc,
|
|
159
|
+
connection: null,
|
|
160
|
+
sessionId: "",
|
|
161
|
+
pendingPermission: null,
|
|
162
|
+
permissionCallbacks: /* @__PURE__ */ new Map(),
|
|
163
|
+
emit,
|
|
164
|
+
terminals: /* @__PURE__ */ new Map()
|
|
165
|
+
};
|
|
166
|
+
proc.wait().then(({ exitCode }) => {
|
|
167
|
+
if (this.instances.has(sessionId)) {
|
|
168
|
+
emit({ amuxEvent: "error", message: `Agent process exited with code ${exitCode}` });
|
|
169
|
+
this.instances.delete(sessionId);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
const input = Writable.toWeb(proc.stdin);
|
|
174
|
+
const output = Readable.toWeb(proc.stdout);
|
|
175
|
+
const stream = ndJsonStream(input, output);
|
|
176
|
+
const client = this.createClient(sessionId, instance);
|
|
177
|
+
instance.connection = new ClientSideConnection(() => client, stream);
|
|
178
|
+
const initResult = await withTimeout(
|
|
179
|
+
instance.connection.initialize({
|
|
180
|
+
protocolVersion: 1,
|
|
181
|
+
clientCapabilities: {
|
|
182
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
183
|
+
terminal: true
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
INIT_TIMEOUT_MS,
|
|
187
|
+
"Agent initialization"
|
|
188
|
+
);
|
|
189
|
+
debug("acp", ` Initialized agent: ${initResult.agentInfo?.name} v${initResult.agentInfo?.version}`);
|
|
190
|
+
const canResume = initResult.agentCapabilities?.sessionCapabilities?.resume !== void 0;
|
|
191
|
+
let acpSessionId;
|
|
192
|
+
let sessionResult;
|
|
193
|
+
if (existingAcpSessionId && canResume) {
|
|
194
|
+
let resumeSucceeded = false;
|
|
195
|
+
try {
|
|
196
|
+
debug("acp", ` Resuming ACP session ${existingAcpSessionId}...`);
|
|
197
|
+
sessionResult = await withTimeout(
|
|
198
|
+
instance.connection.unstable_resumeSession({
|
|
199
|
+
sessionId: existingAcpSessionId,
|
|
200
|
+
cwd,
|
|
201
|
+
mcpServers: []
|
|
202
|
+
}),
|
|
203
|
+
INIT_TIMEOUT_MS,
|
|
204
|
+
"Session resume"
|
|
205
|
+
);
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
207
|
+
acpSessionId = existingAcpSessionId;
|
|
208
|
+
debug("acp", ` ACP session resumed successfully`);
|
|
209
|
+
resumeSucceeded = true;
|
|
210
|
+
} catch (resumeErr) {
|
|
211
|
+
debug("acp", ` Resume failed, creating new session:`, resumeErr);
|
|
212
|
+
}
|
|
213
|
+
if (!resumeSucceeded) {
|
|
214
|
+
sessionResult = await withTimeout(
|
|
215
|
+
instance.connection.newSession({ cwd, mcpServers: [] }),
|
|
216
|
+
INIT_TIMEOUT_MS,
|
|
217
|
+
"New session creation"
|
|
218
|
+
);
|
|
219
|
+
acpSessionId = sessionResult.sessionId;
|
|
220
|
+
debug("acp", ` New ACP session created: ${acpSessionId}`);
|
|
221
|
+
this.onSessionIdChanged?.(sessionId, acpSessionId);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
debug("acp", ` Creating new ACP session in ${cwd}...`);
|
|
225
|
+
sessionResult = await withTimeout(
|
|
226
|
+
instance.connection.newSession({ cwd, mcpServers: [] }),
|
|
227
|
+
INIT_TIMEOUT_MS,
|
|
228
|
+
"New session creation"
|
|
229
|
+
);
|
|
230
|
+
acpSessionId = sessionResult.sessionId;
|
|
231
|
+
debug("acp", ` ACP session created: ${acpSessionId}`);
|
|
232
|
+
this.onSessionIdChanged?.(sessionId, acpSessionId);
|
|
233
|
+
}
|
|
234
|
+
instance.sessionId = acpSessionId;
|
|
235
|
+
this.instances.set(sessionId, instance);
|
|
236
|
+
const models = sessionResult?.models?.availableModels;
|
|
237
|
+
const modes = sessionResult?.modes?.availableModes;
|
|
238
|
+
if (sessionResult?.modes) {
|
|
239
|
+
emit({
|
|
240
|
+
sessionUpdate: "current_mode_update",
|
|
241
|
+
currentModeId: sessionResult.modes.currentModeId
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
sessionId: acpSessionId,
|
|
246
|
+
models,
|
|
247
|
+
modes
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`[acp] Error starting agent for session ${sessionId}:`, err);
|
|
251
|
+
await proc.kill();
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
createClient(_sessionId, instance) {
|
|
256
|
+
return {
|
|
257
|
+
async requestPermission(params) {
|
|
258
|
+
const requestId = randomUUID();
|
|
259
|
+
const permission = {
|
|
260
|
+
requestId,
|
|
261
|
+
toolCallId: params.toolCall.toolCallId,
|
|
262
|
+
title: params.toolCall.title ?? "Permission Required",
|
|
263
|
+
options: params.options.map((o) => ({
|
|
264
|
+
optionId: o.optionId,
|
|
265
|
+
name: o.name,
|
|
266
|
+
kind: o.kind
|
|
267
|
+
}))
|
|
268
|
+
};
|
|
269
|
+
instance.pendingPermission = permission;
|
|
270
|
+
instance.emit({ amuxEvent: "permission_request", permission });
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
instance.permissionCallbacks.set(requestId, {
|
|
273
|
+
resolve: (optionId) => {
|
|
274
|
+
instance.pendingPermission = null;
|
|
275
|
+
instance.emit({ amuxEvent: "permission_cleared" });
|
|
276
|
+
resolve({ outcome: { outcome: "selected", optionId } });
|
|
277
|
+
},
|
|
278
|
+
reject
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
async sessionUpdate(params) {
|
|
283
|
+
debug("acp", ` sessionUpdate received:`, JSON.stringify(params));
|
|
284
|
+
const normalized = normalizeSessionUpdate(params.update);
|
|
285
|
+
instance.emit(normalized);
|
|
286
|
+
},
|
|
287
|
+
async readTextFile(params) {
|
|
288
|
+
const content = await fs.readFile(params.path, "utf-8");
|
|
289
|
+
return { content };
|
|
290
|
+
},
|
|
291
|
+
async writeTextFile(params) {
|
|
292
|
+
await fs.writeFile(params.path, params.content);
|
|
293
|
+
return {};
|
|
294
|
+
},
|
|
295
|
+
async createTerminal(params) {
|
|
296
|
+
debug("acp", ` createTerminal request:`, JSON.stringify(params));
|
|
297
|
+
const terminalId = randomUUID();
|
|
298
|
+
const outputByteLimit = params.outputByteLimit ?? 1024 * 1024;
|
|
299
|
+
const termProc = nodeSpawn(params.command, params.args ?? [], {
|
|
300
|
+
cwd: params.cwd ?? void 0,
|
|
301
|
+
env: params.env ? { ...process.env, ...Object.fromEntries(params.env.map((e) => [e.name, e.value])) } : process.env,
|
|
302
|
+
shell: true,
|
|
303
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
304
|
+
});
|
|
305
|
+
const terminal = {
|
|
306
|
+
process: termProc,
|
|
307
|
+
output: "",
|
|
308
|
+
exitCode: null,
|
|
309
|
+
signal: null,
|
|
310
|
+
truncated: false,
|
|
311
|
+
outputByteLimit
|
|
312
|
+
};
|
|
313
|
+
const appendOutput = (data) => {
|
|
314
|
+
terminal.output += data.toString();
|
|
315
|
+
if (terminal.output.length > terminal.outputByteLimit) {
|
|
316
|
+
terminal.output = terminal.output.slice(-terminal.outputByteLimit);
|
|
317
|
+
terminal.truncated = true;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
termProc.stdout?.on("data", appendOutput);
|
|
321
|
+
termProc.stderr?.on("data", appendOutput);
|
|
322
|
+
termProc.on("exit", (code, signal) => {
|
|
323
|
+
debug("acp", ` Terminal ${terminalId} exited with code ${code}, signal ${signal}`);
|
|
324
|
+
terminal.exitCode = code ?? null;
|
|
325
|
+
terminal.signal = signal ?? null;
|
|
326
|
+
});
|
|
327
|
+
termProc.on("error", (err) => {
|
|
328
|
+
console.error(`[acp] Terminal ${terminalId} error:`, err.message);
|
|
329
|
+
terminal.output += `
|
|
330
|
+
Error: ${err.message}`;
|
|
331
|
+
terminal.exitCode = -1;
|
|
332
|
+
});
|
|
333
|
+
instance.terminals.set(terminalId, terminal);
|
|
334
|
+
debug("acp", ` Created terminal ${terminalId} for command: ${params.command}`);
|
|
335
|
+
return { terminalId };
|
|
336
|
+
},
|
|
337
|
+
async terminalOutput(params) {
|
|
338
|
+
debug("acp", ` terminalOutput request for terminal ${params.terminalId}`);
|
|
339
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
340
|
+
if (!terminal) {
|
|
341
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
output: terminal.output,
|
|
345
|
+
truncated: terminal.truncated,
|
|
346
|
+
exitStatus: terminal.exitCode !== null || terminal.signal !== null ? {
|
|
347
|
+
exitCode: terminal.exitCode,
|
|
348
|
+
signal: terminal.signal
|
|
349
|
+
} : void 0
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
async waitForTerminalExit(params) {
|
|
353
|
+
debug("acp", ` waitForTerminalExit request for terminal ${params.terminalId}`);
|
|
354
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
355
|
+
if (!terminal) {
|
|
356
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
357
|
+
}
|
|
358
|
+
if (terminal.exitCode !== null || terminal.signal !== null) {
|
|
359
|
+
return {
|
|
360
|
+
exitCode: terminal.exitCode,
|
|
361
|
+
signal: terminal.signal
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return new Promise((resolve) => {
|
|
365
|
+
terminal.process.on("exit", (code, signal) => {
|
|
366
|
+
resolve({
|
|
367
|
+
exitCode: code ?? null,
|
|
368
|
+
signal: signal ?? null
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
// Note: killTerminalCommand not in SDK Client interface yet, but we implement handlers
|
|
374
|
+
// for completeness when the SDK adds support
|
|
375
|
+
async killTerminal(params) {
|
|
376
|
+
debug("acp", ` killTerminal request for terminal ${params.terminalId}`);
|
|
377
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
378
|
+
if (!terminal) {
|
|
379
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
380
|
+
}
|
|
381
|
+
terminal.process.kill("SIGTERM");
|
|
382
|
+
return {};
|
|
383
|
+
},
|
|
384
|
+
async releaseTerminal(params) {
|
|
385
|
+
debug("acp", ` releaseTerminal request for terminal ${params.terminalId}`);
|
|
386
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
387
|
+
if (terminal) {
|
|
388
|
+
if (terminal.exitCode === null) {
|
|
389
|
+
terminal.process.kill("SIGKILL");
|
|
390
|
+
}
|
|
391
|
+
instance.terminals.delete(params.terminalId);
|
|
392
|
+
}
|
|
393
|
+
return {};
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async prompt(sessionId, content, _emit) {
|
|
398
|
+
const instance = this.instances.get(sessionId);
|
|
399
|
+
if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
|
|
400
|
+
debug("acp", ` Sending prompt to session ${instance.sessionId} with ${content.length} content block(s)...`);
|
|
401
|
+
const result = await instance.connection.prompt({
|
|
402
|
+
sessionId: instance.sessionId,
|
|
403
|
+
prompt: content
|
|
404
|
+
});
|
|
405
|
+
debug("acp", ` Prompt complete, stopReason: ${result.stopReason}`);
|
|
406
|
+
}
|
|
407
|
+
async stop(sessionId) {
|
|
408
|
+
const instance = this.instances.get(sessionId);
|
|
409
|
+
if (!instance) return;
|
|
410
|
+
for (const [, callback] of instance.permissionCallbacks) {
|
|
411
|
+
callback.reject(new Error("Agent stopped"));
|
|
412
|
+
}
|
|
413
|
+
instance.permissionCallbacks.clear();
|
|
414
|
+
instance.pendingPermission = null;
|
|
415
|
+
this.instances.delete(sessionId);
|
|
416
|
+
await instance.process.kill();
|
|
417
|
+
}
|
|
418
|
+
async stopAll() {
|
|
419
|
+
const sessionIds = [...this.instances.keys()];
|
|
420
|
+
await Promise.all(sessionIds.map((id) => this.stop(id)));
|
|
421
|
+
}
|
|
422
|
+
isRunning(sessionId) {
|
|
423
|
+
return this.instances.has(sessionId);
|
|
424
|
+
}
|
|
425
|
+
respondToPermission(sessionId, requestId, optionId) {
|
|
426
|
+
const instance = this.instances.get(sessionId);
|
|
427
|
+
const callback = instance?.permissionCallbacks.get(requestId);
|
|
428
|
+
if (!callback) {
|
|
429
|
+
throw new Error(`No pending permission request ${requestId}`);
|
|
430
|
+
}
|
|
431
|
+
callback.resolve(optionId);
|
|
432
|
+
instance.permissionCallbacks.delete(requestId);
|
|
433
|
+
}
|
|
434
|
+
getPendingPermission(sessionId) {
|
|
435
|
+
const instance = this.instances.get(sessionId);
|
|
436
|
+
return instance?.pendingPermission ?? null;
|
|
437
|
+
}
|
|
438
|
+
async cancel(sessionId) {
|
|
439
|
+
const instance = this.instances.get(sessionId);
|
|
440
|
+
if (!instance) return;
|
|
441
|
+
await instance.connection.cancel({ sessionId: instance.sessionId });
|
|
442
|
+
}
|
|
443
|
+
async setMode(sessionId, modeId) {
|
|
444
|
+
const instance = this.instances.get(sessionId);
|
|
445
|
+
if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
|
|
446
|
+
await instance.connection.setSessionMode({
|
|
447
|
+
sessionId: instance.sessionId,
|
|
448
|
+
modeId
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
async setModel(sessionId, modelId) {
|
|
452
|
+
const instance = this.instances.get(sessionId);
|
|
453
|
+
if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
|
|
454
|
+
await instance.connection.unstable_setSessionModel({
|
|
455
|
+
sessionId: instance.sessionId,
|
|
456
|
+
modelId
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/agents/backends/mock.ts
|
|
462
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
463
|
+
function delay(ms) {
|
|
464
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
465
|
+
}
|
|
466
|
+
var MockBackend = class {
|
|
467
|
+
type = "mock";
|
|
468
|
+
running = /* @__PURE__ */ new Set();
|
|
469
|
+
matches(config2) {
|
|
470
|
+
return config2.command === "__mock__";
|
|
471
|
+
}
|
|
472
|
+
async start(sessionId, _config, _cwd, _existingAcpSessionId, emit) {
|
|
473
|
+
debug("mock", ` Starting mock agent for session ${sessionId}`);
|
|
474
|
+
emit({
|
|
475
|
+
sessionUpdate: "current_mode_update",
|
|
476
|
+
currentModeId: "mock"
|
|
477
|
+
});
|
|
478
|
+
debug("mock", ` Mock agent ready for session ${sessionId}`);
|
|
479
|
+
this.running.add(sessionId);
|
|
480
|
+
return {
|
|
481
|
+
sessionId: randomUUID2(),
|
|
482
|
+
models: [{ modelId: "mock-model", name: "Mock Model" }],
|
|
483
|
+
modes: [{ id: "mock", name: "Mock Mode" }]
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
async prompt(sessionId, content, emit) {
|
|
487
|
+
const text = content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
488
|
+
debug("mock", ` Mock prompt for session ${sessionId}: "${text.slice(0, 50)}..."`);
|
|
489
|
+
const words = [
|
|
490
|
+
"This",
|
|
491
|
+
"is",
|
|
492
|
+
"a",
|
|
493
|
+
"mock",
|
|
494
|
+
"response",
|
|
495
|
+
"from",
|
|
496
|
+
"the",
|
|
497
|
+
"mock",
|
|
498
|
+
"agent.",
|
|
499
|
+
"It",
|
|
500
|
+
"simulates",
|
|
501
|
+
"streaming",
|
|
502
|
+
"text",
|
|
503
|
+
"chunks",
|
|
504
|
+
"for",
|
|
505
|
+
"performance",
|
|
506
|
+
"testing.",
|
|
507
|
+
"The",
|
|
508
|
+
"response",
|
|
509
|
+
"arrives",
|
|
510
|
+
"in",
|
|
511
|
+
"small",
|
|
512
|
+
"pieces",
|
|
513
|
+
"to",
|
|
514
|
+
"test",
|
|
515
|
+
"UI",
|
|
516
|
+
"rendering."
|
|
517
|
+
];
|
|
518
|
+
for (let i = 0; i < words.length; i += 3) {
|
|
519
|
+
const chunk = words.slice(i, i + 3).join(" ") + " ";
|
|
520
|
+
emit({
|
|
521
|
+
sessionUpdate: "agent_message_chunk",
|
|
522
|
+
content: { type: "text", text: chunk }
|
|
523
|
+
});
|
|
524
|
+
await delay(50);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async stop(sessionId) {
|
|
528
|
+
debug("mock", ` Stopping mock agent for session ${sessionId}`);
|
|
529
|
+
this.running.delete(sessionId);
|
|
530
|
+
}
|
|
531
|
+
async stopAll() {
|
|
532
|
+
const sessionIds = [...this.running];
|
|
533
|
+
await Promise.all(sessionIds.map((id) => this.stop(id)));
|
|
534
|
+
}
|
|
535
|
+
isRunning(sessionId) {
|
|
536
|
+
return this.running.has(sessionId);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/agents/backends/stress.ts
|
|
541
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
542
|
+
|
|
543
|
+
// src/stress/generators.ts
|
|
544
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
545
|
+
var DEFAULT_STRESS_CONFIG = {
|
|
546
|
+
eventsPerTurn: 50,
|
|
547
|
+
toolCallProbability: 0.4,
|
|
548
|
+
thoughtProbability: 0.6,
|
|
549
|
+
planProbability: 0.3,
|
|
550
|
+
chunkSize: 50,
|
|
551
|
+
delayMs: 0
|
|
552
|
+
};
|
|
553
|
+
var MARKDOWN_SAMPLES = [
|
|
554
|
+
// Sample 1: Code block with TypeScript
|
|
555
|
+
`Here's a React component that handles user authentication:
|
|
556
|
+
|
|
557
|
+
\`\`\`typescript
|
|
558
|
+
import { useState, useEffect } from 'react';
|
|
559
|
+
import { useAuth } from '@/hooks/useAuth';
|
|
560
|
+
|
|
561
|
+
interface AuthProviderProps {
|
|
562
|
+
children: React.ReactNode;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function AuthProvider({ children }: AuthProviderProps) {
|
|
566
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
567
|
+
const { user, login, logout } = useAuth();
|
|
568
|
+
|
|
569
|
+
useEffect(() => {
|
|
570
|
+
// Check for existing session
|
|
571
|
+
const checkSession = async () => {
|
|
572
|
+
try {
|
|
573
|
+
await validateToken();
|
|
574
|
+
} finally {
|
|
575
|
+
setIsLoading(false);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
checkSession();
|
|
579
|
+
}, []);
|
|
580
|
+
|
|
581
|
+
if (isLoading) {
|
|
582
|
+
return <LoadingSpinner />;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<AuthContext.Provider value={{ user, login, logout }}>
|
|
587
|
+
{children}
|
|
588
|
+
</AuthContext.Provider>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
This component wraps your app and provides authentication context.`,
|
|
594
|
+
// Sample 2: Multiple code blocks
|
|
595
|
+
`Let me show you how to set up the API client:
|
|
596
|
+
|
|
597
|
+
First, install the dependencies:
|
|
598
|
+
|
|
599
|
+
\`\`\`bash
|
|
600
|
+
npm install @tanstack/react-query axios
|
|
601
|
+
\`\`\`
|
|
602
|
+
|
|
603
|
+
Then create the client:
|
|
604
|
+
|
|
605
|
+
\`\`\`typescript
|
|
606
|
+
// src/lib/api.ts
|
|
607
|
+
import axios from 'axios';
|
|
608
|
+
|
|
609
|
+
export const apiClient = axios.create({
|
|
610
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
|
611
|
+
timeout: 10000,
|
|
612
|
+
headers: {
|
|
613
|
+
'Content-Type': 'application/json',
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Add auth interceptor
|
|
618
|
+
apiClient.interceptors.request.use((config) => {
|
|
619
|
+
const token = localStorage.getItem('auth_token');
|
|
620
|
+
if (token) {
|
|
621
|
+
config.headers.Authorization = \`Bearer \${token}\`;
|
|
622
|
+
}
|
|
623
|
+
return config;
|
|
624
|
+
});
|
|
625
|
+
\`\`\`
|
|
626
|
+
|
|
627
|
+
And set up React Query:
|
|
628
|
+
|
|
629
|
+
\`\`\`typescript
|
|
630
|
+
// src/providers/QueryProvider.tsx
|
|
631
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
632
|
+
|
|
633
|
+
const queryClient = new QueryClient({
|
|
634
|
+
defaultOptions: {
|
|
635
|
+
queries: {
|
|
636
|
+
staleTime: 5 * 60 * 1000,
|
|
637
|
+
retry: 1,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
|
643
|
+
return (
|
|
644
|
+
<QueryClientProvider client={queryClient}>
|
|
645
|
+
{children}
|
|
646
|
+
</QueryClientProvider>
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
\`\`\``,
|
|
650
|
+
// Sample 3: Lists and inline code
|
|
651
|
+
`Here's what I found in the codebase:
|
|
652
|
+
|
|
653
|
+
1. **Components** - Located in \`src/components/\`
|
|
654
|
+
- \`Button.tsx\` - Primary button component
|
|
655
|
+
- \`Input.tsx\` - Form input with validation
|
|
656
|
+
- \`Modal.tsx\` - Accessible modal dialog
|
|
657
|
+
|
|
658
|
+
2. **Hooks** - Custom React hooks in \`src/hooks/\`
|
|
659
|
+
- \`useDebounce\` - Debounces rapidly changing values
|
|
660
|
+
- \`useLocalStorage\` - Syncs state with localStorage
|
|
661
|
+
- \`useMediaQuery\` - Responsive breakpoint detection
|
|
662
|
+
|
|
663
|
+
3. **Utils** - Helper functions in \`src/lib/\`
|
|
664
|
+
- \`cn()\` - Class name merger (tailwind-merge + clsx)
|
|
665
|
+
- \`formatDate()\` - Date formatting with Intl API
|
|
666
|
+
- \`validateEmail()\` - Email validation regex
|
|
667
|
+
|
|
668
|
+
The main entry point is \`src/App.tsx\` which sets up routing and providers.`,
|
|
669
|
+
// Sample 4: Table and code
|
|
670
|
+
`Here's a comparison of the state management options:
|
|
671
|
+
|
|
672
|
+
| Feature | Zustand | Redux | Jotai |
|
|
673
|
+
|---------|---------|-------|-------|
|
|
674
|
+
| Bundle size | 1.5kb | 7kb | 2kb |
|
|
675
|
+
| Boilerplate | Low | High | Low |
|
|
676
|
+
| DevTools | Yes | Yes | Yes |
|
|
677
|
+
| TypeScript | Excellent | Good | Excellent |
|
|
678
|
+
|
|
679
|
+
Based on your requirements, I recommend Zustand:
|
|
680
|
+
|
|
681
|
+
\`\`\`typescript
|
|
682
|
+
import { create } from 'zustand';
|
|
683
|
+
|
|
684
|
+
interface AppState {
|
|
685
|
+
count: number;
|
|
686
|
+
increment: () => void;
|
|
687
|
+
decrement: () => void;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export const useAppStore = create<AppState>((set) => ({
|
|
691
|
+
count: 0,
|
|
692
|
+
increment: () => set((state) => ({ count: state.count + 1 })),
|
|
693
|
+
decrement: () => set((state) => ({ count: state.count - 1 })),
|
|
694
|
+
}));
|
|
695
|
+
\`\`\``,
|
|
696
|
+
// Sample 5: Error explanation with code
|
|
697
|
+
`I found the issue! The error occurs because of a race condition:
|
|
698
|
+
|
|
699
|
+
\`\`\`typescript
|
|
700
|
+
// \u274C Bug: Race condition
|
|
701
|
+
useEffect(() => {
|
|
702
|
+
fetchData().then(setData);
|
|
703
|
+
}, [id]);
|
|
704
|
+
|
|
705
|
+
// \u2705 Fix: Cleanup function prevents stale updates
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
let cancelled = false;
|
|
708
|
+
|
|
709
|
+
fetchData().then((result) => {
|
|
710
|
+
if (!cancelled) {
|
|
711
|
+
setData(result);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
return () => {
|
|
716
|
+
cancelled = true;
|
|
717
|
+
};
|
|
718
|
+
}, [id]);
|
|
719
|
+
\`\`\`
|
|
720
|
+
|
|
721
|
+
The fix adds a cleanup function that sets a \`cancelled\` flag when the effect re-runs or the component unmounts.`
|
|
722
|
+
];
|
|
723
|
+
var THOUGHT_PREFIXES = [
|
|
724
|
+
"Let me analyze",
|
|
725
|
+
"I should check",
|
|
726
|
+
"Looking at",
|
|
727
|
+
"Considering",
|
|
728
|
+
"The approach here is",
|
|
729
|
+
"Based on the code",
|
|
730
|
+
"I need to",
|
|
731
|
+
"First",
|
|
732
|
+
"Next",
|
|
733
|
+
"This suggests",
|
|
734
|
+
"The pattern shows",
|
|
735
|
+
"Examining"
|
|
736
|
+
];
|
|
737
|
+
var LOREM_WORDS = [
|
|
738
|
+
"Lorem",
|
|
739
|
+
"ipsum",
|
|
740
|
+
"dolor",
|
|
741
|
+
"sit",
|
|
742
|
+
"amet",
|
|
743
|
+
"consectetur",
|
|
744
|
+
"adipiscing",
|
|
745
|
+
"elit",
|
|
746
|
+
"sed",
|
|
747
|
+
"do",
|
|
748
|
+
"eiusmod",
|
|
749
|
+
"tempor",
|
|
750
|
+
"incididunt",
|
|
751
|
+
"ut",
|
|
752
|
+
"labore",
|
|
753
|
+
"et",
|
|
754
|
+
"dolore",
|
|
755
|
+
"magna",
|
|
756
|
+
"aliqua",
|
|
757
|
+
"Ut",
|
|
758
|
+
"enim",
|
|
759
|
+
"ad",
|
|
760
|
+
"minim",
|
|
761
|
+
"veniam",
|
|
762
|
+
"quis",
|
|
763
|
+
"nostrud"
|
|
764
|
+
];
|
|
765
|
+
var PLAN_TASKS = [
|
|
766
|
+
"Analyze the current implementation",
|
|
767
|
+
"Identify areas for improvement",
|
|
768
|
+
"Refactor the component structure",
|
|
769
|
+
"Update the test suite",
|
|
770
|
+
"Fix type errors",
|
|
771
|
+
"Add documentation",
|
|
772
|
+
"Optimize performance",
|
|
773
|
+
"Review dependencies"
|
|
774
|
+
];
|
|
775
|
+
var RICH_TOOL_CALLS = [
|
|
776
|
+
// Bash tool with command and output
|
|
777
|
+
{
|
|
778
|
+
toolName: "Bash",
|
|
779
|
+
kind: "execute",
|
|
780
|
+
title: "List files in src directory",
|
|
781
|
+
input: {
|
|
782
|
+
command: "ls -la src/",
|
|
783
|
+
description: "List files in src directory"
|
|
784
|
+
},
|
|
785
|
+
output: `total 48
|
|
786
|
+
drwxr-xr-x 8 user user 4096 Jan 10 14:30 .
|
|
787
|
+
drwxr-xr-x 12 user user 4096 Jan 10 14:25 ..
|
|
788
|
+
drwxr-xr-x 4 user user 4096 Jan 10 14:30 components
|
|
789
|
+
drwxr-xr-x 2 user user 4096 Jan 10 14:28 hooks
|
|
790
|
+
drwxr-xr-x 2 user user 4096 Jan 10 14:27 lib
|
|
791
|
+
-rw-r--r-- 1 user user 1245 Jan 10 14:30 App.tsx
|
|
792
|
+
-rw-r--r-- 1 user user 892 Jan 10 14:25 main.tsx
|
|
793
|
+
-rw-r--r-- 1 user user 2341 Jan 10 14:29 index.css`
|
|
794
|
+
},
|
|
795
|
+
// Read tool with file contents
|
|
796
|
+
{
|
|
797
|
+
toolName: "Read",
|
|
798
|
+
kind: "read",
|
|
799
|
+
title: "Read ~/repos/project/src/utils.ts",
|
|
800
|
+
input: {
|
|
801
|
+
file_path: "/home/user/repos/project/src/utils.ts"
|
|
802
|
+
},
|
|
803
|
+
output: `export function cn(...classes: string[]): string {
|
|
804
|
+
return classes.filter(Boolean).join(' ');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export function formatDate(date: Date): string {
|
|
808
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
809
|
+
year: 'numeric',
|
|
810
|
+
month: 'long',
|
|
811
|
+
day: 'numeric',
|
|
812
|
+
}).format(date);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function debounce<T extends (...args: unknown[]) => void>(
|
|
816
|
+
fn: T,
|
|
817
|
+
delay: number
|
|
818
|
+
): T {
|
|
819
|
+
let timeoutId: NodeJS.Timeout;
|
|
820
|
+
return ((...args) => {
|
|
821
|
+
clearTimeout(timeoutId);
|
|
822
|
+
timeoutId = setTimeout(() => fn(...args), delay);
|
|
823
|
+
}) as T;
|
|
824
|
+
}`
|
|
825
|
+
},
|
|
826
|
+
// Edit tool with diff
|
|
827
|
+
{
|
|
828
|
+
toolName: "Edit",
|
|
829
|
+
kind: "edit",
|
|
830
|
+
title: "Edit ~/repos/project/src/config.ts",
|
|
831
|
+
input: {
|
|
832
|
+
file_path: "/home/user/repos/project/src/config.ts",
|
|
833
|
+
old_string: "timeout: 5000",
|
|
834
|
+
new_string: "timeout: 10000"
|
|
835
|
+
},
|
|
836
|
+
diff: `--- a/src/config.ts
|
|
837
|
+
+++ b/src/config.ts
|
|
838
|
+
@@ -5,7 +5,7 @@ export const config = {
|
|
839
|
+
apiUrl: process.env.API_URL,
|
|
840
|
+
environment: process.env.NODE_ENV,
|
|
841
|
+
features: {
|
|
842
|
+
- timeout: 5000,
|
|
843
|
+
+ timeout: 10000,
|
|
844
|
+
retries: 3,
|
|
845
|
+
cacheEnabled: true,
|
|
846
|
+
},`
|
|
847
|
+
},
|
|
848
|
+
// Glob tool
|
|
849
|
+
{
|
|
850
|
+
toolName: "Glob",
|
|
851
|
+
kind: "search",
|
|
852
|
+
title: 'Glob "**/*.test.ts"',
|
|
853
|
+
input: {
|
|
854
|
+
pattern: "**/*.test.ts",
|
|
855
|
+
path: "/home/user/repos/project"
|
|
856
|
+
},
|
|
857
|
+
output: `src/components/Button.test.ts
|
|
858
|
+
src/components/Input.test.ts
|
|
859
|
+
src/hooks/useAuth.test.ts
|
|
860
|
+
src/lib/utils.test.ts
|
|
861
|
+
src/lib/api.test.ts`
|
|
862
|
+
},
|
|
863
|
+
// Grep tool
|
|
864
|
+
{
|
|
865
|
+
toolName: "Grep",
|
|
866
|
+
kind: "search",
|
|
867
|
+
title: 'Grep "useState" in src/',
|
|
868
|
+
input: {
|
|
869
|
+
pattern: "useState",
|
|
870
|
+
path: "/home/user/repos/project/src"
|
|
871
|
+
},
|
|
872
|
+
output: `src/components/Counter.tsx:3:import { useState } from 'react';
|
|
873
|
+
src/components/Form.tsx:1:import { useState, useCallback } from 'react';
|
|
874
|
+
src/hooks/useToggle.ts:1:import { useState } from 'react';
|
|
875
|
+
src/App.tsx:2:import { useState, useEffect } from 'react';`
|
|
876
|
+
},
|
|
877
|
+
// Task/subagent tool
|
|
878
|
+
{
|
|
879
|
+
toolName: "Task",
|
|
880
|
+
kind: "think",
|
|
881
|
+
title: "Explore Task",
|
|
882
|
+
input: {
|
|
883
|
+
subagent_type: "Explore",
|
|
884
|
+
description: "Find authentication implementation",
|
|
885
|
+
prompt: "Search for authentication-related files and understand the auth flow"
|
|
886
|
+
},
|
|
887
|
+
output: `Found 3 relevant files:
|
|
888
|
+
- src/hooks/useAuth.ts (main auth hook)
|
|
889
|
+
- src/context/AuthContext.tsx (auth provider)
|
|
890
|
+
- src/lib/auth.ts (token management)
|
|
891
|
+
|
|
892
|
+
The app uses JWT tokens stored in localStorage with automatic refresh.`
|
|
893
|
+
},
|
|
894
|
+
// Write tool
|
|
895
|
+
{
|
|
896
|
+
toolName: "Write",
|
|
897
|
+
kind: "edit",
|
|
898
|
+
title: "Write ~/repos/project/src/newFile.ts",
|
|
899
|
+
input: {
|
|
900
|
+
file_path: "/home/user/repos/project/src/newFile.ts",
|
|
901
|
+
content: 'export const VERSION = "1.0.0";'
|
|
902
|
+
},
|
|
903
|
+
output: "File created successfully"
|
|
904
|
+
},
|
|
905
|
+
// WebFetch tool
|
|
906
|
+
{
|
|
907
|
+
toolName: "WebFetch",
|
|
908
|
+
kind: "fetch",
|
|
909
|
+
title: "Fetch https://api.example.com/docs",
|
|
910
|
+
input: {
|
|
911
|
+
url: "https://api.example.com/docs",
|
|
912
|
+
prompt: "Get the API documentation"
|
|
913
|
+
},
|
|
914
|
+
output: `# API Documentation
|
|
915
|
+
|
|
916
|
+
## Authentication
|
|
917
|
+
All requests require a Bearer token in the Authorization header.
|
|
918
|
+
|
|
919
|
+
## Endpoints
|
|
920
|
+
- GET /users - List all users
|
|
921
|
+
- POST /users - Create a user
|
|
922
|
+
- GET /users/:id - Get user by ID`
|
|
923
|
+
}
|
|
924
|
+
];
|
|
925
|
+
function randomInt(min, max) {
|
|
926
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
927
|
+
}
|
|
928
|
+
function randomChoice(arr) {
|
|
929
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
930
|
+
}
|
|
931
|
+
function generateText(length) {
|
|
932
|
+
const words = [];
|
|
933
|
+
while (words.join(" ").length < length) {
|
|
934
|
+
words.push(randomChoice(LOREM_WORDS));
|
|
935
|
+
}
|
|
936
|
+
return words.join(" ").slice(0, length);
|
|
937
|
+
}
|
|
938
|
+
function generateMessageChunks(totalLength, chunkSize, kind = "agent_message_chunk") {
|
|
939
|
+
const text = generateText(totalLength);
|
|
940
|
+
const chunks = [];
|
|
941
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
942
|
+
const chunk = text.slice(i, i + chunkSize);
|
|
943
|
+
chunks.push({
|
|
944
|
+
sessionUpdate: kind,
|
|
945
|
+
content: { type: "text", text: chunk }
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
return chunks;
|
|
949
|
+
}
|
|
950
|
+
function generateMarkdownChunks(chunkSize = 50) {
|
|
951
|
+
const markdown = randomChoice(MARKDOWN_SAMPLES);
|
|
952
|
+
const chunks = [];
|
|
953
|
+
for (let i = 0; i < markdown.length; i += chunkSize) {
|
|
954
|
+
const chunk = markdown.slice(i, i + chunkSize);
|
|
955
|
+
chunks.push({
|
|
956
|
+
sessionUpdate: "agent_message_chunk",
|
|
957
|
+
content: { type: "text", text: chunk }
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
return chunks;
|
|
961
|
+
}
|
|
962
|
+
function generateThoughtChunks(totalLength) {
|
|
963
|
+
const prefix = randomChoice(THOUGHT_PREFIXES);
|
|
964
|
+
const rest = generateText(totalLength - prefix.length);
|
|
965
|
+
const text = `${prefix} ${rest}`;
|
|
966
|
+
const chunkSize = randomInt(30, 60);
|
|
967
|
+
const chunks = [];
|
|
968
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
969
|
+
const chunk = text.slice(i, i + chunkSize);
|
|
970
|
+
chunks.push({
|
|
971
|
+
sessionUpdate: "agent_thought_chunk",
|
|
972
|
+
content: { type: "text", text: chunk }
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return chunks;
|
|
976
|
+
}
|
|
977
|
+
function generateRichToolCall(config2) {
|
|
978
|
+
const toolCallId = randomUUID3();
|
|
979
|
+
const toolConfig = config2 ?? randomChoice(RICH_TOOL_CALLS);
|
|
980
|
+
const call = {
|
|
981
|
+
sessionUpdate: "tool_call",
|
|
982
|
+
toolCallId,
|
|
983
|
+
title: toolConfig.title,
|
|
984
|
+
status: "in_progress",
|
|
985
|
+
kind: toolConfig.kind,
|
|
986
|
+
rawInput: toolConfig.input,
|
|
987
|
+
_meta: {
|
|
988
|
+
claudeCode: {
|
|
989
|
+
toolName: toolConfig.toolName
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
const toolResponse = [];
|
|
994
|
+
if (toolConfig.output) {
|
|
995
|
+
toolResponse.push({ type: "text", text: toolConfig.output });
|
|
996
|
+
}
|
|
997
|
+
const content = [];
|
|
998
|
+
if (toolConfig.diff) {
|
|
999
|
+
content.push({ type: "diff", content: toolConfig.diff });
|
|
1000
|
+
}
|
|
1001
|
+
const update = {
|
|
1002
|
+
sessionUpdate: "tool_call_update",
|
|
1003
|
+
toolCallId,
|
|
1004
|
+
status: "completed",
|
|
1005
|
+
// Title update (sometimes tools update their title)
|
|
1006
|
+
title: toolConfig.title.replace("...", ""),
|
|
1007
|
+
_meta: {
|
|
1008
|
+
claudeCode: {
|
|
1009
|
+
toolName: toolConfig.toolName,
|
|
1010
|
+
toolResponse: toolResponse.length > 0 ? toolResponse : void 0
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
...content.length > 0 && { content }
|
|
1014
|
+
};
|
|
1015
|
+
return { call, update, toolCallId };
|
|
1016
|
+
}
|
|
1017
|
+
function generatePlan(entryCount) {
|
|
1018
|
+
const entries = [];
|
|
1019
|
+
const completedCount = randomInt(0, Math.floor(entryCount / 2));
|
|
1020
|
+
for (let i = 0; i < entryCount; i++) {
|
|
1021
|
+
const status = i < completedCount ? "completed" : i === completedCount ? "in_progress" : "pending";
|
|
1022
|
+
entries.push({
|
|
1023
|
+
content: randomChoice(PLAN_TASKS),
|
|
1024
|
+
priority: randomChoice(["high", "medium", "low"]),
|
|
1025
|
+
status
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
sessionUpdate: "plan",
|
|
1030
|
+
entries
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function generateRealisticTurn(config2 = DEFAULT_STRESS_CONFIG) {
|
|
1034
|
+
const events = [];
|
|
1035
|
+
if (Math.random() < config2.thoughtProbability) {
|
|
1036
|
+
const thoughtLength = randomInt(100, 300);
|
|
1037
|
+
events.push(...generateThoughtChunks(thoughtLength));
|
|
1038
|
+
}
|
|
1039
|
+
if (Math.random() < config2.planProbability) {
|
|
1040
|
+
events.push(generatePlan(randomInt(3, 6)));
|
|
1041
|
+
}
|
|
1042
|
+
if (Math.random() < config2.toolCallProbability) {
|
|
1043
|
+
const toolCount = randomInt(1, 3);
|
|
1044
|
+
for (let i = 0; i < toolCount; i++) {
|
|
1045
|
+
const { call, update } = generateRichToolCall();
|
|
1046
|
+
events.push(call);
|
|
1047
|
+
if (Math.random() < 0.3) {
|
|
1048
|
+
events.push(...generateThoughtChunks(randomInt(50, 100)));
|
|
1049
|
+
}
|
|
1050
|
+
events.push(update);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (Math.random() < 0.7) {
|
|
1054
|
+
events.push(...generateMarkdownChunks(config2.chunkSize));
|
|
1055
|
+
} else {
|
|
1056
|
+
const responseLength = randomInt(100, config2.eventsPerTurn * config2.chunkSize);
|
|
1057
|
+
events.push(...generateMessageChunks(responseLength, config2.chunkSize));
|
|
1058
|
+
}
|
|
1059
|
+
return events;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/stress/config.ts
|
|
1063
|
+
var config = {
|
|
1064
|
+
turnDelay: 5,
|
|
1065
|
+
eventsPerTurn: 50,
|
|
1066
|
+
eventDelay: 30,
|
|
1067
|
+
// 30ms between events for visible streaming
|
|
1068
|
+
chunkSize: 20
|
|
1069
|
+
// smaller chunks look more like streaming
|
|
1070
|
+
};
|
|
1071
|
+
function getStressRuntimeConfig() {
|
|
1072
|
+
return { ...config };
|
|
1073
|
+
}
|
|
1074
|
+
function setStressRuntimeConfig(updates) {
|
|
1075
|
+
if (updates.turnDelay !== void 0) {
|
|
1076
|
+
config.turnDelay = Math.max(0, Math.min(60, updates.turnDelay));
|
|
1077
|
+
}
|
|
1078
|
+
if (updates.eventsPerTurn !== void 0) {
|
|
1079
|
+
config.eventsPerTurn = Math.max(10, Math.min(500, updates.eventsPerTurn));
|
|
1080
|
+
}
|
|
1081
|
+
if (updates.eventDelay !== void 0) {
|
|
1082
|
+
config.eventDelay = Math.max(0, Math.min(200, updates.eventDelay));
|
|
1083
|
+
}
|
|
1084
|
+
if (updates.chunkSize !== void 0) {
|
|
1085
|
+
config.chunkSize = Math.max(5, Math.min(100, updates.chunkSize));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/agents/backends/stress.ts
|
|
1090
|
+
function delay2(ms) {
|
|
1091
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1092
|
+
}
|
|
1093
|
+
var StressBackend = class {
|
|
1094
|
+
type = "stress";
|
|
1095
|
+
running = /* @__PURE__ */ new Map();
|
|
1096
|
+
constructor() {
|
|
1097
|
+
}
|
|
1098
|
+
getConfig() {
|
|
1099
|
+
const runtime = getStressRuntimeConfig();
|
|
1100
|
+
return {
|
|
1101
|
+
eventsPerTurn: runtime.eventsPerTurn,
|
|
1102
|
+
toolCallProbability: 0.4,
|
|
1103
|
+
thoughtProbability: 0.6,
|
|
1104
|
+
planProbability: 0.3,
|
|
1105
|
+
chunkSize: runtime.chunkSize,
|
|
1106
|
+
delayMs: runtime.eventDelay
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
matches(config2) {
|
|
1110
|
+
return config2.command === "__stress__";
|
|
1111
|
+
}
|
|
1112
|
+
async start(sessionId, _config, _cwd, _existingAcpSessionId, emit) {
|
|
1113
|
+
debug("stress", ` Starting stress agent for session ${sessionId}`);
|
|
1114
|
+
emit({
|
|
1115
|
+
sessionUpdate: "current_mode_update",
|
|
1116
|
+
currentModeId: "stress"
|
|
1117
|
+
});
|
|
1118
|
+
const agent = { emit, stopped: false };
|
|
1119
|
+
this.running.set(sessionId, agent);
|
|
1120
|
+
this.runEventLoop(sessionId, agent);
|
|
1121
|
+
debug("stress", ` Stress agent ready for session ${sessionId}`);
|
|
1122
|
+
return {
|
|
1123
|
+
sessionId: randomUUID4(),
|
|
1124
|
+
models: [{ modelId: "stress-model", name: "Stress Model" }],
|
|
1125
|
+
modes: [{ id: "stress", name: "Stress Mode" }]
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
getTurnDelayMs() {
|
|
1129
|
+
const { turnDelay } = getStressRuntimeConfig();
|
|
1130
|
+
if (turnDelay === 0) {
|
|
1131
|
+
return 0;
|
|
1132
|
+
}
|
|
1133
|
+
const variance = turnDelay * 0.5;
|
|
1134
|
+
return (turnDelay - variance + Math.random() * variance * 2) * 1e3;
|
|
1135
|
+
}
|
|
1136
|
+
async runEventLoop(sessionId, agent) {
|
|
1137
|
+
await delay2(1e3);
|
|
1138
|
+
let turnCount = 0;
|
|
1139
|
+
while (!agent.stopped) {
|
|
1140
|
+
turnCount++;
|
|
1141
|
+
agent.emit({ amuxEvent: "turn_start" });
|
|
1142
|
+
agent.emit({
|
|
1143
|
+
sessionUpdate: "user_message_chunk",
|
|
1144
|
+
content: { type: "text", text: `[Stress test turn ${turnCount}]` }
|
|
1145
|
+
});
|
|
1146
|
+
const config2 = this.getConfig();
|
|
1147
|
+
const events = generateRealisticTurn(config2);
|
|
1148
|
+
for (const event of events) {
|
|
1149
|
+
if (agent.stopped) break;
|
|
1150
|
+
agent.emit(event);
|
|
1151
|
+
if (config2.delayMs > 0) {
|
|
1152
|
+
await delay2(config2.delayMs);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (!agent.stopped) {
|
|
1156
|
+
agent.emit({ amuxEvent: "turn_end" });
|
|
1157
|
+
}
|
|
1158
|
+
const turnDelay = this.getTurnDelayMs();
|
|
1159
|
+
await delay2(turnDelay);
|
|
1160
|
+
}
|
|
1161
|
+
debug("stress", ` Event loop stopped for session ${sessionId}`);
|
|
1162
|
+
}
|
|
1163
|
+
async prompt(sessionId, content, emit) {
|
|
1164
|
+
const text = content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1165
|
+
debug("stress", ` Prompt for session ${sessionId}: "${text.slice(0, 50)}..."`);
|
|
1166
|
+
const config2 = this.getConfig();
|
|
1167
|
+
const events = generateRealisticTurn(config2);
|
|
1168
|
+
for (const event of events) {
|
|
1169
|
+
emit(event);
|
|
1170
|
+
if (config2.delayMs > 0) {
|
|
1171
|
+
await delay2(config2.delayMs);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async stop(sessionId) {
|
|
1176
|
+
debug("stress", ` Stopping stress agent for session ${sessionId}`);
|
|
1177
|
+
const agent = this.running.get(sessionId);
|
|
1178
|
+
if (agent) {
|
|
1179
|
+
agent.stopped = true;
|
|
1180
|
+
this.running.delete(sessionId);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async stopAll() {
|
|
1184
|
+
const sessionIds = [...this.running.keys()];
|
|
1185
|
+
await Promise.all(sessionIds.map((id) => this.stop(id)));
|
|
1186
|
+
}
|
|
1187
|
+
isRunning(sessionId) {
|
|
1188
|
+
return this.running.has(sessionId);
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
// src/agents/backends/shell.ts
|
|
1193
|
+
var pty = null;
|
|
1194
|
+
async function getPty() {
|
|
1195
|
+
if (!pty) {
|
|
1196
|
+
pty = await import("node-pty");
|
|
1197
|
+
}
|
|
1198
|
+
return pty;
|
|
1199
|
+
}
|
|
1200
|
+
var MAX_SCROLLBACK = 1e5;
|
|
1201
|
+
var ShellBackend = class {
|
|
1202
|
+
type = "shell";
|
|
1203
|
+
isInteractive = true;
|
|
1204
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1205
|
+
matches(config2) {
|
|
1206
|
+
return config2.command === "__shell__";
|
|
1207
|
+
}
|
|
1208
|
+
async start(sessionId, _config, cwd, _existingAcpSessionId, emit) {
|
|
1209
|
+
const existing = this.sessions.get(sessionId);
|
|
1210
|
+
if (existing) {
|
|
1211
|
+
debug("shell", `Session ${sessionId} already running, reusing`);
|
|
1212
|
+
existing.emit = emit;
|
|
1213
|
+
return { sessionId };
|
|
1214
|
+
}
|
|
1215
|
+
const nodePty = await getPty();
|
|
1216
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
1217
|
+
debug("shell", `Spawning ${shell} in ${cwd}`);
|
|
1218
|
+
const p = nodePty.spawn(shell, [], {
|
|
1219
|
+
name: "xterm-256color",
|
|
1220
|
+
cols: 80,
|
|
1221
|
+
rows: 24,
|
|
1222
|
+
cwd,
|
|
1223
|
+
env: process.env
|
|
1224
|
+
});
|
|
1225
|
+
const session = {
|
|
1226
|
+
pty: p,
|
|
1227
|
+
scrollback: "",
|
|
1228
|
+
cwd,
|
|
1229
|
+
emit
|
|
1230
|
+
};
|
|
1231
|
+
p.onData((data) => {
|
|
1232
|
+
session.scrollback += data;
|
|
1233
|
+
if (session.scrollback.length > MAX_SCROLLBACK) {
|
|
1234
|
+
session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
|
|
1235
|
+
}
|
|
1236
|
+
session.emit({ amuxEvent: "terminal_output", data });
|
|
1237
|
+
});
|
|
1238
|
+
p.onExit(({ exitCode, signal }) => {
|
|
1239
|
+
debug("shell", `PTY exited: code=${exitCode}, signal=${signal}`);
|
|
1240
|
+
session.emit({
|
|
1241
|
+
amuxEvent: "terminal_exit",
|
|
1242
|
+
exitCode: exitCode ?? null,
|
|
1243
|
+
signal: signal !== void 0 ? String(signal) : null
|
|
1244
|
+
});
|
|
1245
|
+
this.sessions.delete(sessionId);
|
|
1246
|
+
});
|
|
1247
|
+
this.sessions.set(sessionId, session);
|
|
1248
|
+
debug("shell", `Session ${sessionId} started`);
|
|
1249
|
+
return { sessionId };
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Get accumulated scrollback for replay on reconnect.
|
|
1253
|
+
*/
|
|
1254
|
+
getScrollback(sessionId) {
|
|
1255
|
+
return this.sessions.get(sessionId)?.scrollback;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Write raw input to terminal (keystrokes from client).
|
|
1259
|
+
*/
|
|
1260
|
+
terminalWrite(sessionId, data) {
|
|
1261
|
+
const session = this.sessions.get(sessionId);
|
|
1262
|
+
if (!session) {
|
|
1263
|
+
warn("shell", `terminalWrite: session ${sessionId} not found`);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
session.pty.write(data);
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Resize terminal dimensions.
|
|
1270
|
+
*/
|
|
1271
|
+
terminalResize(sessionId, cols, rows) {
|
|
1272
|
+
const session = this.sessions.get(sessionId);
|
|
1273
|
+
if (!session) {
|
|
1274
|
+
warn("shell", `terminalResize: session ${sessionId} not found`);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
session.pty.resize(cols, rows);
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Shell does not support conversational prompts.
|
|
1281
|
+
*/
|
|
1282
|
+
async prompt() {
|
|
1283
|
+
throw new Error("Shell backend does not support prompt(). Use terminalWrite() instead.");
|
|
1284
|
+
}
|
|
1285
|
+
async stop(sessionId) {
|
|
1286
|
+
const session = this.sessions.get(sessionId);
|
|
1287
|
+
if (!session) return;
|
|
1288
|
+
debug("shell", `Stopping session ${sessionId}`);
|
|
1289
|
+
session.pty.kill();
|
|
1290
|
+
this.sessions.delete(sessionId);
|
|
1291
|
+
}
|
|
1292
|
+
async stopAll() {
|
|
1293
|
+
debug("shell", `Stopping all sessions (${this.sessions.size})`);
|
|
1294
|
+
for (const sessionId of this.sessions.keys()) {
|
|
1295
|
+
await this.stop(sessionId);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
isRunning(sessionId) {
|
|
1299
|
+
return this.sessions.has(sessionId);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
// src/lib/mentions.ts
|
|
1304
|
+
import path from "path";
|
|
1305
|
+
function parseMessageToContentBlocks(message, workingDir) {
|
|
1306
|
+
const blocks = [];
|
|
1307
|
+
const mentionRegex = /@(\.{0,2}[\w\/\.\-]+)/g;
|
|
1308
|
+
let lastIndex = 0;
|
|
1309
|
+
for (const match of message.matchAll(mentionRegex)) {
|
|
1310
|
+
if (match.index > lastIndex) {
|
|
1311
|
+
const text = message.slice(lastIndex, match.index);
|
|
1312
|
+
if (text.trim()) {
|
|
1313
|
+
blocks.push({ type: "text", text });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
const relativePath = match[1];
|
|
1317
|
+
const absolutePath = path.resolve(workingDir, relativePath);
|
|
1318
|
+
blocks.push({
|
|
1319
|
+
type: "resource_link",
|
|
1320
|
+
uri: `file://${absolutePath}`,
|
|
1321
|
+
name: relativePath
|
|
1322
|
+
// Required per ACP spec - use the path the user typed
|
|
1323
|
+
});
|
|
1324
|
+
lastIndex = match.index + match[0].length;
|
|
1325
|
+
}
|
|
1326
|
+
if (lastIndex < message.length) {
|
|
1327
|
+
const text = message.slice(lastIndex);
|
|
1328
|
+
if (text.trim()) {
|
|
1329
|
+
blocks.push({ type: "text", text });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return blocks.length > 0 ? blocks : [{ type: "text", text: message }];
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/agents/manager.ts
|
|
1336
|
+
function generateTitleFromMessage(message) {
|
|
1337
|
+
const cleaned = message.replace(/@[\w./~-]+/g, "").replace(/\s+/g, " ").trim();
|
|
1338
|
+
const firstLine = cleaned.split("\n")[0] || cleaned;
|
|
1339
|
+
if (firstLine.length <= 50) return firstLine;
|
|
1340
|
+
const truncated = firstLine.slice(0, 50);
|
|
1341
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
1342
|
+
return lastSpace > 20 ? truncated.slice(0, lastSpace) + "..." : truncated + "...";
|
|
1343
|
+
}
|
|
1344
|
+
var AgentManager = class extends EventEmitter {
|
|
1345
|
+
backends;
|
|
1346
|
+
agents = /* @__PURE__ */ new Map();
|
|
1347
|
+
constructor() {
|
|
1348
|
+
super();
|
|
1349
|
+
const acpBackend = new AcpBackend();
|
|
1350
|
+
acpBackend.onSessionIdChanged = (sessionId, acpSessionId) => {
|
|
1351
|
+
db.update(sessions).set({ acpSessionId }).where(eq(sessions.id, sessionId)).run();
|
|
1352
|
+
};
|
|
1353
|
+
this.backends = [acpBackend, new ShellBackend(), new MockBackend(), new StressBackend()];
|
|
1354
|
+
}
|
|
1355
|
+
getBackendForConfig(config2) {
|
|
1356
|
+
const backend = this.backends.find((b) => b.matches(config2));
|
|
1357
|
+
if (!backend) {
|
|
1358
|
+
throw new Error(`No backend for agent config: ${config2.command}`);
|
|
1359
|
+
}
|
|
1360
|
+
return backend;
|
|
1361
|
+
}
|
|
1362
|
+
emitUpdate(sessionId, update) {
|
|
1363
|
+
storeEvent(sessionId, update);
|
|
1364
|
+
if (isSessionUpdate(update) && update.sessionUpdate === "session_info_update") {
|
|
1365
|
+
const title = update.title;
|
|
1366
|
+
if (title !== void 0) {
|
|
1367
|
+
db.update(sessions).set({ title }).where(eq(sessions.id, sessionId)).run();
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
this.emit("update", { sessionId, update });
|
|
1371
|
+
}
|
|
1372
|
+
async startForSession(sessionId) {
|
|
1373
|
+
const existing = this.agents.get(sessionId);
|
|
1374
|
+
if (existing?.status === "ready") {
|
|
1375
|
+
const replayEvents2 = getEventsForSession(sessionId);
|
|
1376
|
+
const scrollback = this.getTerminalScrollback(sessionId);
|
|
1377
|
+
debug("agents", `startForSession ${sessionId} - existing ready, scrollback: ${scrollback?.length ?? 0} bytes`);
|
|
1378
|
+
return {
|
|
1379
|
+
acpSessionId: existing.session.sessionId,
|
|
1380
|
+
replayEvents: replayEvents2,
|
|
1381
|
+
scrollback,
|
|
1382
|
+
models: existing.session.models,
|
|
1383
|
+
modes: existing.session.modes
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if (existing?.status === "starting") {
|
|
1387
|
+
return new Promise((resolve, reject) => {
|
|
1388
|
+
const checkReady = () => {
|
|
1389
|
+
const agent = this.agents.get(sessionId);
|
|
1390
|
+
if (agent?.status === "ready") {
|
|
1391
|
+
const replayEvents2 = getEventsForSession(sessionId);
|
|
1392
|
+
const scrollback = this.getTerminalScrollback(sessionId);
|
|
1393
|
+
resolve({
|
|
1394
|
+
acpSessionId: agent.session.sessionId,
|
|
1395
|
+
replayEvents: replayEvents2,
|
|
1396
|
+
scrollback,
|
|
1397
|
+
models: agent.session.models,
|
|
1398
|
+
modes: agent.session.modes
|
|
1399
|
+
});
|
|
1400
|
+
} else if (agent?.status === "dead") {
|
|
1401
|
+
reject(new Error("Agent failed to start"));
|
|
1402
|
+
} else {
|
|
1403
|
+
setTimeout(checkReady, 100);
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
checkReady();
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
const session = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
|
1410
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
1411
|
+
const dbConfig = db.select().from(agentConfigs).where(eq(agentConfigs.id, session.agentConfigId)).get();
|
|
1412
|
+
if (!dbConfig) throw new Error(`Agent config ${session.agentConfigId} not found`);
|
|
1413
|
+
const config2 = {
|
|
1414
|
+
id: dbConfig.id,
|
|
1415
|
+
name: dbConfig.name,
|
|
1416
|
+
command: dbConfig.command,
|
|
1417
|
+
args: dbConfig.args ?? [],
|
|
1418
|
+
env: dbConfig.env ?? {}
|
|
1419
|
+
};
|
|
1420
|
+
const backend = this.getBackendForConfig(config2);
|
|
1421
|
+
const emit = (update) => this.emitUpdate(sessionId, update);
|
|
1422
|
+
this.agents.set(sessionId, {
|
|
1423
|
+
backend,
|
|
1424
|
+
session: { sessionId: "" },
|
|
1425
|
+
status: "starting"
|
|
1426
|
+
});
|
|
1427
|
+
const replayEvents = getEventsForSession(sessionId);
|
|
1428
|
+
try {
|
|
1429
|
+
const agentSession = await backend.start(sessionId, config2, session.directory, session.acpSessionId ?? null, emit);
|
|
1430
|
+
this.agents.set(sessionId, {
|
|
1431
|
+
backend,
|
|
1432
|
+
session: agentSession,
|
|
1433
|
+
status: "ready"
|
|
1434
|
+
});
|
|
1435
|
+
debug("agents", `Agent ready for session ${sessionId}`);
|
|
1436
|
+
const scrollback = this.getTerminalScrollback(sessionId);
|
|
1437
|
+
return {
|
|
1438
|
+
acpSessionId: agentSession.sessionId,
|
|
1439
|
+
replayEvents,
|
|
1440
|
+
scrollback,
|
|
1441
|
+
models: agentSession.models,
|
|
1442
|
+
modes: agentSession.modes
|
|
1443
|
+
};
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
this.agents.set(sessionId, {
|
|
1446
|
+
backend,
|
|
1447
|
+
session: { sessionId: "" },
|
|
1448
|
+
status: "dead"
|
|
1449
|
+
});
|
|
1450
|
+
throw err;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
// Legacy alias for compatibility during migration
|
|
1454
|
+
/** @deprecated Use startForSession instead */
|
|
1455
|
+
startForWindow = this.startForSession.bind(this);
|
|
1456
|
+
async prompt(sessionId, message) {
|
|
1457
|
+
debug("agents", `prompt() called for session ${sessionId}: "${message.slice(0, 50)}..."`);
|
|
1458
|
+
const agent = this.agents.get(sessionId);
|
|
1459
|
+
if (!agent || agent.status !== "ready") {
|
|
1460
|
+
throw new Error(`Agent not ready for session ${sessionId}`);
|
|
1461
|
+
}
|
|
1462
|
+
const session = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
|
1463
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
1464
|
+
if (!session.title) {
|
|
1465
|
+
const title = generateTitleFromMessage(message);
|
|
1466
|
+
if (title) {
|
|
1467
|
+
db.update(sessions).set({ title }).where(eq(sessions.id, sessionId)).run();
|
|
1468
|
+
this.emitUpdate(sessionId, {
|
|
1469
|
+
sessionUpdate: "session_info_update",
|
|
1470
|
+
title
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
startTurn(sessionId);
|
|
1475
|
+
this.emitUpdate(sessionId, { amuxEvent: "turn_start" });
|
|
1476
|
+
this.emitUpdate(sessionId, {
|
|
1477
|
+
sessionUpdate: "user_message_chunk",
|
|
1478
|
+
content: { type: "text", text: message }
|
|
1479
|
+
});
|
|
1480
|
+
const content = parseMessageToContentBlocks(message, session.directory);
|
|
1481
|
+
const emit = (update) => this.emitUpdate(sessionId, update);
|
|
1482
|
+
try {
|
|
1483
|
+
await agent.backend.prompt(sessionId, content, emit);
|
|
1484
|
+
} finally {
|
|
1485
|
+
endTurn(sessionId);
|
|
1486
|
+
this.emitUpdate(sessionId, { amuxEvent: "turn_end" });
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async setMode(sessionId, modeId) {
|
|
1490
|
+
const agent = this.agents.get(sessionId);
|
|
1491
|
+
if (!agent || agent.status !== "ready") {
|
|
1492
|
+
throw new Error(`Agent not ready for session ${sessionId}`);
|
|
1493
|
+
}
|
|
1494
|
+
if (!agent.backend.setMode) {
|
|
1495
|
+
throw new Error(`Backend ${agent.backend.type} does not support setMode`);
|
|
1496
|
+
}
|
|
1497
|
+
await agent.backend.setMode(sessionId, modeId);
|
|
1498
|
+
}
|
|
1499
|
+
async setModel(sessionId, modelId) {
|
|
1500
|
+
const agent = this.agents.get(sessionId);
|
|
1501
|
+
if (!agent || agent.status !== "ready") {
|
|
1502
|
+
throw new Error(`Agent not ready for session ${sessionId}`);
|
|
1503
|
+
}
|
|
1504
|
+
if (!agent.backend.setModel) {
|
|
1505
|
+
throw new Error(`Backend ${agent.backend.type} does not support setModel`);
|
|
1506
|
+
}
|
|
1507
|
+
await agent.backend.setModel(sessionId, modelId);
|
|
1508
|
+
}
|
|
1509
|
+
async cancel(sessionId) {
|
|
1510
|
+
const agent = this.agents.get(sessionId);
|
|
1511
|
+
if (!agent?.backend.cancel) return;
|
|
1512
|
+
await agent.backend.cancel(sessionId);
|
|
1513
|
+
}
|
|
1514
|
+
// Terminal-specific methods (for ShellBackend)
|
|
1515
|
+
terminalWrite(sessionId, data) {
|
|
1516
|
+
const agent = this.agents.get(sessionId);
|
|
1517
|
+
if (!agent) {
|
|
1518
|
+
throw new Error(`No agent running for session ${sessionId}`);
|
|
1519
|
+
}
|
|
1520
|
+
if (!agent.backend.terminalWrite) {
|
|
1521
|
+
throw new Error(`Backend ${agent.backend.type} does not support terminal input`);
|
|
1522
|
+
}
|
|
1523
|
+
agent.backend.terminalWrite(sessionId, data);
|
|
1524
|
+
}
|
|
1525
|
+
terminalResize(sessionId, cols, rows) {
|
|
1526
|
+
const agent = this.agents.get(sessionId);
|
|
1527
|
+
if (!agent) {
|
|
1528
|
+
throw new Error(`No agent running for session ${sessionId}`);
|
|
1529
|
+
}
|
|
1530
|
+
if (!agent.backend.terminalResize) {
|
|
1531
|
+
throw new Error(`Backend ${agent.backend.type} does not support terminal resize`);
|
|
1532
|
+
}
|
|
1533
|
+
agent.backend.terminalResize(sessionId, cols, rows);
|
|
1534
|
+
}
|
|
1535
|
+
getTerminalScrollback(sessionId) {
|
|
1536
|
+
const agent = this.agents.get(sessionId);
|
|
1537
|
+
if (!agent?.backend.getScrollback) return void 0;
|
|
1538
|
+
return agent.backend.getScrollback(sessionId);
|
|
1539
|
+
}
|
|
1540
|
+
isTerminalSession(sessionId) {
|
|
1541
|
+
const agent = this.agents.get(sessionId);
|
|
1542
|
+
return agent?.backend.isInteractive ?? false;
|
|
1543
|
+
}
|
|
1544
|
+
respondPermission(sessionId, requestId, optionId) {
|
|
1545
|
+
const agent = this.agents.get(sessionId);
|
|
1546
|
+
if (!agent) {
|
|
1547
|
+
console.warn(`[AgentManager] No agent running for session ${sessionId}, ignoring permission response`);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (!agent.backend.respondToPermission) {
|
|
1551
|
+
throw new Error(`Backend ${agent.backend.type} does not support permissions`);
|
|
1552
|
+
}
|
|
1553
|
+
agent.backend.respondToPermission(sessionId, requestId, optionId);
|
|
1554
|
+
}
|
|
1555
|
+
async stopForSession(sessionId) {
|
|
1556
|
+
const agent = this.agents.get(sessionId);
|
|
1557
|
+
if (agent) {
|
|
1558
|
+
await agent.backend.stop(sessionId);
|
|
1559
|
+
}
|
|
1560
|
+
this.agents.delete(sessionId);
|
|
1561
|
+
}
|
|
1562
|
+
// Legacy alias for compatibility during migration
|
|
1563
|
+
/** @deprecated Use stopForSession instead */
|
|
1564
|
+
stopForWindow = this.stopForSession.bind(this);
|
|
1565
|
+
async stopAll() {
|
|
1566
|
+
const sessionIds = Array.from(this.agents.keys());
|
|
1567
|
+
await Promise.all(sessionIds.map((id) => this.stopForSession(id)));
|
|
1568
|
+
}
|
|
1569
|
+
getForSession(sessionId) {
|
|
1570
|
+
return this.agents.get(sessionId);
|
|
1571
|
+
}
|
|
1572
|
+
getPendingPermission(sessionId) {
|
|
1573
|
+
const agent = this.agents.get(sessionId);
|
|
1574
|
+
if (!agent?.backend.getPendingPermission) return null;
|
|
1575
|
+
return agent.backend.getPendingPermission(sessionId);
|
|
1576
|
+
}
|
|
1577
|
+
// Legacy alias for compatibility during migration
|
|
1578
|
+
/** @deprecated Use getForSession instead */
|
|
1579
|
+
getForWindow = this.getForSession.bind(this);
|
|
1580
|
+
registerBackend(backend) {
|
|
1581
|
+
this.backends.unshift(backend);
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
var agentManager = new AgentManager();
|
|
1585
|
+
|
|
1586
|
+
export {
|
|
1587
|
+
getStressRuntimeConfig,
|
|
1588
|
+
setStressRuntimeConfig,
|
|
1589
|
+
agentManager
|
|
1590
|
+
};
|
|
1591
|
+
//# sourceMappingURL=chunk-RSLSN7F2.js.map
|