agents-relay 1.0.27 → 1.0.29
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/README.md +2 -0
- package/dist/cli.js +17 -4
- package/dist/planner.js +12 -4
- package/dist/reconciler.js +6 -5
- package/dist/store.js +1 -1
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +4 -0
- package/skills/agents-relay/agents/planner.agent.md +5 -13
- package/skills/agents-relay/contracts/task-job-contract.md +36 -0
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ autonomous runtime uses the bundled `skills/agents-relay/agents/planner.agent.md
|
|
|
38
38
|
snapshot as JSON on stdin and must return `{ "objective_status": "in_progress"|"satisfied", "assessment": "...", "next_tasks": [...] }`.
|
|
39
39
|
Planner-created tasks use stable IDs and the existing parent/subtask tree, so
|
|
40
40
|
retries and daemon restarts do not create duplicate work.
|
|
41
|
+
Task producers must follow `skills/agents-relay/contracts/task-job-contract.md`; that shared skill contract defines story size, timeout derivation, lifecycle events, review, and auto-merge semantics.
|
|
42
|
+
All task producers use the same story-size contract: `S`≈5 minutes, `M`≈15 minutes, and `L`≈45 minutes of normal-reasoning execution, with `M` as the default. `high` reasoning doubles that overall window. ChatGPT Browser Worker start-ack timeout is derived as one third of the effective overall timeout, clamped between 1 and 5 minutes. Orchestrators and planners should choose `storySize`, not raw timeout milliseconds; `--timeout` remains only as an explicit compatibility/debug override.
|
|
41
43
|
|
|
42
44
|
job create first resolves an existing open PR with the same head/base, then creates it only when needed. Re-running it is idempotent: it reuses the PR and maintains exactly one trusted agents-relay:job:v1 marker.
|
|
43
45
|
Use `--start` to create/reuse the job and submit its initial task in one invocation. Start mode requires explicit `--adapter`, `--model`, `--output`, and non-empty `--input`; the task ID defaults to `initial`. Re-running with the same task definition reuses that task and never duplicates it. Omitting `--start` preserves create-only behavior, leaving an `OPEN` job with zero tasks until a caller submits work.
|
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:
|
|
@@ -98,7 +99,7 @@ Example:
|
|
|
98
99
|
submit: `Queue a task for a managed job.
|
|
99
100
|
|
|
100
101
|
Required: --repo OWNER/REPO, --pr NUMBER, --id JOB_ID, and --input TEXT.
|
|
101
|
-
Required for worker tasks: --adapter codex|chatgpt, --model MODEL, and --output task-pr|file. File output also requires --output-path PATH.\nCommon: --task-id ID, --priority P0|P1|P2|P3, --max-attempts N. Model-backed execution requires --events nats in the running reconciler/daemon.
|
|
102
|
+
Required for worker tasks: --adapter codex|chatgpt, --model MODEL, and --output task-pr|file. File output also requires --output-path PATH.\nCommon: --task-id ID, --priority P0|P1|P2|P3, --story-size S|M|L, --max-attempts N. Story size defaults to M. Task producers follow the shared Task / Job Contract; prefer --story-size over raw --timeout. Model-backed execution requires --events nats in the running reconciler/daemon.
|
|
102
103
|
|
|
103
104
|
Example:
|
|
104
105
|
npx agents-relay submit --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model gpt-5-6 --output task-pr --input "Run the required tests and report the result through events."`,
|
|
@@ -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);
|
|
@@ -427,7 +440,7 @@ async function runJobCommand(action, args) {
|
|
|
427
440
|
console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state, task: start ? arg(args, '--task-id', 'initial') : undefined }, null, 2));
|
|
428
441
|
}
|
|
429
442
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
430
|
-
export function runtimeReviewer() { return { adapter: 'chatgpt', routing: { provider: 'chatgpt', model: 'account-defaults', decidedBy: 'fixed-chatgpt-reviewer', decidedAt: new Date().toISOString() }
|
|
443
|
+
export function runtimeReviewer() { return { adapter: 'chatgpt', routing: { provider: 'chatgpt', model: 'account-defaults', decidedBy: 'fixed-chatgpt-reviewer', decidedAt: new Date().toISOString() } }; }
|
|
431
444
|
export function runtime(store, args, bus) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, reviewer: runtimeReviewer(), eventBus: bus, emit, chatgptTabClosePolicy: policy }); }
|
|
432
445
|
export async function createService(args) {
|
|
433
446
|
const loaded = await storeFor(args);
|
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
|
});
|
|
@@ -134,6 +140,7 @@ export class CommandObjectivePlanner {
|
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
142
|
const DEFAULT_PLANNER_AGENT = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills', 'agents-relay', 'agents', 'planner.agent.md');
|
|
143
|
+
const DEFAULT_TASK_JOB_CONTRACT = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills', 'agents-relay', 'contracts', 'task-job-contract.md');
|
|
137
144
|
function parsePlannerAgentMessage(line) {
|
|
138
145
|
try {
|
|
139
146
|
const value = JSON.parse(line);
|
|
@@ -156,7 +163,8 @@ export class AgentObjectivePlanner {
|
|
|
156
163
|
}
|
|
157
164
|
async plan(context, signal) {
|
|
158
165
|
const instructions = await readFile(this.agentFile, 'utf8');
|
|
159
|
-
const
|
|
166
|
+
const contract = await readFile(DEFAULT_TASK_JOB_CONTRACT, 'utf8');
|
|
167
|
+
const prompt = instructions.trim() + '\n\n## Shared Task / Job Contract\n\n' + contract.trim() + '\n\n## Durable planner input\n\n' + plannerInput(context);
|
|
160
168
|
return await new Promise((resolve, reject) => {
|
|
161
169
|
const child = spawn(this.command, ['exec', '--json', '--', prompt], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
162
170
|
let stdoutBuffer = '';
|
package/dist/reconciler.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { storySizeTimeoutMs, taskOverallTimeoutMs, chatGptStartAckTimeoutMs } 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);
|
|
@@ -342,7 +343,7 @@ export class Reconciler {
|
|
|
342
343
|
const controller = new AbortController();
|
|
343
344
|
let execution;
|
|
344
345
|
const workerTask = { ...task, input: workerInput };
|
|
345
|
-
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, Date.now(),
|
|
346
|
+
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, Date.now(), this.options.startAckTimeoutMs ?? chatGptStartAckTimeoutMs(task.timeoutMs)) : null;
|
|
346
347
|
const closePolicy = task.chatgptTabClosePolicy ?? this.options.chatgptTabClosePolicy ?? adapter.tabClosePolicy ?? 'after-start';
|
|
347
348
|
const terminalWait = task.adapter === 'chatgpt' && closePolicy === 'after-terminal'
|
|
348
349
|
? this.terminalWait(task.id, durableExecutionId, startedAt - 1)
|
|
@@ -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,7 @@
|
|
|
1
|
+
export const STORY_SIZE_TIMEOUT_MS = { S: 300000, M: 900000, L: 2700000 };
|
|
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); }
|
|
4
|
+
export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(300000, Math.max(60000, Math.floor(overallTimeoutMs / 3))); }
|
|
1
5
|
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
6
|
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
7
|
export function assertJobTransition(from, to) { if (from !== to && !jobTransitions[from].includes(to))
|
package/package.json
CHANGED
|
@@ -7,6 +7,10 @@ description: Create and operate durable asynchronous agent jobs through GitHub P
|
|
|
7
7
|
|
|
8
8
|
Use Agents Relay when an orchestrator needs durable delegated work that survives the current process.
|
|
9
9
|
|
|
10
|
+
## Required shared contract
|
|
11
|
+
|
|
12
|
+
Before creating, planning, reviewing, or managing a job/task, read and follow `contracts/task-job-contract.md`. This is the canonical producer/runtime protocol for story size, lifecycle events, review, and auto-merge.
|
|
13
|
+
|
|
10
14
|
## Core task contract
|
|
11
15
|
|
|
12
16
|
A managed child task is always a descriptive unit of work. It says what an agent must accomplish; it is never a shell command.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
You are the default objective planner for Agents Relay autonomous jobs.
|
|
4
4
|
|
|
5
|
+
Before producing tasks, read and follow `../contracts/task-job-contract.md`.
|
|
6
|
+
|
|
5
7
|
Inspect the durable objective and task tree, then decide whether the objective is satisfied or which descriptive agent tasks should run next.
|
|
6
8
|
|
|
7
9
|
## Rules
|
|
@@ -12,21 +14,11 @@ Inspect the durable objective and task tree, then decide whether the objective i
|
|
|
12
14
|
- Preserve parent/subtask causality and stable IDs.
|
|
13
15
|
- A child task describes what an agent must accomplish. Never emit a shell command as a worker task.
|
|
14
16
|
- Every child task must use adapter `codex` or `chatgpt`.
|
|
15
|
-
- Every child task must declare exactly one output:
|
|
16
|
-
- `{"kind":"task_pr"}`, or
|
|
17
|
-
- `{"kind":"file","path":"..."}`.
|
|
17
|
+
- Every child task must declare exactly one output: `task_pr` or `file`.
|
|
18
18
|
- Include routing metadata for every model-backed child task.
|
|
19
|
-
-
|
|
20
|
-
- If a child task includes `priority`, emit the canonical string `P0`, `P1`, `P2`, or `P3` exactly. Do not emit numeric priorities, lowercase aliases, or labels such as `high`/`medium`/`low`.
|
|
19
|
+
- Every child task must include `storySize` according to the shared Task / Job Contract. Use `M` when uncertain.
|
|
21
20
|
- Return no prose outside the JSON result.
|
|
22
21
|
|
|
23
22
|
## Output
|
|
24
23
|
|
|
25
|
-
Return exactly one JSON object with
|
|
26
|
-
- objective_status: "in_progress" or "satisfied"
|
|
27
|
-
- assessment: a short evidence-based assessment
|
|
28
|
-
- next_tasks: an array of typed task objects
|
|
29
|
-
|
|
30
|
-
Each next task contains: id, input, adapter, output, routing, and may also contain priority, projectName, agentName, dependencies, capabilities, maxAttempts, and timeoutMs.
|
|
31
|
-
|
|
32
|
-
When the objective is satisfied, next_tasks must be empty.
|
|
24
|
+
Return exactly one JSON object with `objective_status`, `assessment`, and `next_tasks`. Each next task contains id, input, adapter, output, routing, storySize, and may also contain priority, projectName, agentName, dependencies, capabilities, maxAttempts, and timeoutMs. When the objective is satisfied, next_tasks must be empty.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Agents Relay Task / Job Contract
|
|
2
|
+
|
|
3
|
+
This contract is the shared protocol for every component that creates, schedules, reviews, or executes Agents Relay jobs and tasks.
|
|
4
|
+
|
|
5
|
+
## Task producers
|
|
6
|
+
|
|
7
|
+
Any task producer — orchestrator, planner, or nested task-producing agent — must follow this contract.
|
|
8
|
+
Task producers choose semantic task properties such as objective, dependencies, adapter/routing, priority, and `storySize`. They do not normally choose raw timeout milliseconds.
|
|
9
|
+
|
|
10
|
+
## Story size
|
|
11
|
+
|
|
12
|
+
Every worker task has one of three story sizes:
|
|
13
|
+
|
|
14
|
+
- `S`: about 5 minutes of normal-reasoning execution.
|
|
15
|
+
- `M`: about 15 minutes of normal-reasoning execution. This is the default when size is absent or uncertain.
|
|
16
|
+
- `L`: about 45 minutes of normal-reasoning execution.
|
|
17
|
+
|
|
18
|
+
Relay derives the overall execution timeout from `storySize`. If `routing.reasoning` is `high`, Relay doubles the overall execution timeout. Other reasoning levels do not change it.
|
|
19
|
+
|
|
20
|
+
For ChatGPT Browser Worker tasks, Relay derives start-ack timeout from the effective overall timeout:
|
|
21
|
+
|
|
22
|
+
`startAckTimeout = clamp(overallTimeout / 3, 1 minute, 5 minutes)`
|
|
23
|
+
|
|
24
|
+
This applies equally to normal ChatGPT tasks and reviewer tasks. Raw `timeoutMs` is a compatibility/debug/hotfix override, not the normal task-production interface.
|
|
25
|
+
|
|
26
|
+
## Worker lifecycle
|
|
27
|
+
|
|
28
|
+
Relay owns process lifecycle. Workers own semantic task lifecycle. A worker must publish canonical `task.started` as its first execution action, then exactly one of `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. Process launch/exit events do not substitute for worker semantic events.
|
|
29
|
+
|
|
30
|
+
## Job and review lifecycle
|
|
31
|
+
|
|
32
|
+
A normal task never merges a PR. After all non-superseded work gates pass, Relay creates a SHA-bound final review task. The reviewer emits exactly one of `review.approved`, `review.changes_required`, or `review.blocked`. When `autoMerge` is enabled (default), Relay merges only after `review.approved`, pinned to the exact reviewed head SHA. A new task or new PR head makes earlier final-review evidence stale and requires a fresh review.
|
|
33
|
+
|
|
34
|
+
## Fixed and autonomous jobs
|
|
35
|
+
|
|
36
|
+
`fixed` jobs execute the orchestrator-provided task set and then proceed to final review. `autonomous` jobs may ask the planner for additional task batches. Planner-produced tasks must obey this same contract, including explicit `storySize`.
|