agents-relay 1.0.53 → 1.0.54
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.js +1 -1
- package/dist/reconciler.js +10 -10
- package/dist/scheduler.js +4 -4
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -411,7 +411,7 @@ async function runTaskCommand(action, args) {
|
|
|
411
411
|
if (action === 'retry' || action === 'cancel') {
|
|
412
412
|
if (action === 'retry')
|
|
413
413
|
task.maxAttempts = extendRetryBudget(task);
|
|
414
|
-
task.state = action === 'retry' ? '
|
|
414
|
+
task.state = action === 'retry' ? 'QUEUED' : 'CANCELLED';
|
|
415
415
|
task.error = action === 'retry' ? null : 'Cancelled by user';
|
|
416
416
|
task.updatedAt = new Date().toISOString();
|
|
417
417
|
await store.saveTask(task);
|
package/dist/reconciler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { storySizeTimeoutMs, taskOverallTimeoutMs, chatGptStartAckTimeoutMs } from './types.js';
|
|
3
|
-
import { schedule, transitionTask } from './scheduler.js';
|
|
3
|
+
import { dependencyReady, schedule, transitionTask } from './scheduler.js';
|
|
4
4
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
5
5
|
import { eventFor } from './events.js';
|
|
6
6
|
import { parsePlannerResult, plannerInput } from './planner.js';
|
|
@@ -160,7 +160,7 @@ export class Reconciler {
|
|
|
160
160
|
for (const task of scheduled.tasks.filter(item => item.state === 'SUCCEEDED' && (item.continuation || scheduled.continuation) && !item.continuationDeliveredAt))
|
|
161
161
|
this.detach(this.deliverContinuation(scheduled, task), scheduled.id, task.id, 'continuation');
|
|
162
162
|
const capacity = Math.max(0, (this.options.maxConcurrent ?? 4) - this.live.size - this.plannerLive.size);
|
|
163
|
-
for (const task of scheduled.tasks.filter(t => t.state === '
|
|
163
|
+
for (const task of scheduled.tasks.filter(t => t.state === 'QUEUED' && dependencyReady(t, new Map(scheduled.tasks.map(item => [item.id, item])))).sort((a, b) => compareTaskPriority(a, b, scheduled.priority ?? 'P2')).slice(0, capacity)) {
|
|
164
164
|
if (task.kind === 'planner')
|
|
165
165
|
await this.launchPlanner(scheduled, task);
|
|
166
166
|
else if (task.adapter === 'orchestrator') {
|
|
@@ -199,7 +199,7 @@ export class Reconciler {
|
|
|
199
199
|
return;
|
|
200
200
|
job.review = { ...job.review, outcome: 'stale', updatedAt: new Date().toISOString() };
|
|
201
201
|
const staleTask = job.review.taskId ? job.tasks.find(task => task.id === job.review?.taskId) : undefined;
|
|
202
|
-
if (staleTask && ['QUEUED', '
|
|
202
|
+
if (staleTask && ['QUEUED', 'RUNNING', 'WAITING'].includes(staleTask.state)) {
|
|
203
203
|
this.live.get(staleTask.id)?.execution.cancel();
|
|
204
204
|
transitionTask(staleTask, 'CANCELLED');
|
|
205
205
|
staleTask.error = 'Review evidence became stale after the PR head changed';
|
|
@@ -218,7 +218,7 @@ export class Reconciler {
|
|
|
218
218
|
if (review?.headSha === headSha && ['started', 'approved', 'changes_required', 'blocked'].includes(review.outcome))
|
|
219
219
|
return;
|
|
220
220
|
const reviewTasks = job.tasks.filter(task => task.agentName === 'reviewer' && task.reviewHeadSha === headSha);
|
|
221
|
-
if (reviewTasks.some(task => ['QUEUED', '
|
|
221
|
+
if (reviewTasks.some(task => ['QUEUED', 'RUNNING', 'WAITING'].includes(task.state)))
|
|
222
222
|
return;
|
|
223
223
|
if (reviewTasks.some(task => task.state === 'SUCCEEDED' && review?.headSha === headSha && review.outcome === 'approved'))
|
|
224
224
|
return;
|
|
@@ -235,7 +235,7 @@ export class Reconciler {
|
|
|
235
235
|
await this.emit(eventFor(job.id, id, null, 'review.created', 'queued', `Final review task ${id} created for ${headSha} using ${reviewerRouting.reason}`, 'orchestrator', { headSha, agentName: 'reviewer', reviewerRouting: { workerId: reviewerRouting.selection.workerId, reason: reviewerRouting.reason, selectedBy: reviewerRouting.selection.selectedBy } }));
|
|
236
236
|
const ready = schedule(await this.store.load(job.id));
|
|
237
237
|
await this.store.saveJob(ready);
|
|
238
|
-
const reviewTask = ready.tasks.find(item => item.id === id && item.state === '
|
|
238
|
+
const reviewTask = ready.tasks.find(item => item.id === id && item.state === 'QUEUED');
|
|
239
239
|
if (reviewTask && (this.options.maxConcurrent ?? 4) > this.live.size + this.plannerLive.size)
|
|
240
240
|
await this.launch(ready, reviewTask);
|
|
241
241
|
}
|
|
@@ -270,7 +270,7 @@ export class Reconciler {
|
|
|
270
270
|
async routeExecutableTasks(job) {
|
|
271
271
|
const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
|
|
272
272
|
const candidates = workerCandidates(this.options.adapters, await this.store.listAgents(), usage);
|
|
273
|
-
for (const task of job.tasks.filter(item => item.kind !== 'planner' && item.agentName !== 'reviewer' && item.adapter !== 'orchestrator' && ['QUEUED', '
|
|
273
|
+
for (const task of job.tasks.filter(item => item.kind !== 'planner' && item.agentName !== 'reviewer' && item.adapter !== 'orchestrator' && ['QUEUED', 'BLOCKED'].includes(item.state) && item.output && !item.workerSelection && (item.workerRequirements || item.planningSource === 'model-planner'))) {
|
|
274
274
|
const requirements = task.workerRequirements ?? { capabilities: task.capabilities.length > 0 ? task.capabilities : ['model'] };
|
|
275
275
|
const routed = selectWorker(candidates, requirements);
|
|
276
276
|
task.workerSelection = routed.selection;
|
|
@@ -365,7 +365,7 @@ export class Reconciler {
|
|
|
365
365
|
this.plannerLive.delete(task.id);
|
|
366
366
|
}
|
|
367
367
|
if (task.attempt < task.maxAttempts) {
|
|
368
|
-
transitionTask(task, '
|
|
368
|
+
transitionTask(task, 'QUEUED');
|
|
369
369
|
task.leaseOwner = null;
|
|
370
370
|
task.leaseExpiresAt = null;
|
|
371
371
|
await this.store.saveTask(task);
|
|
@@ -391,7 +391,7 @@ export class Reconciler {
|
|
|
391
391
|
const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
|
|
392
392
|
const scopedTasks = Number.isFinite(slot) ? job.tasks.filter(task => Date.parse(task.createdAt) >= slot) : job.tasks;
|
|
393
393
|
const planners = scopedTasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
394
|
-
if (planners.some(task => ['QUEUED', '
|
|
394
|
+
if (planners.some(task => ['QUEUED', 'RUNNING', 'WAITING'].includes(task.state)))
|
|
395
395
|
return job;
|
|
396
396
|
if (scopedTasks.some(task => task.state === 'FAILED' || task.state === 'BLOCKED'))
|
|
397
397
|
return job;
|
|
@@ -598,7 +598,7 @@ export class Reconciler {
|
|
|
598
598
|
task.error = error.message;
|
|
599
599
|
task.leaseOwner = null;
|
|
600
600
|
task.leaseExpiresAt = null;
|
|
601
|
-
transitionTask(task, task.attempt < task.maxAttempts ? '
|
|
601
|
+
transitionTask(task, task.attempt < task.maxAttempts ? 'QUEUED' : 'FAILED');
|
|
602
602
|
await this.store.saveTask(task);
|
|
603
603
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, task.state === 'FAILED' ? 'planner.failed' : 'planner.retry', task.state === 'FAILED' ? 'failed' : 'queued', `Planner ${task.id}: ${error.message}`, 'user'));
|
|
604
604
|
this.detach(this.reconcile(jobId), jobId, task.id, 'planner retry reconciliation');
|
|
@@ -636,7 +636,7 @@ export class Reconciler {
|
|
|
636
636
|
task.error = message;
|
|
637
637
|
task.leaseOwner = null;
|
|
638
638
|
task.leaseExpiresAt = null;
|
|
639
|
-
transitionTask(task, task.attempt < task.maxAttempts ? '
|
|
639
|
+
transitionTask(task, task.attempt < task.maxAttempts ? 'QUEUED' : 'FAILED');
|
|
640
640
|
await this.store.saveTask(task);
|
|
641
641
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.failed', 'failed', `Planner ${task.id}: ${message}`, 'user'));
|
|
642
642
|
}
|
package/dist/scheduler.js
CHANGED
|
@@ -33,11 +33,11 @@ export function schedule(job) {
|
|
|
33
33
|
const map = new Map(job.tasks.map(t => [t.id, t]));
|
|
34
34
|
for (const task of scopedTasks) {
|
|
35
35
|
if (['QUEUED', 'WAITING'].includes(task.state) && dependencyReady(task, map))
|
|
36
|
-
transitionTask(task, '
|
|
36
|
+
transitionTask(task, 'QUEUED');
|
|
37
37
|
}
|
|
38
|
-
const active = scopedTasks.some(t => ['
|
|
38
|
+
const active = scopedTasks.some(t => ['QUEUED', 'RUNNING'].includes(t.state));
|
|
39
39
|
const blocked = scopedTasks.some(t => t.state === 'BLOCKED');
|
|
40
|
-
const waiting = scopedTasks.some(t =>
|
|
40
|
+
const waiting = scopedTasks.some(t => t.state === 'WAITING');
|
|
41
41
|
const failed = scopedTasks.some(t => t.state === 'FAILED');
|
|
42
42
|
const cancelled = scopedTasks.every(t => t.state === 'CANCELLED');
|
|
43
43
|
const next = active ? 'RUNNING' : failed ? 'FAILED' : blocked ? 'BLOCKED' : waiting ? 'WAITING' : cancelled ? 'CANCELLED' : completionGateSatisfied(job) ? 'COMPLETED' : 'WAITING';
|
|
@@ -65,7 +65,7 @@ export function extendRetryBudget(task, windowSize = 3) {
|
|
|
65
65
|
const usedOrBudgeted = Math.max(task.maxAttempts, task.attempt);
|
|
66
66
|
return Math.max(windowSize, (Math.floor(usedOrBudgeted / windowSize) + 1) * windowSize);
|
|
67
67
|
}
|
|
68
|
-
export function transitionTask(task, state) { assertTaskTransition(task.state, state); task.state = state; if (['
|
|
68
|
+
export function transitionTask(task, state) { assertTaskTransition(task.state, state); task.state = state; if (['QUEUED', 'RUNNING', 'SUCCEEDED'].includes(state))
|
|
69
69
|
task.error = null; task.updatedAt = new Date().toISOString(); }
|
|
70
70
|
export const SCHEDULE_RESOLUTION_MS = 600_000;
|
|
71
71
|
export const RUN_BUCKET_LABEL_COUNT = 144;
|
package/dist/types.js
CHANGED
|
@@ -3,7 +3,7 @@ export function storySizeTimeoutMs(size) { return STORY_SIZE_TIMEOUT_MS[size]; }
|
|
|
3
3
|
export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutMs(size) * (reasoning?.toLowerCase() === 'high' ? 2 : 1); }
|
|
4
4
|
export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(600000, Math.max(120000, Math.floor(overallTimeoutMs / 3))); }
|
|
5
5
|
const jobTransitions = { OPEN: ['RUNNING', 'WAITING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], RUNNING: ['OPEN', 'WAITING', 'BLOCKED', 'COMPLETED', 'FAILED', 'CANCELLED'], WAITING: ['RUNNING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], BLOCKED: ['RUNNING', 'COMPLETED', 'CANCELLED'], COMPLETED: ['RUNNING', 'CANCELLED'], FAILED: ['RUNNING', 'COMPLETED', 'CANCELLED'], CANCELLED: ['RUNNING', 'COMPLETED'] };
|
|
6
|
-
const taskTransitions = { QUEUED: ['
|
|
6
|
+
const taskTransitions = { QUEUED: ['RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['WAITING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'QUEUED'], WAITING: ['QUEUED', 'BLOCKED', 'CANCELLED'], BLOCKED: ['QUEUED', 'CANCELLED'], SUCCEEDED: [], FAILED: ['QUEUED', 'RUNNING', 'CANCELLED'], CANCELLED: [] };
|
|
7
7
|
export function assertJobTransition(from, to) { if (from !== to && !jobTransitions[from].includes(to))
|
|
8
8
|
throw new Error(`Invalid job transition ${from} -> ${to}`); }
|
|
9
9
|
export function assertTaskTransition(from, to) { if (from !== to && !taskTransitions[from].includes(to))
|