agents-relay 1.0.124 → 1.0.126
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/cli-help.js +8 -1
- package/dist/cli.js +17 -4
- package/dist/dashboard.js +9 -2
- package/dist/reconciler-review.js +4 -12
- package/dist/reconciler-runtime.js +0 -48
- package/dist/runtime-status.js +1 -1
- package/dist/runtime-summary.js +15 -0
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +1 -1
package/dist/cli-help.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const usage = 'agents-relay <job|task|blocker|context|init|record|agent-register|agent-discover|reconcile|tick|serve> [options]';
|
|
1
|
+
export const usage = 'agents-relay <job|task|blocker|context|init|record|agent-register|agent-discover|status|reconcile|tick|serve> [options]';
|
|
2
2
|
const help = {
|
|
3
3
|
root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
|
|
4
4
|
|
|
@@ -14,6 +14,7 @@ Commands:
|
|
|
14
14
|
record Backfill a completed task into durable history
|
|
15
15
|
agent-register Register an agent role and its capabilities
|
|
16
16
|
agent-discover Find registered agents matching hard filters
|
|
17
|
+
status Show live aggregate runtime status from agents-relayd
|
|
17
18
|
reconcile Run reconciliation once
|
|
18
19
|
tick Run the indexed autonomous scheduler heartbeat
|
|
19
20
|
serve Run the dashboard and reconciliation service
|
|
@@ -75,6 +76,12 @@ Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head S
|
|
|
75
76
|
'job cancel': `Cancel the durable Job and close its backing pull request.
|
|
76
77
|
|
|
77
78
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
|
|
79
|
+
status: `Show live aggregate runtime status from the running Agents Relay service.
|
|
80
|
+
|
|
81
|
+
Usage:
|
|
82
|
+
npx agents-relay status [--host HOST] [--port PORT]
|
|
83
|
+
|
|
84
|
+
Returns the same summary used by the dashboard header plus a running array with concrete RUNNING task and execution identities. Defaults to 127.0.0.1:8787.`,
|
|
78
85
|
task: `Manage durable tasks.
|
|
79
86
|
|
|
80
87
|
Usage:
|
package/dist/cli.js
CHANGED
|
@@ -601,6 +601,23 @@ async function main() {
|
|
|
601
601
|
return;
|
|
602
602
|
}
|
|
603
603
|
const bus = eventBus(args);
|
|
604
|
+
if (command === 'status') {
|
|
605
|
+
const host = arg(args, '--host', '127.0.0.1');
|
|
606
|
+
const port = Number(arg(args, '--port', String(SERVICE_DEFAULTS.port)));
|
|
607
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
608
|
+
throw new Error('--port must be an integer between 1 and 65535');
|
|
609
|
+
let response;
|
|
610
|
+
try {
|
|
611
|
+
response = await fetch(`http://${host}:${port}/api/runtime-summary`);
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
throw new Error(`Agents Relay service unavailable at ${host}:${port}: ${error instanceof Error ? error.message : String(error)}`);
|
|
615
|
+
}
|
|
616
|
+
if (!response.ok)
|
|
617
|
+
throw new Error(`Agents Relay status request failed: HTTP ${response.status}`);
|
|
618
|
+
console.log(JSON.stringify(await response.json(), null, 2));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
604
621
|
if (command === 'tick') {
|
|
605
622
|
const repo = repository(args);
|
|
606
623
|
const auth = await githubAuthContext(args);
|
|
@@ -649,10 +666,6 @@ async function main() {
|
|
|
649
666
|
console.log(`Recorded ${id}`);
|
|
650
667
|
return;
|
|
651
668
|
}
|
|
652
|
-
if (command === 'status') {
|
|
653
|
-
console.log(JSON.stringify(await store.load(job.id), null, 2));
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
669
|
if (command === 'reconcile' && !localFile) {
|
|
657
670
|
try {
|
|
658
671
|
await requestManagedReconciliation(bus, job);
|
package/dist/dashboard.js
CHANGED
|
@@ -352,8 +352,15 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
352
352
|
return;
|
|
353
353
|
}
|
|
354
354
|
if (req.url === '/api/runtime-summary') {
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
try {
|
|
356
|
+
const jobs = await discoveredJobs();
|
|
357
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
358
|
+
res.end(JSON.stringify(summarizeRuntime(jobs)));
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
res.writeHead(502);
|
|
362
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
363
|
+
}
|
|
357
364
|
return;
|
|
358
365
|
}
|
|
359
366
|
if (req.url === '/api/status') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
2
2
|
import { schedule, transitionTaskEvent, transitionJobEvent } from './scheduler.js';
|
|
3
|
-
import { implementationComplete } from './runtime-status.js';
|
|
3
|
+
import { implementationComplete, implementationTasks } from './runtime-status.js';
|
|
4
4
|
import { eventFor } from './events.js';
|
|
5
5
|
import { selectReviewer } from './reviewer-routing.js';
|
|
6
6
|
import { routeWorker, workerCandidates } from './worker-routing.js';
|
|
@@ -26,8 +26,6 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
26
26
|
async ensureFinalReview(job, prState, headSha) {
|
|
27
27
|
if (job.schedule || job.reviewEnabled === false || prState !== 'OPEN' || job.state !== 'ACTIVE' || !implementationComplete(job) || !headSha)
|
|
28
28
|
return;
|
|
29
|
-
if (!this.preReviewGatesPass(job))
|
|
30
|
-
return;
|
|
31
29
|
const review = job.review;
|
|
32
30
|
if (review?.headSha === headSha && ['started', 'approved', 'changes_required', 'blocked'].includes(review.outcome))
|
|
33
31
|
return;
|
|
@@ -38,9 +36,9 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
38
36
|
return;
|
|
39
37
|
const id = `final-review-${headSha.slice(0, 12)}`;
|
|
40
38
|
const now = new Date().toISOString();
|
|
41
|
-
const
|
|
42
|
-
const reviewerRouting = await this.selectReviewer(
|
|
43
|
-
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies:
|
|
39
|
+
const reviewGateTasks = implementationTasks(job);
|
|
40
|
+
const reviewerRouting = await this.selectReviewer(reviewGateTasks);
|
|
41
|
+
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: reviewGateTasks.map(item => item.id), capabilities: ['review'], workerSelection: reviewerRouting.selection, adapter: reviewerRouting.candidate.adapter, input: `Review existing PR #${job.prNumber} at exactly head SHA ${headSha}. Job objective: ${job.objective ?? job.description ?? job.title}. Use the selected reviewer agent contract as the authoritative review protocol.`, output: { kind: 'task_pr' }, routing: reviewerRouting.candidate.routing, continuation: null, continuationDeliveredAt: null, reviewHeadSha: headSha, state: 'QUEUED', attempt: 0, maxAttempts: this.options.reviewer?.maxAttempts ?? 2, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.reviewer?.timeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now };
|
|
44
42
|
await this.store.appendTask(task);
|
|
45
43
|
job.review = { outcome: 'pending', headSha, taskId: id, evidence: null, updatedAt: now };
|
|
46
44
|
job.updatedAt = now;
|
|
@@ -145,12 +143,6 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
145
143
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'model.selected', 'queued', `Selected ${task.routing.model} · thinking ${task.routing.reasoning ?? 'unknown'}`, 'user', { routing: task.routing }, { agent: 'relay', component: 'agents-relay' }));
|
|
146
144
|
}
|
|
147
145
|
}
|
|
148
|
-
isSupersededTask(task) {
|
|
149
|
-
return task.state === 'CANCELLED' && typeof task.error === 'string' && task.error.startsWith('Superseded ');
|
|
150
|
-
}
|
|
151
|
-
preReviewGatesPass(job) {
|
|
152
|
-
return job.tasks.filter(task => task.agentName !== 'reviewer' && !this.isSupersededTask(task)).every(task => task.state === 'SUCCEEDED');
|
|
153
|
-
}
|
|
154
146
|
async applyReviewEvent(event) {
|
|
155
147
|
const job = await this.store.load(event.job_id);
|
|
156
148
|
const task = job.tasks.find(item => item.id === event.task_id && item.agentName === 'reviewer');
|
|
@@ -22,7 +22,6 @@ export class ReconcilerRuntimeBase {
|
|
|
22
22
|
live = new Map();
|
|
23
23
|
plannerLive = new Map();
|
|
24
24
|
terminalTimers = new Map();
|
|
25
|
-
terminalExitGraceTimers = new Map();
|
|
26
25
|
continuationLive = new Set();
|
|
27
26
|
launchFailureClaims = new Set();
|
|
28
27
|
terminalWaiters = new Map();
|
|
@@ -108,9 +107,6 @@ export class ReconcilerRuntimeBase {
|
|
|
108
107
|
for (const timer of this.terminalTimers.values())
|
|
109
108
|
clearTimeout(timer);
|
|
110
109
|
this.terminalTimers.clear();
|
|
111
|
-
for (const timer of this.terminalExitGraceTimers.values())
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
this.terminalExitGraceTimers.clear();
|
|
114
110
|
for (const planner of this.plannerLive.values()) {
|
|
115
111
|
clearTimeout(planner.timer);
|
|
116
112
|
planner.controller.abort();
|
|
@@ -171,10 +167,6 @@ export class ReconcilerRuntimeBase {
|
|
|
171
167
|
if (timer)
|
|
172
168
|
clearTimeout(timer);
|
|
173
169
|
this.terminalTimers.delete(taskId);
|
|
174
|
-
const graceTimer = this.terminalExitGraceTimers.get(taskId);
|
|
175
|
-
if (graceTimer)
|
|
176
|
-
clearTimeout(graceTimer);
|
|
177
|
-
this.terminalExitGraceTimers.delete(taskId);
|
|
178
170
|
}
|
|
179
171
|
async markWorkerStarted(event) {
|
|
180
172
|
const live = this.live.get(event.task_id);
|
|
@@ -245,53 +237,13 @@ export class ReconcilerRuntimeBase {
|
|
|
245
237
|
}
|
|
246
238
|
async runtimeSettled(jobId, taskId, executionId, result, error) {
|
|
247
239
|
const live = this.live.get(taskId);
|
|
248
|
-
const liveWorkerStarted = live?.executionId === executionId && live.workerStarted;
|
|
249
240
|
if (live?.executionId === executionId)
|
|
250
241
|
live.processSettled = true;
|
|
251
242
|
const job = await this.store.load(jobId);
|
|
252
243
|
const task = job.tasks.find(item => item.id === taskId);
|
|
253
|
-
const workerStarted = liveWorkerStarted || task?.state === 'RUNNING';
|
|
254
244
|
if (!task || task.executionId !== executionId)
|
|
255
245
|
return;
|
|
256
246
|
await this.emit(eventFor(jobId, taskId, task.parentTaskId, task.adapter === 'chatgpt' ? 'task.process.async_exited' : 'task.process.exited', error ? 'failed' : 'succeeded', error ?? result?.summary ?? 'Worker runtime exited', 'orchestrator', { authoritative: false, waitingForTerminalEvent: true, processError: error }, { agent: 'relay', component: 'agents-relay' }));
|
|
257
|
-
if (task.adapter === 'codex' && !terminalExecutionClaimed(jobId, taskId, executionId))
|
|
258
|
-
this.scheduleUnexpectedExit(jobId, taskId, executionId, workerStarted === true);
|
|
259
|
-
}
|
|
260
|
-
scheduleUnexpectedExit(jobId, taskId, executionId, workerStarted) {
|
|
261
|
-
const graceMs = Math.max(1, this.options.terminalExitGraceMs ?? 1000);
|
|
262
|
-
const timer = setTimeout(() => {
|
|
263
|
-
this.terminalExitGraceTimers.delete(taskId);
|
|
264
|
-
this.detach(this.failUnexpectedExit(jobId, taskId, executionId, workerStarted), jobId, taskId, 'unexpected worker process exit');
|
|
265
|
-
}, graceMs);
|
|
266
|
-
this.terminalExitGraceTimers.set(taskId, timer);
|
|
267
|
-
}
|
|
268
|
-
async failUnexpectedExit(jobId, taskId, executionId, workerStarted) {
|
|
269
|
-
if (terminalExecutionClaimed(jobId, taskId, executionId))
|
|
270
|
-
return;
|
|
271
|
-
const current = await this.store.load(jobId);
|
|
272
|
-
const currentTask = current.tasks.find(item => item.id === taskId);
|
|
273
|
-
if (!currentTask || currentTask.executionId !== executionId || ['SUCCEEDED', 'FAILED', 'BLOCKED', 'CANCELLED'].includes(currentTask.state)) {
|
|
274
|
-
this.clearExecution(taskId, executionId);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const reason = workerStarted ? 'worker_exit_without_terminal_event' : 'worker_exit_before_start';
|
|
278
|
-
const message = workerStarted
|
|
279
|
-
? `Worker ${taskId} exited after task.started without publishing a terminal task event`
|
|
280
|
-
: `Worker ${taskId} exited before publishing task.started`;
|
|
281
|
-
const transition = await this.taskTransitions.apply(jobId, taskId, 'watchdog.failed', {
|
|
282
|
-
eligible: (_job, task) => task.executionId === executionId && !terminalExecutionClaimed(jobId, taskId, executionId),
|
|
283
|
-
mutate: (_job, task) => {
|
|
284
|
-
this.clearExecution(task.id, executionId);
|
|
285
|
-
task.error = message;
|
|
286
|
-
task.failureReason = reason;
|
|
287
|
-
task.leaseOwner = null;
|
|
288
|
-
task.leaseExpiresAt = null;
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
if (!transition?.decision.accepted)
|
|
292
|
-
return;
|
|
293
|
-
const event = eventFor(jobId, taskId, transition.task.parentTaskId, 'task.failed', 'failed', message, 'user', { reason, terminalGraceMs: this.options.terminalExitGraceMs ?? 1000, workflowEvent: 'watchdog.failed' }, { agent: 'relay', component: 'agents-relay', watchdog: true });
|
|
294
|
-
this.detach(this.emit(event), jobId, taskId, 'watchdog failure event');
|
|
295
247
|
}
|
|
296
248
|
async timeoutTask(jobId, taskId, executionId) {
|
|
297
249
|
const live = this.live.get(taskId);
|
package/dist/runtime-status.js
CHANGED
|
@@ -2,7 +2,7 @@ function isSuperseded(task) {
|
|
|
2
2
|
return task.state === 'CANCELLED' && typeof task.error === 'string' && task.error.startsWith('Superseded ');
|
|
3
3
|
}
|
|
4
4
|
export function implementationTasks(job) {
|
|
5
|
-
return job.tasks.filter(task => task.agentName !== 'reviewer' && !isSuperseded(task));
|
|
5
|
+
return job.tasks.filter(task => task.agentName !== 'reviewer' && task.agentName !== 'troubleshooter' && !isSuperseded(task));
|
|
6
6
|
}
|
|
7
7
|
export function implementationComplete(job) {
|
|
8
8
|
const tasks = implementationTasks(job);
|
package/dist/runtime-summary.js
CHANGED
|
@@ -10,6 +10,7 @@ export function summarizeRuntime(jobs) {
|
|
|
10
10
|
let chatgpt = 0;
|
|
11
11
|
let codex = 0;
|
|
12
12
|
const models = new Map();
|
|
13
|
+
const running = [];
|
|
13
14
|
for (const job of jobs) {
|
|
14
15
|
if (job.state === 'ACTIVE')
|
|
15
16
|
activeJobs += 1;
|
|
@@ -25,6 +26,19 @@ export function summarizeRuntime(jobs) {
|
|
|
25
26
|
const model = task.routing?.model;
|
|
26
27
|
if (model)
|
|
27
28
|
models.set(model, (models.get(model) ?? 0) + 1);
|
|
29
|
+
running.push({
|
|
30
|
+
repository: job.repository,
|
|
31
|
+
prNumber: job.prNumber,
|
|
32
|
+
jobId: job.id,
|
|
33
|
+
jobTitle: job.title,
|
|
34
|
+
taskId: task.id,
|
|
35
|
+
agentName: task.agentName ?? null,
|
|
36
|
+
worker: task.workerSelection?.adapter ?? task.adapter,
|
|
37
|
+
provider: task.routing?.provider ?? null,
|
|
38
|
+
model: model ?? null,
|
|
39
|
+
executionId: task.executionId,
|
|
40
|
+
leaseExpiresAt: task.leaseExpiresAt,
|
|
41
|
+
});
|
|
28
42
|
}
|
|
29
43
|
}
|
|
30
44
|
return {
|
|
@@ -34,5 +48,6 @@ export function summarizeRuntime(jobs) {
|
|
|
34
48
|
models: [...models.entries()]
|
|
35
49
|
.map(([model, count]) => ({ model, count }))
|
|
36
50
|
.sort((left, right) => right.count - left.count || left.model.localeCompare(right.model)),
|
|
51
|
+
running,
|
|
37
52
|
};
|
|
38
53
|
}
|
package/package.json
CHANGED
|
@@ -51,7 +51,7 @@ Agents Relay maps those terminal events to durable state:
|
|
|
51
51
|
|
|
52
52
|
A model-backed task must not launch without an event bus. If no terminal event arrives before the task timeout, Relay records an explicit timeout failure. A Codex process exit or ChatGPT submission receipt alone never changes the task to a terminal state.
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
Worker process exit is observational only. Relay does not perform an exit-time terminal check or synthesize task failure from process exit; semantic terminal state comes from the worker event contract or the normal task timeout.
|
|
55
55
|
|
|
56
56
|
## Managed GitHub jobs
|
|
57
57
|
|