agent-detective 1.0.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 +101 -0
- package/dist/chunk-H2IXGHNA.js +1237 -0
- package/dist/chunk-OIYJYLCB.js +685 -0
- package/dist/doctor-3ZMDZLW6.js +193 -0
- package/dist/index.js +661 -0
- package/dist/init-IPCDLNHA.js +179 -0
- package/package.json +88 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createLimitedConcurrencyTaskQueue,
|
|
4
|
+
createMemoryTaskQueue,
|
|
5
|
+
createPluginSystem
|
|
6
|
+
} from "./chunk-OIYJYLCB.js";
|
|
7
|
+
import {
|
|
8
|
+
APP_NAME,
|
|
9
|
+
APP_VERSION,
|
|
10
|
+
applyLogLevelAliasForObservability,
|
|
11
|
+
createServer,
|
|
12
|
+
getAgentLabel,
|
|
13
|
+
listAgents,
|
|
14
|
+
loadConfig,
|
|
15
|
+
normalizeAgent
|
|
16
|
+
} from "./chunk-H2IXGHNA.js";
|
|
17
|
+
|
|
18
|
+
// src/core/event-bus.ts
|
|
19
|
+
var AsyncEventBus = class {
|
|
20
|
+
constructor(log = console) {
|
|
21
|
+
this.log = log;
|
|
22
|
+
}
|
|
23
|
+
log;
|
|
24
|
+
handlers = /* @__PURE__ */ new Map();
|
|
25
|
+
/**
|
|
26
|
+
* Register a listener for an event.
|
|
27
|
+
*/
|
|
28
|
+
on(event, handler) {
|
|
29
|
+
const list = this.handlers.get(event) || [];
|
|
30
|
+
list.push(handler);
|
|
31
|
+
this.handlers.set(event, list);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Unregister a listener for an event.
|
|
35
|
+
*/
|
|
36
|
+
off(event, handler) {
|
|
37
|
+
const list = this.handlers.get(event) || [];
|
|
38
|
+
const index = list.indexOf(handler);
|
|
39
|
+
if (index !== -1) {
|
|
40
|
+
list.splice(index, 1);
|
|
41
|
+
if (list.length === 0) {
|
|
42
|
+
this.handlers.delete(event);
|
|
43
|
+
} else {
|
|
44
|
+
this.handlers.set(event, list);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Emit an event without waiting for responses (fire-and-forget).
|
|
50
|
+
*/
|
|
51
|
+
emit(event, ...args) {
|
|
52
|
+
const list = this.handlers.get(event) || [];
|
|
53
|
+
for (const handler of list) {
|
|
54
|
+
try {
|
|
55
|
+
void handler(...args);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
this.log.error(`Error in event handler for ${event}: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Invoke all handlers asynchronously and gather their return values.
|
|
63
|
+
* Useful for hook-like behavior where handlers provide data.
|
|
64
|
+
*/
|
|
65
|
+
async invokeAsync(event, ...args) {
|
|
66
|
+
const list = this.handlers.get(event) || [];
|
|
67
|
+
const promises = list.map(async (handler) => {
|
|
68
|
+
try {
|
|
69
|
+
return await handler(...args);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.log.error(
|
|
72
|
+
`Error in async event handler for ${event}: ${err instanceof Error ? err.message : String(err)}`
|
|
73
|
+
);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const results = await Promise.all(promises);
|
|
78
|
+
return results.filter((r) => r !== null && r !== void 0);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function createEventBus(log) {
|
|
82
|
+
return new AsyncEventBus(log);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/core/orchestrator.ts
|
|
86
|
+
import { StandardEvents } from "@agent-detective/sdk";
|
|
87
|
+
|
|
88
|
+
// src/core/run-records.ts
|
|
89
|
+
import { appendFile } from "fs/promises";
|
|
90
|
+
var RUN_RECORD_SCHEMA = "agent-detective.run-record/v1";
|
|
91
|
+
function createRunRecordWriter(absolutePath, logger) {
|
|
92
|
+
return {
|
|
93
|
+
async append(record) {
|
|
94
|
+
try {
|
|
95
|
+
await appendFile(absolutePath, `${JSON.stringify(record)}
|
|
96
|
+
`, "utf8");
|
|
97
|
+
} catch (err) {
|
|
98
|
+
logger.warn(`runRecords: append failed (${absolutePath}): ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function buildRunRecordBase(task) {
|
|
104
|
+
const issueKey = task.replyTo?.type === "issue" && typeof task.replyTo.id === "string" ? task.replyTo.id : void 0;
|
|
105
|
+
return {
|
|
106
|
+
taskId: task.id,
|
|
107
|
+
...typeof task.source === "string" && task.source.length > 0 ? { source: task.source } : {},
|
|
108
|
+
...issueKey ? { issueKey } : {}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/core/orchestrator.ts
|
|
113
|
+
function createOrchestrator(deps) {
|
|
114
|
+
const { eventBus, agentRunner, enqueue, logger, maxWallTimeMs, runRecords } = deps;
|
|
115
|
+
function start() {
|
|
116
|
+
eventBus.on(StandardEvents.TASK_CREATED, handleTaskCreated);
|
|
117
|
+
}
|
|
118
|
+
async function handleTaskCreated(task) {
|
|
119
|
+
const queueKey = task.id;
|
|
120
|
+
await enqueue(queueKey, async () => {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const base = buildRunRecordBase(task);
|
|
123
|
+
if (runRecords) {
|
|
124
|
+
await runRecords.append({
|
|
125
|
+
schema: RUN_RECORD_SCHEMA,
|
|
126
|
+
phase: "started",
|
|
127
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
128
|
+
...base
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const contextPieces = await eventBus.invokeAsync(
|
|
133
|
+
StandardEvents.TASK_GATHER_CONTEXT,
|
|
134
|
+
task
|
|
135
|
+
);
|
|
136
|
+
let finalPrompt = task.message;
|
|
137
|
+
if (contextPieces.length > 0) {
|
|
138
|
+
finalPrompt += "\n\nAdditional Context:\n" + contextPieces.join("\n\n");
|
|
139
|
+
}
|
|
140
|
+
const runPromise = agentRunner.runAgentForChat(task.id, finalPrompt, {
|
|
141
|
+
repoPath: task.context.repoPath,
|
|
142
|
+
model: task.context.model,
|
|
143
|
+
cwd: task.context.cwd,
|
|
144
|
+
readOnly: task.metadata?.readOnly === true,
|
|
145
|
+
threadId: task.context.threadId ?? void 0
|
|
146
|
+
});
|
|
147
|
+
const result = maxWallTimeMs !== void 0 && maxWallTimeMs > 0 ? await raceWithWallTimeout(runPromise, maxWallTimeMs) : await runPromise;
|
|
148
|
+
eventBus.emit(StandardEvents.TASK_COMPLETED, {
|
|
149
|
+
event: task,
|
|
150
|
+
result: result.text
|
|
151
|
+
});
|
|
152
|
+
if (runRecords) {
|
|
153
|
+
await runRecords.append({
|
|
154
|
+
schema: RUN_RECORD_SCHEMA,
|
|
155
|
+
phase: "completed",
|
|
156
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
157
|
+
...base,
|
|
158
|
+
durationMs: Date.now() - startedAt
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
163
|
+
logger.error(`Orchestrator error for task ${task.id}: ${message}`);
|
|
164
|
+
eventBus.emit(StandardEvents.TASK_FAILED, {
|
|
165
|
+
event: task,
|
|
166
|
+
error: message
|
|
167
|
+
});
|
|
168
|
+
if (runRecords) {
|
|
169
|
+
await runRecords.append({
|
|
170
|
+
schema: RUN_RECORD_SCHEMA,
|
|
171
|
+
phase: "failed",
|
|
172
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
173
|
+
...base,
|
|
174
|
+
durationMs: Date.now() - startedAt,
|
|
175
|
+
error: message
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
start
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function raceWithWallTimeout(work, maxWallTimeMs) {
|
|
186
|
+
let timeoutId;
|
|
187
|
+
const timeout = new Promise((_, reject) => {
|
|
188
|
+
timeoutId = setTimeout(() => {
|
|
189
|
+
reject(new Error(`Task exceeded orchestrator wall time (${maxWallTimeMs}ms)`));
|
|
190
|
+
}, maxWallTimeMs);
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
return await Promise.race([work, timeout]);
|
|
194
|
+
} finally {
|
|
195
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/core/process.ts
|
|
200
|
+
import {
|
|
201
|
+
shellQuote,
|
|
202
|
+
wrapCommandWithPty,
|
|
203
|
+
terminateChildProcess,
|
|
204
|
+
execLocal,
|
|
205
|
+
execLocalStreaming
|
|
206
|
+
} from "@agent-detective/process-utils";
|
|
207
|
+
|
|
208
|
+
// src/core/agent-runner.ts
|
|
209
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
210
|
+
var DEFAULT_MAX_BUFFER = 10 * 1024 * 1024;
|
|
211
|
+
function createAgentRunner(options) {
|
|
212
|
+
const {
|
|
213
|
+
agentTimeoutMs = DEFAULT_TIMEOUT_MS,
|
|
214
|
+
agentMaxBuffer = DEFAULT_MAX_BUFFER,
|
|
215
|
+
execLocal: execLocal2,
|
|
216
|
+
execLocalStreaming: execLocalStreaming2,
|
|
217
|
+
terminateChildProcess: terminateChildProcess2,
|
|
218
|
+
defaultModels,
|
|
219
|
+
postFinalGraceMs = 3e4,
|
|
220
|
+
forceKillDelayMs = 1e3,
|
|
221
|
+
logger: log = console,
|
|
222
|
+
defaultAgentId
|
|
223
|
+
} = options;
|
|
224
|
+
const agents = /* @__PURE__ */ new Map();
|
|
225
|
+
const activeRuns = /* @__PURE__ */ new Map();
|
|
226
|
+
const agentSerializationTails = /* @__PURE__ */ new Map();
|
|
227
|
+
function buildActiveRunKey(taskId, contextKey) {
|
|
228
|
+
return `${taskId}:${contextKey || "default"}`;
|
|
229
|
+
}
|
|
230
|
+
async function acquireAgentSlot(agentId, taskId) {
|
|
231
|
+
const prev = agentSerializationTails.get(agentId) ?? Promise.resolve();
|
|
232
|
+
let release;
|
|
233
|
+
const ours = new Promise((resolve2) => {
|
|
234
|
+
release = resolve2;
|
|
235
|
+
});
|
|
236
|
+
agentSerializationTails.set(agentId, ours);
|
|
237
|
+
const waitStartedAt = Date.now();
|
|
238
|
+
await prev;
|
|
239
|
+
const waitMs = Date.now() - waitStartedAt;
|
|
240
|
+
if (waitMs > 10) {
|
|
241
|
+
log.info(
|
|
242
|
+
`Agent queued task=${taskId} agent=${agentId} singleInstance=true waitMs=${waitMs}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return () => {
|
|
246
|
+
release();
|
|
247
|
+
if (agentSerializationTails.get(agentId) === ours) {
|
|
248
|
+
agentSerializationTails.delete(agentId);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async function runAgentForChat(taskId, prompt, runOptions = {}) {
|
|
253
|
+
const {
|
|
254
|
+
contextKey,
|
|
255
|
+
repoPath,
|
|
256
|
+
cwd = process.cwd(),
|
|
257
|
+
agentId: overrideAgentId,
|
|
258
|
+
model: modelOverride,
|
|
259
|
+
onFinal,
|
|
260
|
+
onProgress,
|
|
261
|
+
onStdout: callerOnStdout,
|
|
262
|
+
readOnly,
|
|
263
|
+
timeoutMs: runTimeoutMs,
|
|
264
|
+
threadId,
|
|
265
|
+
inputFiles
|
|
266
|
+
} = runOptions;
|
|
267
|
+
const effectiveAgentId = overrideAgentId || defaultAgentId || "opencode";
|
|
268
|
+
const agent = agents.get(effectiveAgentId);
|
|
269
|
+
if (!agent) {
|
|
270
|
+
throw new Error(`Unknown agent: ${effectiveAgentId}. Ensure it is registered.`);
|
|
271
|
+
}
|
|
272
|
+
const activeKey = buildActiveRunKey(taskId, contextKey);
|
|
273
|
+
const releaseAgentSlot = agent.singleInstance ? await acquireAgentSlot(agent.id, taskId) : null;
|
|
274
|
+
log.info(
|
|
275
|
+
`Agent start task=${taskId} agent=${effectiveAgentId} repo=${repoPath || "none"}${readOnly ? " readOnly=true" : ""}`
|
|
276
|
+
);
|
|
277
|
+
const startedAt = Date.now();
|
|
278
|
+
const run = {
|
|
279
|
+
child: null,
|
|
280
|
+
finalEmitted: false,
|
|
281
|
+
settled: false,
|
|
282
|
+
stopPending: false
|
|
283
|
+
};
|
|
284
|
+
activeRuns.set(activeKey, run);
|
|
285
|
+
const schedulePostFinalKill = () => {
|
|
286
|
+
if (!run.child || run.finalEmitted) return;
|
|
287
|
+
const delayMs = Math.max(0, Number(postFinalGraceMs) || 0);
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
if (run.settled || !run.child) return;
|
|
290
|
+
terminateChildProcess2?.(run.child, "SIGTERM");
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
if (run.settled || !run.child) return;
|
|
293
|
+
terminateChildProcess2?.(run.child, "SIGKILL");
|
|
294
|
+
}, Math.max(0, Number(forceKillDelayMs) || 0));
|
|
295
|
+
}, delayMs);
|
|
296
|
+
};
|
|
297
|
+
const emitFinal = (text) => {
|
|
298
|
+
const normalizedText = String(text || "").trim();
|
|
299
|
+
if (!normalizedText || run.finalEmitted) return;
|
|
300
|
+
run.finalEmitted = true;
|
|
301
|
+
onFinal?.(normalizedText);
|
|
302
|
+
schedulePostFinalKill();
|
|
303
|
+
};
|
|
304
|
+
const emitProgress = (payload) => {
|
|
305
|
+
onProgress?.(payload);
|
|
306
|
+
};
|
|
307
|
+
try {
|
|
308
|
+
const result = await runShellAgent(agent, {
|
|
309
|
+
prompt,
|
|
310
|
+
cwd,
|
|
311
|
+
model: modelOverride,
|
|
312
|
+
defaultModels,
|
|
313
|
+
run,
|
|
314
|
+
emitFinal,
|
|
315
|
+
emitProgress,
|
|
316
|
+
callerOnStdout,
|
|
317
|
+
readOnly,
|
|
318
|
+
timeoutMs: runTimeoutMs,
|
|
319
|
+
threadId: threadId && String(threadId).trim() ? String(threadId).trim() : void 0,
|
|
320
|
+
inputFiles
|
|
321
|
+
});
|
|
322
|
+
run.settled = true;
|
|
323
|
+
const elapsedMs = Date.now() - startedAt;
|
|
324
|
+
result.usage = { ...result.usage, wallTimeMs: elapsedMs };
|
|
325
|
+
log.info(`Agent finished task=${taskId} durationMs=${elapsedMs}${result.threadId ? ` threadId=${result.threadId}` : ""}`);
|
|
326
|
+
return result;
|
|
327
|
+
} catch (err) {
|
|
328
|
+
run.settled = true;
|
|
329
|
+
const elapsedMs = Date.now() - startedAt;
|
|
330
|
+
log.error(
|
|
331
|
+
`Agent error task=${taskId} durationMs=${elapsedMs} error=${err.message}`
|
|
332
|
+
);
|
|
333
|
+
throw err;
|
|
334
|
+
} finally {
|
|
335
|
+
activeRuns.delete(activeKey);
|
|
336
|
+
releaseAgentSlot?.();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function runShellAgent(agent, {
|
|
340
|
+
prompt,
|
|
341
|
+
cwd,
|
|
342
|
+
model,
|
|
343
|
+
defaultModels: defaultModels2,
|
|
344
|
+
run,
|
|
345
|
+
emitFinal,
|
|
346
|
+
emitProgress,
|
|
347
|
+
callerOnStdout,
|
|
348
|
+
readOnly,
|
|
349
|
+
timeoutMs: shellTimeoutMs,
|
|
350
|
+
threadId,
|
|
351
|
+
inputFiles
|
|
352
|
+
}) {
|
|
353
|
+
const effectiveTimeoutMs = typeof shellTimeoutMs === "number" && shellTimeoutMs > 0 ? shellTimeoutMs : agentTimeoutMs;
|
|
354
|
+
const promptBase64 = Buffer.from(prompt, "utf8").toString("base64");
|
|
355
|
+
const promptExpression = '"$PROMPT"';
|
|
356
|
+
const effectiveModel = model || defaultModels2?.[agent.id]?.defaultModel || agent.defaultModel;
|
|
357
|
+
const agentCmd = agent.buildCommand?.({
|
|
358
|
+
prompt,
|
|
359
|
+
promptExpression,
|
|
360
|
+
model: effectiveModel,
|
|
361
|
+
thinking: void 0,
|
|
362
|
+
readOnly,
|
|
363
|
+
threadId,
|
|
364
|
+
inputFiles
|
|
365
|
+
}) || `${agent.command} ${promptExpression}`;
|
|
366
|
+
const command = [
|
|
367
|
+
`PROMPT_B64=${shellQuote(promptBase64)};`,
|
|
368
|
+
'PROMPT=$(printf %s "$PROMPT_B64" | base64 --decode);',
|
|
369
|
+
agentCmd
|
|
370
|
+
].join(" ");
|
|
371
|
+
let commandToRun = command;
|
|
372
|
+
if (agent.needsPty) {
|
|
373
|
+
commandToRun = wrapCommandWithPty(commandToRun);
|
|
374
|
+
}
|
|
375
|
+
if (agent.mergeStderr) {
|
|
376
|
+
commandToRun = `${commandToRun} 2>&1`;
|
|
377
|
+
}
|
|
378
|
+
const canStream = typeof emitProgress === "function" || typeof agent.parseStreamingOutput === "function";
|
|
379
|
+
if (canStream) {
|
|
380
|
+
let streamedOutput = "";
|
|
381
|
+
await execLocalStreaming2("bash", ["-lc", commandToRun], {
|
|
382
|
+
timeout: effectiveTimeoutMs,
|
|
383
|
+
maxBuffer: agentMaxBuffer,
|
|
384
|
+
cwd,
|
|
385
|
+
onSpawn: (child) => {
|
|
386
|
+
run.child = child;
|
|
387
|
+
},
|
|
388
|
+
onStdout: (chunk) => {
|
|
389
|
+
streamedOutput += chunk;
|
|
390
|
+
callerOnStdout?.(chunk);
|
|
391
|
+
if (agent.parseStreamingOutput) {
|
|
392
|
+
const partial = agent.parseStreamingOutput(streamedOutput);
|
|
393
|
+
if (partial.commentaryMessages?.length) {
|
|
394
|
+
emitProgress(partial.commentaryMessages);
|
|
395
|
+
}
|
|
396
|
+
if (partial.sawFinal && partial.text) {
|
|
397
|
+
emitFinal(partial.text);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
const parsed = agent.parseOutput?.(streamedOutput) || { text: streamedOutput, sawJson: false };
|
|
403
|
+
return { text: parsed.text || streamedOutput, sawJson: parsed.sawJson, threadId: parsed.threadId, usage: parsed.usage };
|
|
404
|
+
} else {
|
|
405
|
+
const output = await execLocal2("bash", ["-lc", commandToRun], {
|
|
406
|
+
timeout: effectiveTimeoutMs,
|
|
407
|
+
maxBuffer: agentMaxBuffer,
|
|
408
|
+
cwd
|
|
409
|
+
});
|
|
410
|
+
const parsed = agent.parseOutput?.(output) || { text: output, sawJson: false };
|
|
411
|
+
if (parsed.text) {
|
|
412
|
+
emitFinal(parsed.text);
|
|
413
|
+
}
|
|
414
|
+
return { text: parsed.text || output, sawJson: parsed.sawJson, threadId: parsed.threadId, usage: parsed.usage };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
runAgentForChat,
|
|
419
|
+
stopActiveRun: async (taskId, contextKey) => {
|
|
420
|
+
const run = activeRuns.get(buildActiveRunKey(taskId, contextKey));
|
|
421
|
+
if (!run || run.settled) return { status: "idle" };
|
|
422
|
+
if (run.stopPending) return { status: "stopping" };
|
|
423
|
+
run.stopPending = true;
|
|
424
|
+
terminateChildProcess2?.(run.child, "SIGTERM");
|
|
425
|
+
return { status: "stopping" };
|
|
426
|
+
},
|
|
427
|
+
registerAgent: (agent) => {
|
|
428
|
+
if (agents.has(agent.id)) {
|
|
429
|
+
log.warn(`Agent ${agent.id} already registered, overwriting`);
|
|
430
|
+
}
|
|
431
|
+
agents.set(agent.id, agent);
|
|
432
|
+
},
|
|
433
|
+
listAgents: async () => {
|
|
434
|
+
const results = [];
|
|
435
|
+
for (const agent of agents.values()) {
|
|
436
|
+
const available = agent.checkAvailable ? await agent.checkAvailable() : true;
|
|
437
|
+
results.push({
|
|
438
|
+
id: agent.id,
|
|
439
|
+
label: agent.label,
|
|
440
|
+
defaultModel: agent.defaultModel,
|
|
441
|
+
available,
|
|
442
|
+
needsPty: agent.needsPty,
|
|
443
|
+
mergeStderr: agent.mergeStderr
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return results;
|
|
447
|
+
},
|
|
448
|
+
shutdown: () => {
|
|
449
|
+
for (const [key, run] of activeRuns) {
|
|
450
|
+
if (!run.settled && run.child) {
|
|
451
|
+
log.info(`Shutting down active agent run: ${key}`);
|
|
452
|
+
terminateChildProcess2?.(run.child, "SIGTERM");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/index.ts
|
|
460
|
+
import { createObservability } from "@agent-detective/observability";
|
|
461
|
+
import { isAbsolute, resolve } from "path";
|
|
462
|
+
import { homedir } from "os";
|
|
463
|
+
function resolveCliArgs(argv) {
|
|
464
|
+
const args = argv.slice(2);
|
|
465
|
+
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
466
|
+
return { command: "help" };
|
|
467
|
+
}
|
|
468
|
+
if (args.includes("--version") || args[0] === "version") {
|
|
469
|
+
return { command: "version" };
|
|
470
|
+
}
|
|
471
|
+
const command = args[0] === "doctor" ? "doctor" : args[0] === "init" ? "init" : args[0] === "validate-config" || args.includes("--validate-config") ? "validate-config" : "serve";
|
|
472
|
+
let configRoot;
|
|
473
|
+
for (let i = 0; i < args.length; i++) {
|
|
474
|
+
const a = args[i];
|
|
475
|
+
if (a === "--config-root") {
|
|
476
|
+
configRoot = args[i + 1];
|
|
477
|
+
i++;
|
|
478
|
+
} else if (a?.startsWith("--config-root=")) {
|
|
479
|
+
configRoot = a.slice("--config-root=".length);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return { command, configRoot };
|
|
483
|
+
}
|
|
484
|
+
function printHelp() {
|
|
485
|
+
console.log(`${APP_NAME} ${APP_VERSION}
|
|
486
|
+
|
|
487
|
+
Usage:
|
|
488
|
+
${APP_NAME} [--config-root <dir>] Start server (default)
|
|
489
|
+
${APP_NAME} doctor [--config-root <dir>] [--json] [--verbose]
|
|
490
|
+
Validate config/tools/plugins and exit
|
|
491
|
+
${APP_NAME} validate-config [--config-root <dir>] [--json] [--verbose]
|
|
492
|
+
Validate config only and exit
|
|
493
|
+
${APP_NAME} init [--config-root <dir>] [--repo-path <dir>] [--repo-name <name>]
|
|
494
|
+
[--agent <id>] [--force] [--json]
|
|
495
|
+
Scaffold config/local.json for a mock first run
|
|
496
|
+
${APP_NAME} --version Print version and exit
|
|
497
|
+
${APP_NAME} --help Print this help and exit
|
|
498
|
+
|
|
499
|
+
Config:
|
|
500
|
+
--config-root <dir> Directory containing config/ (or the config/ dir itself)
|
|
501
|
+
AGENT_DETECTIVE_CONFIG_ROOT can be used instead of --config-root
|
|
502
|
+
`);
|
|
503
|
+
}
|
|
504
|
+
function resolveInstallRoot(cliConfigRoot) {
|
|
505
|
+
const expandHome = (value) => {
|
|
506
|
+
if (value === "~") return homedir();
|
|
507
|
+
if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
|
|
508
|
+
return value;
|
|
509
|
+
};
|
|
510
|
+
if (cliConfigRoot) return resolve(expandHome(cliConfigRoot));
|
|
511
|
+
if (process.env.AGENT_DETECTIVE_CONFIG_ROOT) return resolve(expandHome(process.env.AGENT_DETECTIVE_CONFIG_ROOT));
|
|
512
|
+
return void 0;
|
|
513
|
+
}
|
|
514
|
+
function resolveConfigDirFromInstallRoot(installRoot) {
|
|
515
|
+
if (!installRoot) return void 0;
|
|
516
|
+
if (installRoot.split(/[\\/]/).pop() === "config") return installRoot;
|
|
517
|
+
return resolve(installRoot, "config");
|
|
518
|
+
}
|
|
519
|
+
async function serve(installRoot) {
|
|
520
|
+
applyLogLevelAliasForObservability();
|
|
521
|
+
const configRoot = resolveConfigDirFromInstallRoot(installRoot);
|
|
522
|
+
const configRootUsed = configRoot ?? resolve(process.cwd(), "config");
|
|
523
|
+
const config = loadConfig({ configRoot });
|
|
524
|
+
const observability = createObservability(config.observability || {});
|
|
525
|
+
const logger = observability.logger;
|
|
526
|
+
const serverLogger = logger.child("server");
|
|
527
|
+
serverLogger.info("Starting agent-detective...", {
|
|
528
|
+
agent: getAgentLabel(config.agent || "opencode"),
|
|
529
|
+
port: config.port || 3001,
|
|
530
|
+
configRoot: configRootUsed
|
|
531
|
+
});
|
|
532
|
+
const defaultModels = {};
|
|
533
|
+
let runnerConfig;
|
|
534
|
+
if (config.agents) {
|
|
535
|
+
for (const [key, value] of Object.entries(config.agents)) {
|
|
536
|
+
if (key === "runner") {
|
|
537
|
+
runnerConfig = value;
|
|
538
|
+
} else {
|
|
539
|
+
defaultModels[key] = value;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const agentRunner = createAgentRunner({
|
|
544
|
+
execLocal,
|
|
545
|
+
execLocalStreaming,
|
|
546
|
+
terminateChildProcess,
|
|
547
|
+
defaultModels,
|
|
548
|
+
agentTimeoutMs: runnerConfig?.timeoutMs,
|
|
549
|
+
agentMaxBuffer: runnerConfig?.maxBufferBytes,
|
|
550
|
+
postFinalGraceMs: runnerConfig?.postFinalGraceMs,
|
|
551
|
+
forceKillDelayMs: runnerConfig?.forceKillDelayMs,
|
|
552
|
+
logger: logger.child("agent-runner"),
|
|
553
|
+
defaultAgentId: normalizeAgent(config.agent)
|
|
554
|
+
});
|
|
555
|
+
for (const agent of listAgents()) {
|
|
556
|
+
agentRunner.registerAgent(agent);
|
|
557
|
+
}
|
|
558
|
+
const eventBus = createEventBus(logger.child("events"));
|
|
559
|
+
const memoryQueue = createMemoryTaskQueue(logger.child("task-queue"));
|
|
560
|
+
const taskQueue = config.tasks?.maxConcurrent !== void 0 && config.tasks.maxConcurrent > 0 ? createLimitedConcurrencyTaskQueue(memoryQueue, config.tasks.maxConcurrent, logger.child("task-queue")) : memoryQueue;
|
|
561
|
+
const runRecordsPath = config.runRecords?.path !== void 0 && config.runRecords.path.trim().length > 0 ? isAbsolute(config.runRecords.path) ? config.runRecords.path : resolve(configRootUsed, config.runRecords.path) : void 0;
|
|
562
|
+
const runRecords = runRecordsPath ? createRunRecordWriter(runRecordsPath, logger.child("run-records")) : void 0;
|
|
563
|
+
const pluginSystem = createPluginSystem({
|
|
564
|
+
agentRunner,
|
|
565
|
+
events: eventBus,
|
|
566
|
+
logger: logger.child("plugin-system"),
|
|
567
|
+
metrics: observability.metrics,
|
|
568
|
+
health: observability.health,
|
|
569
|
+
failOnContractErrors: config.pluginSystem?.failOnContractErrors ?? false,
|
|
570
|
+
failOnDependencyErrors: config.pluginSystem?.failOnDependencyErrors ?? true,
|
|
571
|
+
failOnPluginLoadErrors: config.pluginSystem?.failOnPluginLoadErrors ?? true,
|
|
572
|
+
pathResolutionRoot: installRoot ?? process.cwd(),
|
|
573
|
+
taskQueue
|
|
574
|
+
});
|
|
575
|
+
const enqueue = pluginSystem.enqueue;
|
|
576
|
+
const orchestrator = createOrchestrator({
|
|
577
|
+
eventBus,
|
|
578
|
+
agentRunner,
|
|
579
|
+
enqueue,
|
|
580
|
+
logger: logger.child("orchestrator"),
|
|
581
|
+
maxWallTimeMs: config.tasks?.maxWallTimeMs,
|
|
582
|
+
runRecords
|
|
583
|
+
});
|
|
584
|
+
orchestrator.start();
|
|
585
|
+
const { app } = await createServer(config, observability, defaultModels, agentRunner, enqueue, {
|
|
586
|
+
getPluginTags: () => pluginSystem.getPluginTags(),
|
|
587
|
+
getPluginStatus: () => ({
|
|
588
|
+
loaded: pluginSystem.getLoadedPlugins().map((p) => `${p.name}@${p.version}`),
|
|
589
|
+
failures: pluginSystem.getPluginLoadFailures()
|
|
590
|
+
})
|
|
591
|
+
});
|
|
592
|
+
await pluginSystem.loadAll(app, config);
|
|
593
|
+
const loaded = pluginSystem.getLoadedPlugins();
|
|
594
|
+
if (loaded.length > 0) {
|
|
595
|
+
serverLogger.info("Loaded plugins", {
|
|
596
|
+
plugins: loaded.map((p) => `${p.name}@${p.version}`)
|
|
597
|
+
});
|
|
598
|
+
} else {
|
|
599
|
+
serverLogger.info("No plugins loaded");
|
|
600
|
+
}
|
|
601
|
+
const PORT = config.port || 3001;
|
|
602
|
+
async function gracefulShutdown(signal) {
|
|
603
|
+
serverLogger.info(`Received ${signal}, shutting down...`);
|
|
604
|
+
const timeout = setTimeout(() => process.exit(1), 1e4);
|
|
605
|
+
timeout.unref();
|
|
606
|
+
await pluginSystem.shutdown();
|
|
607
|
+
await app.close();
|
|
608
|
+
agentRunner.shutdown();
|
|
609
|
+
clearTimeout(timeout);
|
|
610
|
+
process.exit(0);
|
|
611
|
+
}
|
|
612
|
+
process.on("SIGINT", () => {
|
|
613
|
+
void gracefulShutdown("SIGINT");
|
|
614
|
+
});
|
|
615
|
+
process.on("SIGTERM", () => {
|
|
616
|
+
void gracefulShutdown("SIGTERM");
|
|
617
|
+
});
|
|
618
|
+
await app.listen({ port: PORT, host: "0.0.0.0" });
|
|
619
|
+
serverLogger.info("Server started", {
|
|
620
|
+
port: PORT,
|
|
621
|
+
listeningOn: `http://localhost:${PORT}`
|
|
622
|
+
});
|
|
623
|
+
const spec = app.swagger();
|
|
624
|
+
serverLogger.info("Generated OpenAPI spec", {
|
|
625
|
+
paths: Object.keys(spec.paths ?? {}).length,
|
|
626
|
+
tags: (spec.tags ?? []).map((t) => t.name).join(", ")
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async function main() {
|
|
630
|
+
const cli = resolveCliArgs(process.argv);
|
|
631
|
+
const installRoot = resolveInstallRoot(cli.configRoot);
|
|
632
|
+
if (cli.command === "help") {
|
|
633
|
+
printHelp();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (cli.command === "version") {
|
|
637
|
+
console.log(APP_VERSION);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (cli.command === "doctor") {
|
|
641
|
+
const mod = await import("./doctor-3ZMDZLW6.js");
|
|
642
|
+
await mod.runDoctor({ installRoot, argv: process.argv });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (cli.command === "validate-config") {
|
|
646
|
+
const mod = await import("./doctor-3ZMDZLW6.js");
|
|
647
|
+
await mod.runValidateConfig({ installRoot, argv: process.argv });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (cli.command === "init") {
|
|
651
|
+
const mod = await import("./init-IPCDLNHA.js");
|
|
652
|
+
await mod.runInit({ installRoot, argv: process.argv });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
await serve(installRoot);
|
|
656
|
+
}
|
|
657
|
+
void main().catch((err) => {
|
|
658
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
659
|
+
console.error(message);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
});
|