@peanut996/acp-router 0.8.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 +350 -0
- package/dist/acp-client.d.ts +179 -0
- package/dist/acp-client.d.ts.map +1 -0
- package/dist/acp-client.js +718 -0
- package/dist/acp-client.js.map +1 -0
- package/dist/agents.d.ts +180 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +430 -0
- package/dist/agents.js.map +1 -0
- package/dist/bin/acp-router-cli.d.ts +3 -0
- package/dist/bin/acp-router-cli.d.ts.map +1 -0
- package/dist/bin/acp-router-cli.js +252 -0
- package/dist/bin/acp-router-cli.js.map +1 -0
- package/dist/bin/acp-router.d.ts +3 -0
- package/dist/bin/acp-router.d.ts.map +1 -0
- package/dist/bin/acp-router.js +7 -0
- package/dist/bin/acp-router.js.map +1 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +131 -0
- package/dist/constants.js.map +1 -0
- package/dist/jobs.d.ts +133 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +716 -0
- package/dist/jobs.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +180 -0
- package/dist/server.js.map +1 -0
- package/dist/storage.d.ts +166 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +506 -0
- package/dist/storage.js.map +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +78 -0
- package/dist/utils.js.map +1 -0
- package/package.json +55 -0
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { MAX_RECURSION_DEPTH, LOG_DIR, ACTIVE_JOB_STATUSES, TERMINAL_JOB_STATUSES, SERVER_NAME, SERVER_VERSION, COMMAND_TIMEOUT_MS } from "./constants.js";
|
|
3
|
+
import { safeEnv, clampInteger, preview, hashText, isPlainObject, createId, resolveBooleanOverride } from "./utils.js";
|
|
4
|
+
import { readConfig, readRegistry, writeRegistry, readJobEventLog, readLogTail, recordJobProcess, normalizeProcessInfo, appendJsonl } from "./storage.js";
|
|
5
|
+
import { discoverAgents, chooseAgent, validateWorktree, collectWorktreeState, isAcpRunReady, buildAcpUnavailableError, planLaunch, resolveAcpLaunchTarget } from "./agents.js";
|
|
6
|
+
import { AcpStdioClient, runAcpStdioJob } from "./acp-client.js";
|
|
7
|
+
const ACTIVE_RUNS = new Map();
|
|
8
|
+
async function createJob(args) {
|
|
9
|
+
const recursionDepth = Number.parseInt(process.env.ACP_ROUTER_DEPTH ?? "0", 10) || 0;
|
|
10
|
+
if (recursionDepth >= MAX_RECURSION_DEPTH) {
|
|
11
|
+
return {
|
|
12
|
+
status: "failed",
|
|
13
|
+
error: "recursion_limit",
|
|
14
|
+
message: `Agent Router recursion limit reached (depth=${recursionDepth}). This prevents infinite agent dispatch loops.`
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const worktreeCheck = await validateWorktree(args.worktree);
|
|
18
|
+
if (!worktreeCheck.ok) {
|
|
19
|
+
return {
|
|
20
|
+
status: "failed",
|
|
21
|
+
error: worktreeCheck.reason,
|
|
22
|
+
message: "V1 requires an existing absolute worktree path before dispatching an external agent."
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const config = await readConfig();
|
|
26
|
+
const registry = await readRegistry();
|
|
27
|
+
const mode = args.mode ?? "implementation";
|
|
28
|
+
const permissionProfile = args.permissionProfile ?? config.safety.defaultPermissionProfile;
|
|
29
|
+
if (permissionProfile === "bypassPermissions" && !config.safety.allowBypassPermissions) {
|
|
30
|
+
return {
|
|
31
|
+
status: "failed",
|
|
32
|
+
error: "bypassPermissions_disabled",
|
|
33
|
+
message: "The Agent Router config does not allow bypassPermissions by default."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const availableAgents = await discoverAgents({ includeNotInstalled: false }).then((value) => value.agents);
|
|
37
|
+
const selected = args.agent
|
|
38
|
+
? { agentId: args.agent, reason: "agent explicitly requested" }
|
|
39
|
+
: chooseAgent(availableAgents, config, mode);
|
|
40
|
+
if (!selected.agentId) {
|
|
41
|
+
return {
|
|
42
|
+
status: "failed",
|
|
43
|
+
error: "no_available_agent",
|
|
44
|
+
message: "No available agent was configured or discovered. Use discover_coding_agents first."
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const selectedAgent = availableAgents.find((agent) => agent.id === selected.agentId);
|
|
48
|
+
if (!selectedAgent || selectedAgent.status !== "available") {
|
|
49
|
+
return {
|
|
50
|
+
status: "failed",
|
|
51
|
+
error: "agent_unavailable",
|
|
52
|
+
agentId: selected.agentId,
|
|
53
|
+
message: "The requested agent is not currently available. Use discover_coding_agents to inspect status."
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const activeConflict = findActiveWorktreeJob(registry, args.worktree, permissionProfile);
|
|
57
|
+
if (activeConflict) {
|
|
58
|
+
return {
|
|
59
|
+
status: "failed",
|
|
60
|
+
error: "worktree_locked",
|
|
61
|
+
jobId: activeConflict.jobId,
|
|
62
|
+
message: "Another writable Agent Router job is already active for this worktree."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const now = new Date().toISOString();
|
|
66
|
+
const jobId = createId("job");
|
|
67
|
+
const sessionId = args.sessionId || createId(`sess_${selected.agentId}`);
|
|
68
|
+
const logPath = path.join(LOG_DIR, `${jobId}.jsonl`);
|
|
69
|
+
const worktreeState = args.collectDiff === false
|
|
70
|
+
? { skipped: true, reason: "collectDiff disabled" }
|
|
71
|
+
: await collectWorktreeState(args.worktree);
|
|
72
|
+
const launchingEnabled = resolveBooleanOverride(args.launchExternalAgents, config.safety.launchExternalAgents);
|
|
73
|
+
const inheritEnvironment = resolveBooleanOverride(args.inheritEnvironment, config.safety.inheritEnvironment);
|
|
74
|
+
const agentEnv = safeEnv({ inheritEnvironment });
|
|
75
|
+
const asyncRequested = args.async !== false;
|
|
76
|
+
if (launchingEnabled && !isAcpRunReady(selectedAgent)) {
|
|
77
|
+
return {
|
|
78
|
+
status: "failed",
|
|
79
|
+
error: "acp_required",
|
|
80
|
+
agentId: selected.agentId,
|
|
81
|
+
message: buildAcpUnavailableError(selectedAgent)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const launchPlan = planLaunch({ launchingEnabled, selectedAgent });
|
|
85
|
+
const adapterStatus = launchPlan.adapterStatus;
|
|
86
|
+
const initialStatus = launchPlan.runnable ? "running" : launchPlan.status;
|
|
87
|
+
const initialSummary = launchPlan.summary;
|
|
88
|
+
const initialRisks = launchPlan.risks;
|
|
89
|
+
const recentEvents = [
|
|
90
|
+
{
|
|
91
|
+
type: "job_created",
|
|
92
|
+
timestamp: now,
|
|
93
|
+
message: "Job recorded in local Agent Router registry."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: adapterStatus,
|
|
97
|
+
timestamp: now,
|
|
98
|
+
message: initialSummary
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
const session = registry.sessions[sessionId] ?? {
|
|
102
|
+
sessionId,
|
|
103
|
+
providerSessionId: null,
|
|
104
|
+
agentId: selected.agentId,
|
|
105
|
+
title: preview(args.prompt, 60),
|
|
106
|
+
status: "idle",
|
|
107
|
+
worktree: args.worktree,
|
|
108
|
+
createdAt: now,
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
lastJobId: null,
|
|
111
|
+
source: "dispatcher_registry",
|
|
112
|
+
canContinue: true
|
|
113
|
+
};
|
|
114
|
+
const job = {
|
|
115
|
+
jobId,
|
|
116
|
+
sessionId,
|
|
117
|
+
agentId: selected.agentId,
|
|
118
|
+
status: initialStatus,
|
|
119
|
+
worktree: args.worktree,
|
|
120
|
+
mode,
|
|
121
|
+
permissionProfile,
|
|
122
|
+
collectDiff: args.collectDiff !== false,
|
|
123
|
+
promptPreview: preview(args.prompt, 160),
|
|
124
|
+
promptHash: await hashText(args.prompt),
|
|
125
|
+
startedAt: now,
|
|
126
|
+
endedAt: initialStatus === "running" ? null : now,
|
|
127
|
+
timeoutSec: args.timeoutSec ?? 3600,
|
|
128
|
+
metadata: isPlainObject(args.metadata) ? args.metadata : {},
|
|
129
|
+
recursionDepth,
|
|
130
|
+
resultSummary: initialSummary,
|
|
131
|
+
changedFiles: [],
|
|
132
|
+
validation: [],
|
|
133
|
+
risks: initialRisks,
|
|
134
|
+
logPath,
|
|
135
|
+
adapterStatus,
|
|
136
|
+
launchExternalAgents: launchingEnabled,
|
|
137
|
+
inheritEnvironment,
|
|
138
|
+
selectionReason: selected.reason,
|
|
139
|
+
worktreeState,
|
|
140
|
+
recentEvents
|
|
141
|
+
};
|
|
142
|
+
session.updatedAt = now;
|
|
143
|
+
session.lastJobId = jobId;
|
|
144
|
+
registry.sessions[sessionId] = session;
|
|
145
|
+
registry.jobs[jobId] = job;
|
|
146
|
+
await writeRegistry(registry);
|
|
147
|
+
await appendJsonl(logPath, recentEvents.map((event) => ({ ...event, jobId, sessionId, agentId: selected.agentId })));
|
|
148
|
+
if (launchPlan.runnable) {
|
|
149
|
+
const runRequest = {
|
|
150
|
+
args,
|
|
151
|
+
job,
|
|
152
|
+
session,
|
|
153
|
+
selectedAgent,
|
|
154
|
+
timeoutSec: args.timeoutSec ?? 3600,
|
|
155
|
+
agentEnv,
|
|
156
|
+
launchKind: launchPlan.kind
|
|
157
|
+
};
|
|
158
|
+
if (asyncRequested) {
|
|
159
|
+
startBackgroundJobRun(runRequest);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
await executeAndPersistJobRun(runRequest);
|
|
163
|
+
const updatedRegistry = await readRegistry();
|
|
164
|
+
Object.assign(job, updatedRegistry.jobs[jobId] ?? job);
|
|
165
|
+
Object.assign(session, updatedRegistry.sessions[sessionId] ?? session);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
jobId,
|
|
170
|
+
sessionId,
|
|
171
|
+
agentId: selected.agentId,
|
|
172
|
+
status: job.status,
|
|
173
|
+
worktree: args.worktree,
|
|
174
|
+
startedAt: now,
|
|
175
|
+
endedAt: job.endedAt,
|
|
176
|
+
summary: job.resultSummary,
|
|
177
|
+
changedFiles: job.changedFiles,
|
|
178
|
+
validation: job.validation,
|
|
179
|
+
risks: job.risks,
|
|
180
|
+
logPath,
|
|
181
|
+
worktreeState: job.worktreeState,
|
|
182
|
+
adapterStatus: job.adapterStatus,
|
|
183
|
+
providerSessionId: session.providerSessionId,
|
|
184
|
+
stopReason: job.stopReason,
|
|
185
|
+
failureReason: job.failureReason ?? null,
|
|
186
|
+
agentErrors: job.agentErrors ?? [],
|
|
187
|
+
availableModels: job.availableModels ?? session.availableModels ?? [],
|
|
188
|
+
agentConfigOptions: job.agentConfigOptions ?? session.agentConfigOptions ?? [],
|
|
189
|
+
launchExternalAgents: job.launchExternalAgents,
|
|
190
|
+
inheritEnvironment: job.inheritEnvironment,
|
|
191
|
+
selectionReason: selected.reason,
|
|
192
|
+
message: `${selected.agentId} job recorded by Agent Router alpha. Use get_coding_agent_job to inspect it.`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function startBackgroundJobRun(runRequest) {
|
|
196
|
+
executeAndPersistJobRun(runRequest).catch(async (error) => {
|
|
197
|
+
await markJobRunCrashed(runRequest, error);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function executeAndPersistJobRun({ args, job, session, selectedAgent, timeoutSec, agentEnv, launchKind }) {
|
|
201
|
+
const controller = createRunController(job.jobId);
|
|
202
|
+
ACTIVE_RUNS.set(job.jobId, controller);
|
|
203
|
+
try {
|
|
204
|
+
if (launchKind !== "acp_stdio") {
|
|
205
|
+
throw new Error(`Unsupported launch kind: ${launchKind}. ACP is required and CLI fallback has been removed.`);
|
|
206
|
+
}
|
|
207
|
+
const runResult = await runAcpStdioJob({
|
|
208
|
+
args,
|
|
209
|
+
job,
|
|
210
|
+
session,
|
|
211
|
+
selectedAgent,
|
|
212
|
+
timeoutSec,
|
|
213
|
+
agentEnv,
|
|
214
|
+
controller
|
|
215
|
+
});
|
|
216
|
+
await persistJobRunResult({ job, session, selectedAgent, runResult });
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
ACTIVE_RUNS.delete(job.jobId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function createRunController(jobId) {
|
|
223
|
+
return {
|
|
224
|
+
jobId,
|
|
225
|
+
cancelRequested: false,
|
|
226
|
+
cancelReason: null,
|
|
227
|
+
cancelProcess: null,
|
|
228
|
+
processInfo: null,
|
|
229
|
+
async recordProcess(processInfo) {
|
|
230
|
+
const normalized = normalizeProcessInfo(processInfo);
|
|
231
|
+
if (!normalized)
|
|
232
|
+
return;
|
|
233
|
+
this.processInfo = normalized;
|
|
234
|
+
await recordJobProcess(this.jobId, normalized);
|
|
235
|
+
},
|
|
236
|
+
cancel(reason) {
|
|
237
|
+
this.cancelRequested = true;
|
|
238
|
+
this.cancelReason = reason || "Cancelled by Agent Router caller.";
|
|
239
|
+
if (typeof this.cancelProcess === "function") {
|
|
240
|
+
return Boolean(this.cancelProcess());
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function persistJobRunResult({ job, session, selectedAgent, runResult }) {
|
|
247
|
+
const registry = await readRegistry();
|
|
248
|
+
const currentJob = registry.jobs[job.jobId] ?? job;
|
|
249
|
+
const currentSession = registry.sessions[session.sessionId] ?? session;
|
|
250
|
+
const jobPatch = currentJob.status === "cancelled" && runResult.jobPatch.status !== "cancelled"
|
|
251
|
+
? {
|
|
252
|
+
...runResult.jobPatch,
|
|
253
|
+
status: "cancelled",
|
|
254
|
+
endedAt: currentJob.endedAt ?? runResult.jobPatch.endedAt,
|
|
255
|
+
resultSummary: currentJob.resultSummary ?? "Cancelled by Agent Router caller.",
|
|
256
|
+
risks: currentJob.risks ?? []
|
|
257
|
+
}
|
|
258
|
+
: runResult.jobPatch;
|
|
259
|
+
const processRecord = isPlainObject(currentJob.process) ? { ...currentJob.process } : null;
|
|
260
|
+
Object.assign(currentJob, jobPatch);
|
|
261
|
+
if (processRecord) {
|
|
262
|
+
currentJob.process = {
|
|
263
|
+
...processRecord,
|
|
264
|
+
status: currentJob.status,
|
|
265
|
+
endedAt: processRecord.endedAt ?? currentJob.endedAt ?? new Date().toISOString()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
Object.assign(currentSession, runResult.sessionPatch);
|
|
269
|
+
currentJob.recentEvents = [...(currentJob.recentEvents ?? []), ...runResult.events];
|
|
270
|
+
currentSession.updatedAt = currentJob.endedAt;
|
|
271
|
+
currentSession.lastJobId = currentJob.jobId;
|
|
272
|
+
registry.jobs[currentJob.jobId] = currentJob;
|
|
273
|
+
registry.sessions[currentSession.sessionId] = currentSession;
|
|
274
|
+
await writeRegistry(registry);
|
|
275
|
+
}
|
|
276
|
+
async function markJobRunCrashed(runRequest, error) {
|
|
277
|
+
ACTIVE_RUNS.delete(runRequest.job.jobId);
|
|
278
|
+
const failedAt = new Date().toISOString();
|
|
279
|
+
const message = `Agent Router runner crashed: ${error.message}`;
|
|
280
|
+
const runResult = {
|
|
281
|
+
events: [
|
|
282
|
+
{
|
|
283
|
+
type: "dispatcher_runner_error",
|
|
284
|
+
timestamp: failedAt,
|
|
285
|
+
message,
|
|
286
|
+
errorMessage: error.message
|
|
287
|
+
}
|
|
288
|
+
],
|
|
289
|
+
sessionPatch: {
|
|
290
|
+
status: "idle",
|
|
291
|
+
canContinue: Boolean(runRequest.session.providerSessionId)
|
|
292
|
+
},
|
|
293
|
+
jobPatch: {
|
|
294
|
+
status: "failed",
|
|
295
|
+
endedAt: failedAt,
|
|
296
|
+
failureReason: message,
|
|
297
|
+
resultSummary: message,
|
|
298
|
+
risks: ["Inspect the job log before re-running the agent."]
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
await persistJobRunResult({
|
|
302
|
+
job: runRequest.job,
|
|
303
|
+
session: runRequest.session,
|
|
304
|
+
selectedAgent: runRequest.selectedAgent,
|
|
305
|
+
runResult
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async function listJobs(args) {
|
|
309
|
+
const registry = await readRegistry();
|
|
310
|
+
const limit = args.limit ?? 50;
|
|
311
|
+
const jobs = Object.values(registry.jobs)
|
|
312
|
+
.filter((job) => !args.status || job.status === args.status)
|
|
313
|
+
.filter((job) => !args.agent || job.agentId === args.agent)
|
|
314
|
+
.filter((job) => !args.worktree || job.worktree === args.worktree)
|
|
315
|
+
.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)))
|
|
316
|
+
.slice(0, limit);
|
|
317
|
+
return { jobs };
|
|
318
|
+
}
|
|
319
|
+
async function getJob(args) {
|
|
320
|
+
const registry = await readRegistry();
|
|
321
|
+
const job = registry.jobs[args.jobId];
|
|
322
|
+
if (!job)
|
|
323
|
+
return { jobId: args.jobId, status: "not_found" };
|
|
324
|
+
return { job };
|
|
325
|
+
}
|
|
326
|
+
async function tailJobEvents(args) {
|
|
327
|
+
const registry = await readRegistry();
|
|
328
|
+
const job = registry.jobs[args.jobId];
|
|
329
|
+
if (!job) {
|
|
330
|
+
return {
|
|
331
|
+
jobId: args.jobId,
|
|
332
|
+
status: "not_found",
|
|
333
|
+
events: [],
|
|
334
|
+
nextEventIndex: null,
|
|
335
|
+
hasMore: false,
|
|
336
|
+
note: "No Agent Router job exists for this jobId."
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const limit = clampInteger(args.limit, 50, 1, 200);
|
|
340
|
+
const afterEventIndex = Number.isInteger(args.afterEventIndex) ? args.afterEventIndex : null;
|
|
341
|
+
const startIndex = afterEventIndex == null ? 0 : afterEventIndex + 1;
|
|
342
|
+
const eventLog = await readJobEventLog(job.logPath);
|
|
343
|
+
const totalEvents = eventLog.events.length;
|
|
344
|
+
const events = eventLog.events.slice(startIndex, startIndex + limit);
|
|
345
|
+
const lastReturned = events.length > 0
|
|
346
|
+
? events[events.length - 1].eventIndex
|
|
347
|
+
: afterEventIndex;
|
|
348
|
+
const result = {
|
|
349
|
+
jobId: job.jobId,
|
|
350
|
+
status: job.status,
|
|
351
|
+
agentId: job.agentId,
|
|
352
|
+
sessionId: job.sessionId,
|
|
353
|
+
adapterStatus: job.adapterStatus ?? null,
|
|
354
|
+
providerSessionId: registry.sessions[job.sessionId]?.providerSessionId ?? null,
|
|
355
|
+
failureReason: job.failureReason ?? null,
|
|
356
|
+
changedFiles: Array.isArray(job.changedFiles) ? job.changedFiles : [],
|
|
357
|
+
risks: Array.isArray(job.risks) ? job.risks : [],
|
|
358
|
+
startedAt: job.startedAt ?? null,
|
|
359
|
+
endedAt: job.endedAt ?? null,
|
|
360
|
+
logPath: job.logPath ?? null,
|
|
361
|
+
events,
|
|
362
|
+
nextEventIndex: lastReturned,
|
|
363
|
+
hasMore: startIndex + events.length < totalEvents,
|
|
364
|
+
totalEventCount: totalEvents
|
|
365
|
+
};
|
|
366
|
+
if (eventLog.note)
|
|
367
|
+
result.note = eventLog.note;
|
|
368
|
+
if (eventLog.parseErrors.length > 0)
|
|
369
|
+
result.parseErrors = eventLog.parseErrors;
|
|
370
|
+
if (args.includeLogTail === true) {
|
|
371
|
+
result.logTail = await readLogTail(job.logPath, clampInteger(args.logTailBytes, 8192, 1, 65536));
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
async function cancelJob(args) {
|
|
376
|
+
const registry = await readRegistry();
|
|
377
|
+
const job = registry.jobs[args.jobId];
|
|
378
|
+
if (!job)
|
|
379
|
+
return { jobId: args.jobId, status: "not_found" };
|
|
380
|
+
let activeProcessCancelled = false;
|
|
381
|
+
let activeProcessInfo = null;
|
|
382
|
+
if (!TERMINAL_JOB_STATUSES.has(job.status)) {
|
|
383
|
+
const activeRun = ACTIVE_RUNS.get(job.jobId);
|
|
384
|
+
if (activeRun) {
|
|
385
|
+
activeProcessInfo = activeRun.processInfo;
|
|
386
|
+
activeProcessCancelled = activeRun.cancel(args.reason || "Cancelled by Agent Router caller.");
|
|
387
|
+
}
|
|
388
|
+
job.status = "cancelled";
|
|
389
|
+
job.endedAt = new Date().toISOString();
|
|
390
|
+
job.resultSummary = args.reason || "Cancelled by Agent Router caller.";
|
|
391
|
+
if (isPlainObject(job.process) || isPlainObject(activeProcessInfo)) {
|
|
392
|
+
job.process = {
|
|
393
|
+
...(isPlainObject(job.process) ? job.process : {}),
|
|
394
|
+
...(isPlainObject(activeProcessInfo) ? activeProcessInfo : {}),
|
|
395
|
+
status: "cancelled",
|
|
396
|
+
killSignal: "SIGTERM",
|
|
397
|
+
killRequestedAt: job.endedAt,
|
|
398
|
+
killStatus: activeProcessCancelled ? "signal_sent" : "not_owned_by_current_server"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
job.recentEvents = [
|
|
402
|
+
...(job.recentEvents ?? []),
|
|
403
|
+
{
|
|
404
|
+
type: "cancelled",
|
|
405
|
+
timestamp: job.endedAt,
|
|
406
|
+
message: args.reason || "Cancelled by Agent Router caller."
|
|
407
|
+
}
|
|
408
|
+
];
|
|
409
|
+
await writeRegistry(registry);
|
|
410
|
+
await appendJsonl(job.logPath ?? "", job.recentEvents.slice(-1).map((event) => ({ ...event, jobId: job.jobId, sessionId: job.sessionId, agentId: job.agentId })));
|
|
411
|
+
}
|
|
412
|
+
return { jobId: job.jobId, status: job.status, activeProcessCancelled };
|
|
413
|
+
}
|
|
414
|
+
async function listSessions(args) {
|
|
415
|
+
const registry = await readRegistry();
|
|
416
|
+
const config = await readConfig();
|
|
417
|
+
const limit = args.limit ?? 50;
|
|
418
|
+
const localSessions = Object.values(registry.sessions)
|
|
419
|
+
.filter((session) => args.includeArchived || session.status !== "archived")
|
|
420
|
+
.filter((session) => !args.agent || session.agentId === args.agent)
|
|
421
|
+
.filter((session) => !args.worktree || session.worktree === args.worktree)
|
|
422
|
+
.map(compactSessionForList);
|
|
423
|
+
const nativeResult = await maybeListNativeSessions({ args, config, registry });
|
|
424
|
+
const sessions = mergeSessionLists({
|
|
425
|
+
localSessions,
|
|
426
|
+
nativeSessions: nativeResult.sessions,
|
|
427
|
+
limit
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
sessions,
|
|
431
|
+
nativeSessionList: nativeResult.meta
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function compactSessionForList(session) {
|
|
435
|
+
return {
|
|
436
|
+
sessionId: session.sessionId,
|
|
437
|
+
providerSessionId: session.providerSessionId ?? null,
|
|
438
|
+
agentId: session.agentId,
|
|
439
|
+
title: session.title,
|
|
440
|
+
status: session.status,
|
|
441
|
+
worktree: session.worktree,
|
|
442
|
+
createdAt: session.createdAt,
|
|
443
|
+
updatedAt: session.updatedAt,
|
|
444
|
+
lastJobId: session.lastJobId,
|
|
445
|
+
source: session.source,
|
|
446
|
+
canContinue: session.canContinue,
|
|
447
|
+
availableModelCount: Array.isArray(session.availableModels) ? session.availableModels.length : 0,
|
|
448
|
+
configOptionCount: Array.isArray(session.agentConfigOptions) ? session.agentConfigOptions.length : 0,
|
|
449
|
+
additionalDirectories: Array.isArray(session.additionalDirectories) ? session.additionalDirectories : [],
|
|
450
|
+
nativeMeta: isPlainObject(session.nativeMeta) ? session.nativeMeta : null
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async function maybeListNativeSessions({ args, config, registry }) {
|
|
454
|
+
if (config.safety.launchExternalAgents !== true) {
|
|
455
|
+
return { sessions: [], meta: { attempted: false, reason: "launch_external_agents_disabled" } };
|
|
456
|
+
}
|
|
457
|
+
if (args.worktree && !path.isAbsolute(args.worktree)) {
|
|
458
|
+
return { sessions: [], meta: { attempted: false, reason: "worktree_must_be_absolute" } };
|
|
459
|
+
}
|
|
460
|
+
const availableAgents = await discoverAgents({ includeNotInstalled: false }).then((value) => value.agents);
|
|
461
|
+
const acpAgents = availableAgents.filter((agent) => (agent.status === "available"
|
|
462
|
+
&& agent.acp?.available
|
|
463
|
+
&& (!args.agent || agent.id === args.agent)));
|
|
464
|
+
if (acpAgents.length === 0) {
|
|
465
|
+
return { sessions: [], meta: { attempted: false, reason: "no_native_acp_agent_available" } };
|
|
466
|
+
}
|
|
467
|
+
const sessions = [];
|
|
468
|
+
const agents = [];
|
|
469
|
+
const results = await Promise.allSettled(acpAgents.map((agent) => listAcpNativeSessions({
|
|
470
|
+
selectedAgent: agent,
|
|
471
|
+
worktree: args.worktree ?? null,
|
|
472
|
+
env: safeEnv({ inheritEnvironment: config.safety.inheritEnvironment === true })
|
|
473
|
+
}).then((result) => ({ agent, result }))));
|
|
474
|
+
for (const settled of results) {
|
|
475
|
+
if (settled.status === "fulfilled") {
|
|
476
|
+
const { agent, result } = settled.value;
|
|
477
|
+
sessions.push(...mapNativeSessions({
|
|
478
|
+
nativeSessions: result.sessions,
|
|
479
|
+
registry,
|
|
480
|
+
args,
|
|
481
|
+
agentId: agent.id
|
|
482
|
+
}));
|
|
483
|
+
agents.push({
|
|
484
|
+
attempted: true,
|
|
485
|
+
agentId: agent.id,
|
|
486
|
+
supported: result.supported,
|
|
487
|
+
sessionCount: result.sessions.length,
|
|
488
|
+
pages: result.pages,
|
|
489
|
+
nextCursor: result.nextCursor ?? null
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const agent = acpAgents[results.indexOf(settled)];
|
|
494
|
+
agents.push({
|
|
495
|
+
attempted: true,
|
|
496
|
+
agentId: agent.id,
|
|
497
|
+
supported: null,
|
|
498
|
+
error: settled.reason?.message ?? String(settled.reason)
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
sessions,
|
|
504
|
+
meta: {
|
|
505
|
+
attempted: true,
|
|
506
|
+
agents
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
async function listAcpNativeSessions({ selectedAgent, worktree, env }) {
|
|
511
|
+
const cwd = worktree ?? process.cwd();
|
|
512
|
+
const launchTarget = resolveAcpLaunchTarget(selectedAgent.acp, selectedAgent, cwd);
|
|
513
|
+
if (!launchTarget)
|
|
514
|
+
throw new Error(`No ACP adapter is available for ${selectedAgent.id}.`);
|
|
515
|
+
const client = new AcpStdioClient({
|
|
516
|
+
command: launchTarget.command,
|
|
517
|
+
args: launchTarget.args,
|
|
518
|
+
cwd,
|
|
519
|
+
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
520
|
+
env,
|
|
521
|
+
onEvent: () => { }
|
|
522
|
+
});
|
|
523
|
+
try {
|
|
524
|
+
await client.start();
|
|
525
|
+
const initialize = await client.request("initialize", {
|
|
526
|
+
protocolVersion: 1,
|
|
527
|
+
clientCapabilities: {},
|
|
528
|
+
clientInfo: {
|
|
529
|
+
name: SERVER_NAME,
|
|
530
|
+
title: "Agent Router",
|
|
531
|
+
version: SERVER_VERSION
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
const supported = Boolean(initialize?.agentCapabilities?.sessionCapabilities?.list);
|
|
535
|
+
if (!supported)
|
|
536
|
+
return { supported: false, sessions: [], pages: 0, nextCursor: null };
|
|
537
|
+
const sessions = [];
|
|
538
|
+
let cursor = null;
|
|
539
|
+
let pages = 0;
|
|
540
|
+
do {
|
|
541
|
+
const params = {};
|
|
542
|
+
if (worktree)
|
|
543
|
+
params.cwd = worktree;
|
|
544
|
+
if (cursor)
|
|
545
|
+
params.cursor = cursor;
|
|
546
|
+
const page = await client.request("session/list", params);
|
|
547
|
+
if (Array.isArray(page.sessions))
|
|
548
|
+
sessions.push(...page.sessions);
|
|
549
|
+
cursor = typeof page.nextCursor === "string" && page.nextCursor ? page.nextCursor : null;
|
|
550
|
+
pages += 1;
|
|
551
|
+
} while (cursor && pages < 10 && sessions.length < 500);
|
|
552
|
+
return { supported: true, sessions, pages, nextCursor: cursor };
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
client.dispose();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function mapNativeSessions({ nativeSessions, registry, args, agentId }) {
|
|
559
|
+
const localByProvider = new Map();
|
|
560
|
+
for (const session of Object.values(registry.sessions)) {
|
|
561
|
+
if (isPlainObject(session) && session.providerSessionId) {
|
|
562
|
+
localByProvider.set(session.providerSessionId, session);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const result = [];
|
|
566
|
+
for (const nativeSession of nativeSessions) {
|
|
567
|
+
if (!isPlainObject(nativeSession) || typeof nativeSession.sessionId !== "string")
|
|
568
|
+
continue;
|
|
569
|
+
const providerSessionId = nativeSession.sessionId;
|
|
570
|
+
const local = localByProvider.get(providerSessionId);
|
|
571
|
+
if (local) {
|
|
572
|
+
if (!args.includeArchived && local.status === "archived")
|
|
573
|
+
continue;
|
|
574
|
+
if (args.agent && local.agentId !== args.agent)
|
|
575
|
+
continue;
|
|
576
|
+
if (args.worktree && (nativeSession.cwd ?? local.worktree) !== args.worktree)
|
|
577
|
+
continue;
|
|
578
|
+
result.push(compactSessionForList({
|
|
579
|
+
...local,
|
|
580
|
+
title: nativeSession.title ?? local.title,
|
|
581
|
+
worktree: nativeSession.cwd ?? local.worktree,
|
|
582
|
+
updatedAt: nativeSession.updatedAt ?? local.updatedAt,
|
|
583
|
+
source: local.source === "agent_native" ? "agent_native" : "dispatcher_registry+agent_native",
|
|
584
|
+
canContinue: true,
|
|
585
|
+
additionalDirectories: nativeSession.additionalDirectories,
|
|
586
|
+
nativeMeta: nativeSession._meta
|
|
587
|
+
}));
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (args.agent && args.agent !== agentId)
|
|
591
|
+
continue;
|
|
592
|
+
if (args.worktree && nativeSession.cwd !== args.worktree)
|
|
593
|
+
continue;
|
|
594
|
+
result.push(compactSessionForList({
|
|
595
|
+
sessionId: createNativeDispatcherSessionId(agentId, providerSessionId),
|
|
596
|
+
providerSessionId,
|
|
597
|
+
agentId,
|
|
598
|
+
title: nativeSession.title ?? `Native ${agentId} session`,
|
|
599
|
+
status: "idle",
|
|
600
|
+
worktree: nativeSession.cwd ?? null,
|
|
601
|
+
createdAt: nativeSession.updatedAt ?? null,
|
|
602
|
+
updatedAt: nativeSession.updatedAt ?? null,
|
|
603
|
+
lastJobId: null,
|
|
604
|
+
source: "agent_native",
|
|
605
|
+
canContinue: true,
|
|
606
|
+
additionalDirectories: nativeSession.additionalDirectories,
|
|
607
|
+
nativeMeta: nativeSession._meta
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
function mergeSessionLists({ localSessions, nativeSessions, limit }) {
|
|
613
|
+
const byId = new Map();
|
|
614
|
+
for (const session of [...localSessions, ...nativeSessions]) {
|
|
615
|
+
if (!session?.sessionId)
|
|
616
|
+
continue;
|
|
617
|
+
byId.set(session.sessionId, { ...(byId.get(session.sessionId) ?? {}), ...session });
|
|
618
|
+
}
|
|
619
|
+
return Array.from(byId.values())
|
|
620
|
+
.sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")))
|
|
621
|
+
.slice(0, limit);
|
|
622
|
+
}
|
|
623
|
+
function createNativeDispatcherSessionId(agentId, providerSessionId) {
|
|
624
|
+
return `sess_native_${agentId}_${encodeBase64Url(providerSessionId)}`;
|
|
625
|
+
}
|
|
626
|
+
function parseNativeDispatcherSessionId(sessionId) {
|
|
627
|
+
const match = /^sess_native_([^_]+)_(.+)$/.exec(String(sessionId ?? ""));
|
|
628
|
+
if (!match)
|
|
629
|
+
return null;
|
|
630
|
+
const providerSessionId = decodeBase64Url(match[2]);
|
|
631
|
+
if (!providerSessionId)
|
|
632
|
+
return null;
|
|
633
|
+
return {
|
|
634
|
+
agentId: match[1],
|
|
635
|
+
providerSessionId
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function encodeBase64Url(value) {
|
|
639
|
+
return Buffer.from(String(value), "utf8")
|
|
640
|
+
.toString("base64")
|
|
641
|
+
.replaceAll("+", "-")
|
|
642
|
+
.replaceAll("/", "_")
|
|
643
|
+
.replace(/=+$/u, "");
|
|
644
|
+
}
|
|
645
|
+
function decodeBase64Url(value) {
|
|
646
|
+
try {
|
|
647
|
+
const base64 = String(value).replaceAll("-", "+").replaceAll("_", "/");
|
|
648
|
+
const padded = `${base64}${"=".repeat((4 - (base64.length % 4)) % 4)}`;
|
|
649
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function continueSession(args) {
|
|
656
|
+
const registry = await readRegistry();
|
|
657
|
+
let session = registry.sessions[args.sessionId];
|
|
658
|
+
if (!session) {
|
|
659
|
+
const nativeRef = parseNativeDispatcherSessionId(args.sessionId);
|
|
660
|
+
if (!nativeRef || nativeRef.agentId !== args.agent) {
|
|
661
|
+
return {
|
|
662
|
+
sessionId: args.sessionId,
|
|
663
|
+
status: "not_found",
|
|
664
|
+
message: "The dispatcher can only continue sessions already recorded in the registry or native ACP sessions returned by list_coding_agent_sessions."
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const now = new Date().toISOString();
|
|
668
|
+
session = {
|
|
669
|
+
sessionId: args.sessionId,
|
|
670
|
+
providerSessionId: nativeRef.providerSessionId,
|
|
671
|
+
agentId: args.agent,
|
|
672
|
+
title: preview(args.prompt, 60),
|
|
673
|
+
status: "idle",
|
|
674
|
+
worktree: args.worktree,
|
|
675
|
+
createdAt: now,
|
|
676
|
+
updatedAt: now,
|
|
677
|
+
lastJobId: undefined,
|
|
678
|
+
source: "agent_native",
|
|
679
|
+
canContinue: true
|
|
680
|
+
};
|
|
681
|
+
registry.sessions[session.sessionId] = session;
|
|
682
|
+
await writeRegistry(registry);
|
|
683
|
+
}
|
|
684
|
+
return createJob({
|
|
685
|
+
agent: args.agent,
|
|
686
|
+
sessionId: args.sessionId,
|
|
687
|
+
prompt: args.prompt ?? "",
|
|
688
|
+
worktree: args.worktree ?? "",
|
|
689
|
+
async: args.async,
|
|
690
|
+
launchExternalAgents: args.launchExternalAgents,
|
|
691
|
+
inheritEnvironment: args.inheritEnvironment,
|
|
692
|
+
timeoutSec: args.timeoutSec,
|
|
693
|
+
mode: "implementation",
|
|
694
|
+
permissionProfile: "bypassPermissions",
|
|
695
|
+
collectDiff: true
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
async function archiveSession(args) {
|
|
699
|
+
const registry = await readRegistry();
|
|
700
|
+
const session = registry.sessions[args.sessionId];
|
|
701
|
+
if (!session)
|
|
702
|
+
return { sessionId: args.sessionId, status: "not_found" };
|
|
703
|
+
session.status = "archived";
|
|
704
|
+
session.updatedAt = new Date().toISOString();
|
|
705
|
+
await writeRegistry(registry);
|
|
706
|
+
return { sessionId: session.sessionId, status: session.status };
|
|
707
|
+
}
|
|
708
|
+
function findActiveWorktreeJob(registry, worktree, permissionProfile) {
|
|
709
|
+
if (permissionProfile === "plan")
|
|
710
|
+
return null;
|
|
711
|
+
return Object.values(registry.jobs).find((job) => (job.worktree === worktree
|
|
712
|
+
&& job.permissionProfile !== "plan"
|
|
713
|
+
&& ACTIVE_JOB_STATUSES.has(job.status))) ?? null;
|
|
714
|
+
}
|
|
715
|
+
export { ACTIVE_RUNS, createJob, startBackgroundJobRun, executeAndPersistJobRun, createRunController, persistJobRunResult, markJobRunCrashed, listJobs, getJob, tailJobEvents, cancelJob, listSessions, compactSessionForList, maybeListNativeSessions, listAcpNativeSessions, mapNativeSessions, mergeSessionLists, createNativeDispatcherSessionId, parseNativeDispatcherSessionId, encodeBase64Url, decodeBase64Url, continueSession, archiveSession, findActiveWorktreeJob };
|
|
716
|
+
//# sourceMappingURL=jobs.js.map
|