agents-relay 1.0.27 → 1.0.28
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 +15 -2
- package/dist/planner.js +9 -3
- package/dist/reconciler.js +5 -4
- package/dist/store.js +1 -1
- package/dist/types.js +3 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { dashboardManagedJobs, InMemoryStore, GitHubStore, resolvePullRequest, listManagedGitHubJobs } from './store.js';
|
|
8
8
|
import { githubAuthContext } from './github-auth.js';
|
|
9
|
+
import { taskOverallTimeoutMs } from './types.js';
|
|
9
10
|
import { Reconciler } from './reconciler.js';
|
|
10
11
|
import { CodexAdapter, ChatGptAdapter } from './adapters.js';
|
|
11
12
|
import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
|
|
@@ -90,7 +91,7 @@ Actions:
|
|
|
90
91
|
'task update': `Update mutable durable task fields in place.
|
|
91
92
|
|
|
92
93
|
Required: --task-id ID and the normal job storage options.
|
|
93
|
-
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy.
|
|
94
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy.
|
|
94
95
|
Use --clear-routing to remove routing metadata. A model update requires --model; provider defaults to the existing provider or openai.
|
|
95
96
|
|
|
96
97
|
Example:
|
|
@@ -224,6 +225,8 @@ catch (error) {
|
|
|
224
225
|
} }
|
|
225
226
|
function priority(args, fallback = 'P2') { const value = arg(args, '--priority', fallback); if (!['P0', 'P1', 'P2', 'P3'].includes(value))
|
|
226
227
|
throw new Error('--priority must be P0, P1, P2, or P3'); return value; }
|
|
228
|
+
function storySize(args, fallback = 'M') { const value = arg(args, '--story-size', fallback).toUpperCase(); if (!['S', 'M', 'L'].includes(value))
|
|
229
|
+
throw new Error('--story-size must be S, M, or L'); return value; }
|
|
227
230
|
export function executionMode(args, fallback = 'fixed') { const value = arg(args, '--mode', arg(args, '--execution-mode', fallback)); if (value !== 'fixed' && value !== 'autonomous')
|
|
228
231
|
throw new Error('--mode must be fixed or autonomous'); return value; }
|
|
229
232
|
export function chatGptTabClosePolicy(args) {
|
|
@@ -285,10 +288,13 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
|
|
|
285
288
|
throw new Error('--output file requires --output-path');
|
|
286
289
|
const routing = { provider: arg(args, '--provider', 'openai'), model, profile: arg(args, '--profile') || undefined, reasoning: arg(args, '--reasoning') || undefined, cwd: arg(args, '--cwd') || undefined, projectId: arg(args, '--chatgpt-project') || undefined, decidedBy: 'cli', decidedAt: now };
|
|
287
290
|
const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
|
|
291
|
+
const size = storySize(args);
|
|
292
|
+
const timeoutMs = hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing.reasoning)) : taskOverallTimeoutMs(size, routing.reasoning);
|
|
288
293
|
return {
|
|
289
294
|
jobId: job.id,
|
|
290
295
|
id: arg(args, '--task-id', 'initial'),
|
|
291
296
|
priority: priority(args, job.priority ?? 'P2'),
|
|
297
|
+
storySize: size,
|
|
292
298
|
projectName: arg(args, '--project') || undefined,
|
|
293
299
|
agentName: arg(args, '--agent') || undefined,
|
|
294
300
|
parentTaskId: arg(args, '--parent') || null,
|
|
@@ -310,7 +316,7 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
|
|
|
310
316
|
threadId: null,
|
|
311
317
|
result: null,
|
|
312
318
|
error: null,
|
|
313
|
-
timeoutMs
|
|
319
|
+
timeoutMs,
|
|
314
320
|
createdAt: now,
|
|
315
321
|
updatedAt: now
|
|
316
322
|
};
|
|
@@ -335,6 +341,11 @@ async function runTaskCommand(action, args) {
|
|
|
335
341
|
}
|
|
336
342
|
if (hasArg(args, '--priority'))
|
|
337
343
|
task.priority = priority(args, task.priority ?? job.priority ?? 'P2');
|
|
344
|
+
if (hasArg(args, '--story-size')) {
|
|
345
|
+
task.storySize = storySize(args, task.storySize ?? 'M');
|
|
346
|
+
if (!hasArg(args, '--timeout'))
|
|
347
|
+
task.timeoutMs = taskOverallTimeoutMs(task.storySize, task.routing?.reasoning);
|
|
348
|
+
}
|
|
338
349
|
if (hasArg(args, '--input'))
|
|
339
350
|
task.input = arg(args, '--input');
|
|
340
351
|
if (hasArg(args, '--project'))
|
|
@@ -371,6 +382,8 @@ async function runTaskCommand(action, args) {
|
|
|
371
382
|
decidedBy: 'cli:update',
|
|
372
383
|
decidedAt: new Date().toISOString()
|
|
373
384
|
};
|
|
385
|
+
if (hasArg(args, '--reasoning') && !hasArg(args, '--timeout'))
|
|
386
|
+
task.timeoutMs = taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
|
|
374
387
|
}
|
|
375
388
|
task.updatedAt = new Date().toISOString();
|
|
376
389
|
await store.saveTask(task);
|
package/dist/planner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { taskOverallTimeoutMs } from './types.js';
|
|
5
6
|
function record(value) {
|
|
6
7
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
7
8
|
throw new Error('Planner result must be an object');
|
|
@@ -83,17 +84,22 @@ export function parsePlannerResult(value) {
|
|
|
83
84
|
if (output.kind === 'file' && (typeof output.path !== 'string' || output.path.length === 0))
|
|
84
85
|
throw new Error(`Planner next_tasks[${index}].output.path is required for file output`);
|
|
85
86
|
const priority = task.priority === undefined ? undefined : normalizePlannerPriority(task.priority, `next_tasks[${index}].priority`);
|
|
87
|
+
const sizeValue = task.storySize === undefined ? 'M' : String(task.storySize).toUpperCase();
|
|
88
|
+
if (!['S', 'M', 'L'].includes(sizeValue))
|
|
89
|
+
throw new Error('Planner next_tasks[' + index + '].storySize must be S, M, or L');
|
|
90
|
+
const storySize = sizeValue;
|
|
86
91
|
const maxAttempts = task.maxAttempts === undefined ? 3 : task.maxAttempts;
|
|
87
|
-
const
|
|
92
|
+
const route = routing(task.routing, 'next_tasks[' + index + '].routing');
|
|
93
|
+
const timeoutMs = task.timeoutMs === undefined ? taskOverallTimeoutMs(storySize, route?.reasoning) : task.timeoutMs;
|
|
88
94
|
if (typeof maxAttempts !== 'number' || !Number.isInteger(maxAttempts) || maxAttempts < 1)
|
|
89
95
|
throw new Error(`Planner next_tasks[${index}].maxAttempts is invalid`);
|
|
90
96
|
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
91
97
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
92
98
|
return {
|
|
93
|
-
id, input: task.input, priority,
|
|
99
|
+
id, input: task.input, priority, storySize,
|
|
94
100
|
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'),
|
|
95
101
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'),
|
|
96
|
-
adapter: adapter, output: output, routing:
|
|
102
|
+
adapter: adapter, output: output, routing: route,
|
|
97
103
|
continuation: task.continuation, maxAttempts, timeoutMs
|
|
98
104
|
};
|
|
99
105
|
});
|
package/dist/reconciler.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
2
3
|
import { schedule, transitionTask } from './scheduler.js';
|
|
3
4
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
4
5
|
import { eventFor } from './events.js';
|
|
@@ -203,7 +204,7 @@ export class Reconciler {
|
|
|
203
204
|
return;
|
|
204
205
|
const id = `final-review-${headSha.slice(0, 12)}`;
|
|
205
206
|
const now = new Date().toISOString();
|
|
206
|
-
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: job.tasks.filter(item => item.agentName !== 'reviewer' && !this.isSupersededTask(item)).map(item => item.id), capabilities: ['review'], adapter: this.options.reviewer?.adapter ?? 'codex', input: `Review PR #${job.prNumber} at head SHA ${headSha}. Objective: ${job.objective ?? job.description ?? job.title}. First publish task.started as the worker start acknowledgement with data.worker_owned=true, then verify the deterministic pre-review gates have passed and inspect the completed work. Emit review.started, then exactly one of review.approved, review.changes_required, or review.blocked with data.review.headSha set to ${headSha}. Do not modify code. When auto-merge is enabled, Relay will merge this exact reviewed head after review.approved.`, output: { kind: 'task_pr' }, routing: this.options.reviewer?.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 ??
|
|
207
|
+
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: job.tasks.filter(item => item.agentName !== 'reviewer' && !this.isSupersededTask(item)).map(item => item.id), capabilities: ['review'], adapter: this.options.reviewer?.adapter ?? 'codex', input: `Review PR #${job.prNumber} at head SHA ${headSha}. Objective: ${job.objective ?? job.description ?? job.title}. First publish task.started as the worker start acknowledgement with data.worker_owned=true, then verify the deterministic pre-review gates have passed and inspect the completed work. Emit review.started, then exactly one of review.approved, review.changes_required, or review.blocked with data.review.headSha set to ${headSha}. Do not modify code. When auto-merge is enabled, Relay will merge this exact reviewed head after review.approved.`, output: { kind: 'task_pr' }, routing: this.options.reviewer?.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 };
|
|
207
208
|
await this.store.appendTask(task);
|
|
208
209
|
job.review = { outcome: 'pending', headSha, taskId: id, evidence: null, updatedAt: now };
|
|
209
210
|
job.state = 'RUNNING';
|
|
@@ -301,7 +302,7 @@ export class Reconciler {
|
|
|
301
302
|
id = `planner-${number}`;
|
|
302
303
|
}
|
|
303
304
|
const now = new Date().toISOString();
|
|
304
|
-
const task = { jobId: job.id, id, kind: 'planner', agentName: 'planner', parentTaskId: latest?.id ?? null, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: plannerInput({ job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: latest?.id ?? null }), continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.plannerTimeoutMs ??
|
|
305
|
+
const task = { jobId: job.id, id, kind: 'planner', storySize: 'M', agentName: 'planner', parentTaskId: latest?.id ?? null, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: plannerInput({ job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: latest?.id ?? null }), continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now };
|
|
305
306
|
await this.store.appendTask(task);
|
|
306
307
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.created', 'queued', `Planner task ${task.id} created`, 'orchestrator'));
|
|
307
308
|
return this.store.load(job.id);
|
|
@@ -496,7 +497,7 @@ export class Reconciler {
|
|
|
496
497
|
}
|
|
497
498
|
plannerChild(job, planner, spec) {
|
|
498
499
|
const now = new Date().toISOString();
|
|
499
|
-
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, projectName: spec.projectName, agentName: spec.agentName, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], adapter: spec.adapter, input: spec.input, output: spec.output, routing: spec.routing, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ??
|
|
500
|
+
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], adapter: spec.adapter, input: spec.input, output: spec.output, routing: spec.routing, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M', spec.routing?.reasoning), createdAt: now, updatedAt: now };
|
|
500
501
|
}
|
|
501
502
|
clearExecution(taskId, executionId) {
|
|
502
503
|
const live = this.live.get(taskId);
|
|
@@ -697,5 +698,5 @@ export class Reconciler {
|
|
|
697
698
|
} }
|
|
698
699
|
}
|
|
699
700
|
function samePlannerChildDefinition(left, right) {
|
|
700
|
-
return JSON.stringify({ id: left.id, input: left.input, output: left.output, priority: left.priority, projectName: left.projectName, agentName: left.agentName, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, input: right.input, output: right.output, priority: right.priority, projectName: right.projectName, agentName: right.agentName, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
|
|
701
|
+
return JSON.stringify({ id: left.id, input: left.input, output: left.output, priority: left.priority, storySize: left.storySize ?? 'M', projectName: left.projectName, agentName: left.agentName, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, input: right.input, output: right.output, priority: right.priority, storySize: right.storySize ?? 'M', projectName: right.projectName, agentName: right.agentName, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
|
|
701
702
|
}
|
package/dist/store.js
CHANGED
|
@@ -81,7 +81,7 @@ function immutableTaskDefinition(task) {
|
|
|
81
81
|
reasoning: task.routing.reasoning, cwd: task.routing.cwd, projectId: task.routing.projectId
|
|
82
82
|
} : undefined;
|
|
83
83
|
return {
|
|
84
|
-
jobId: task.jobId, id: task.id, priority: task.priority, projectName: task.projectName, agentName: task.agentName,
|
|
84
|
+
jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName,
|
|
85
85
|
kind: task.kind ?? 'work', parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, adapter: task.adapter,
|
|
86
86
|
input: task.input, output: task.output, routing, chatgptTabClosePolicy: task.chatgptTabClosePolicy ?? 'after-start', continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
|
|
87
87
|
};
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export const STORY_SIZE_TIMEOUT_MS = { S: 120000, M: 300000, L: 900000 };
|
|
2
|
+
export function storySizeTimeoutMs(size) { return STORY_SIZE_TIMEOUT_MS[size]; }
|
|
3
|
+
export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutMs(size) * (reasoning?.toLowerCase() === 'high' ? 2 : 1); }
|
|
1
4
|
const jobTransitions = { OPEN: ['RUNNING', 'WAITING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], RUNNING: ['OPEN', 'WAITING', 'BLOCKED', 'COMPLETED', 'FAILED', 'CANCELLED'], WAITING: ['RUNNING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], BLOCKED: ['RUNNING', 'CANCELLED'], COMPLETED: ['RUNNING', 'CANCELLED'], FAILED: ['RUNNING', 'CANCELLED'], CANCELLED: ['RUNNING'] };
|
|
2
5
|
const taskTransitions = { QUEUED: ['READY', 'BLOCKED', 'CANCELLED'], READY: ['RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['WAITING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'READY'], WAITING: ['READY', 'BLOCKED', 'CANCELLED'], BLOCKED: ['READY', 'CANCELLED'], SUCCEEDED: [], FAILED: ['READY', 'CANCELLED'], CANCELLED: [] };
|
|
3
6
|
export function assertJobTransition(from, to) { if (from !== to && !jobTransitions[from].includes(to))
|