agents-relay 1.0.58 → 1.0.60
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 +15 -2
- package/dist/cli.js +45 -18
- package/dist/markers.js +24 -2
- package/dist/pool.js +8 -30
- package/dist/reconciler.js +28 -8
- package/dist/store.js +4 -0
- package/package.json +1 -1
- package/skills/agents-relay/contracts/orchestrator-workflows.md +4 -0
- package/skills/agents-relay/contracts/task-job-contract.md +13 -0
package/README.md
CHANGED
|
@@ -7,13 +7,24 @@ Agents Relay is a generic TypeScript runtime for durable asynchronous agent jobs
|
|
|
7
7
|
For GitHub-backed work, create or adopt the PR through Agents Relay itself. This guarantees the durable job marker exists before any managed task is submitted.
|
|
8
8
|
|
|
9
9
|
~~~sh
|
|
10
|
+
cat > job.md <<'EOF'
|
|
11
|
+
# Goal
|
|
12
|
+
|
|
13
|
+
Standalone Job context. Put a Mermaid block first when a workflow/state/architecture diagram helps.
|
|
14
|
+
EOF
|
|
15
|
+
|
|
10
16
|
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main \
|
|
11
|
-
--id job-1 --title "Objective" --
|
|
17
|
+
--id job-1 --title "Objective" --objective-file job.md --mode fixed
|
|
12
18
|
|
|
13
19
|
# The orchestrator decides the initial batch separately
|
|
20
|
+
cat > task.md <<'EOF'
|
|
21
|
+
# Work
|
|
22
|
+
|
|
23
|
+
Implement this phase using the Job objective as context.
|
|
24
|
+
EOF
|
|
14
25
|
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
|
|
15
26
|
--task-id initial --adapter codex --provider openai --model MODEL --output task-pr \
|
|
16
|
-
--input
|
|
27
|
+
--input-file task.md
|
|
17
28
|
|
|
18
29
|
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
|
|
19
30
|
--task-id child --adapter codex --provider openai --model MODEL --output task-pr \
|
|
@@ -38,6 +49,8 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
|
|
|
38
49
|
|
|
39
50
|
Jobs separate durable lifecycle from derived runtime status. Durable lifecycle is `DRAFT | ACTIVE | COMPLETED | CANCELLED`: a newly created unscheduled Job is DRAFT, creating executable work activates it, and scheduled Jobs are ACTIVE because the scheduler is allowed to advance them. Runtime status (`IDLE`, `WAITING`, `QUEUED`, `RUNNING`, `BLOCKED`, `FAILED`, `REVIEWING`, `MERGE_PENDING`, `MERGED`, or `CANCELLED`) is computed from Tasks, Review, PR, and schedule facts and is never persisted as lifecycle truth.
|
|
40
51
|
|
|
52
|
+
Job objectives and Task inputs are Markdown. Prefer `--objective-file` / `--input-file` (use `-` for stdin) for durable long-form context; inline `--objective` / `--input` remain for short/simple text and are mutually exclusive with the file variants. When a diagram materially clarifies the work, put Mermaid first and then Markdown detail. A newly created Job must capture enough context to finish independently of the originating chat; its objective becomes the default PR body and objective updates keep the PR body synchronized.
|
|
53
|
+
|
|
41
54
|
Jobs default to `fixed`. `job create` persists only the durable job and never
|
|
42
55
|
creates executable tasks. The top-level orchestrator decides whether the user's
|
|
43
56
|
request should start immediately and, if so, creates the first task batch
|
package/dist/cli.js
CHANGED
|
@@ -64,15 +64,15 @@ Example:
|
|
|
64
64
|
|
|
65
65
|
Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
|
|
66
66
|
With --head, reuse/create the PR for that head. With --pr, bind an existing unmanaged PR.
|
|
67
|
-
Common: --base BRANCH (default: main), --title TEXT, --
|
|
67
|
+
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review. --objective and --objective-file are mutually exclusive. For a new --head Job, one is required and must contain enough standalone context to complete the Job without the originating chat/thread. Objective Markdown is also the default PR body; --body is an explicit override.
|
|
68
68
|
Job creation never creates executable tasks. The orchestrator decides the initial task batch separately. Re-running create against an already healthy Job is idempotent and does not mutate its metadata.
|
|
69
69
|
|
|
70
70
|
Example:
|
|
71
|
-
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective"`,
|
|
71
|
+
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective" --objective-file job.md`,
|
|
72
72
|
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
73
73
|
|
|
74
74
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
75
|
-
Mutable: --title, --objective, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options.
|
|
75
|
+
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options. Updating the objective also updates the PR body.
|
|
76
76
|
Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
|
|
77
77
|
|
|
78
78
|
Example:
|
|
@@ -109,16 +109,16 @@ Actions:
|
|
|
109
109
|
cancel Cancel a Task`,
|
|
110
110
|
'task create': `Create one durable Task under an existing Job.
|
|
111
111
|
|
|
112
|
-
Required: normal Job storage options plus --input
|
|
112
|
+
Required: normal Job storage options plus --input-file PATH|- (preferred) or --input MARKDOWN, and --output task-pr|file. --input and --input-file are mutually exclusive. Codex/ChatGPT tasks also require --model MODEL.
|
|
113
113
|
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
114
114
|
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
115
115
|
|
|
116
116
|
Example:
|
|
117
|
-
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model MODEL --output task-pr --input
|
|
117
|
+
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model MODEL --output task-pr --input-file task.md`,
|
|
118
118
|
'task update': `Update mutable durable Task fields in place.
|
|
119
119
|
|
|
120
120
|
Required: --task-id ID and the normal Job storage options.
|
|
121
|
-
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, --draft, --queue.
|
|
121
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
122
122
|
Use --clear-routing to remove routing metadata.
|
|
123
123
|
|
|
124
124
|
Example:
|
|
@@ -233,6 +233,23 @@ class ServiceEventBus {
|
|
|
233
233
|
}
|
|
234
234
|
function arg(args, name, fallback = '') { const index = args.indexOf(name); return index >= 0 ? args[index + 1] ?? fallback : fallback; }
|
|
235
235
|
function hasArg(args, name) { return args.includes(name); }
|
|
236
|
+
async function readStdinText() {
|
|
237
|
+
const chunks = [];
|
|
238
|
+
for await (const chunk of process.stdin)
|
|
239
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
240
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
241
|
+
}
|
|
242
|
+
async function textOption(args, inlineFlag, fileFlag) {
|
|
243
|
+
if (hasArg(args, inlineFlag) && hasArg(args, fileFlag))
|
|
244
|
+
throw new Error(`${inlineFlag} and ${fileFlag} are mutually exclusive`);
|
|
245
|
+
if (hasArg(args, fileFlag)) {
|
|
246
|
+
const path = arg(args, fileFlag);
|
|
247
|
+
if (!path)
|
|
248
|
+
throw new Error(`${fileFlag} requires a path or - for stdin`);
|
|
249
|
+
return path === '-' ? readStdinText() : readFile(path, 'utf8');
|
|
250
|
+
}
|
|
251
|
+
return arg(args, inlineFlag);
|
|
252
|
+
}
|
|
236
253
|
function integerArg(args, name, fallback) { if (!hasArg(args, name))
|
|
237
254
|
return fallback; const value = Number(arg(args, name)); if (!Number.isInteger(value) || value < 0)
|
|
238
255
|
throw new Error(`${name} must be a non-negative integer`); return value; }
|
|
@@ -328,15 +345,15 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
328
345
|
await syncScheduleLabels(client, repositoryName, job);
|
|
329
346
|
return { job: await store.load(options.id), pr };
|
|
330
347
|
}
|
|
331
|
-
function workerTaskFromArgs(job, args, now) {
|
|
348
|
+
async function workerTaskFromArgs(job, args, now) {
|
|
332
349
|
const model = arg(args, '--model');
|
|
333
350
|
const adapterValue = arg(args, '--adapter');
|
|
334
351
|
const adapter = (adapterValue || 'codex');
|
|
335
352
|
const outputKind = arg(args, '--output');
|
|
336
353
|
const outputPath = arg(args, '--output-path');
|
|
337
|
-
const input =
|
|
338
|
-
if (!input)
|
|
339
|
-
throw new Error('task create requires --input');
|
|
354
|
+
const input = await textOption(args, '--input', '--input-file');
|
|
355
|
+
if (!input.trim())
|
|
356
|
+
throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
|
|
340
357
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
|
|
341
358
|
throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
|
|
342
359
|
if (adapter !== 'orchestrator' && !model)
|
|
@@ -397,7 +414,7 @@ async function runTaskCommand(action, args) {
|
|
|
397
414
|
if (['COMPLETED', 'CANCELLED'].includes(job.state))
|
|
398
415
|
throw new Error(`Cannot create task on terminal job ${job.id} (${job.state})`);
|
|
399
416
|
const now = new Date().toISOString();
|
|
400
|
-
const task = workerTaskFromArgs(job, args, now);
|
|
417
|
+
const task = await workerTaskFromArgs(job, args, now);
|
|
401
418
|
task.id = id;
|
|
402
419
|
await store.appendTask(task);
|
|
403
420
|
const refreshed = await store.load(job.id);
|
|
@@ -451,8 +468,8 @@ async function runTaskCommand(action, args) {
|
|
|
451
468
|
if (!hasArg(args, '--timeout'))
|
|
452
469
|
task.timeoutMs = taskOverallTimeoutMs(task.storySize, task.routing?.reasoning);
|
|
453
470
|
}
|
|
454
|
-
if (hasArg(args, '--input'))
|
|
455
|
-
task.input =
|
|
471
|
+
if (hasArg(args, '--input') || hasArg(args, '--input-file'))
|
|
472
|
+
task.input = await textOption(args, '--input', '--input-file');
|
|
456
473
|
if (hasArg(args, '--project'))
|
|
457
474
|
task.projectName = arg(args, '--project') || undefined;
|
|
458
475
|
if (hasArg(args, '--agent'))
|
|
@@ -528,11 +545,15 @@ async function runJobCommand(action, args) {
|
|
|
528
545
|
if (!['create', 'update', 'show', 'list', 'merge', 'cancel'].includes(action))
|
|
529
546
|
throw new Error(usage);
|
|
530
547
|
const repo = repository(args);
|
|
548
|
+
let createObjective;
|
|
531
549
|
if (action === 'create') {
|
|
532
|
-
const taskOnlyFlags = ['--start', '--task-id', '--input', '--adapter', '--provider', '--model', '--profile', '--reasoning', '--story-size', '--project', '--agent', '--parent', '--deps', '--capabilities', '--attachments', '--max-attempts', '--timeout', '--cwd', '--chatgpt-project', '--chatgpt-tab-close-policy', '--output', '--output-path', '--draft', '--queue'];
|
|
550
|
+
const taskOnlyFlags = ['--start', '--task-id', '--input', '--input-file', '--adapter', '--provider', '--model', '--profile', '--reasoning', '--story-size', '--project', '--agent', '--parent', '--deps', '--capabilities', '--attachments', '--max-attempts', '--timeout', '--cwd', '--chatgpt-project', '--chatgpt-tab-close-policy', '--output', '--output-path', '--draft', '--queue'];
|
|
533
551
|
const invalid = taskOnlyFlags.find(flag => hasArg(args, flag));
|
|
534
552
|
if (invalid)
|
|
535
553
|
throw new Error(`job create does not accept task execution option ${invalid}; create the job first, then let the orchestrator create the initial plan/task batch`);
|
|
554
|
+
createObjective = await textOption(args, '--objective', '--objective-file');
|
|
555
|
+
if (!arg(args, '--pr') && !createObjective.trim())
|
|
556
|
+
throw new Error('job create with --head requires --objective-file PATH|- or --objective MARKDOWN containing the complete standalone Job context');
|
|
536
557
|
}
|
|
537
558
|
const auth = await githubAuthContext(args);
|
|
538
559
|
if (action === 'list') {
|
|
@@ -549,10 +570,11 @@ async function runJobCommand(action, args) {
|
|
|
549
570
|
if (action === 'create') {
|
|
550
571
|
if (!prNumber && !arg(args, '--head'))
|
|
551
572
|
throw new Error('job create requires either --head or --pr');
|
|
573
|
+
const objective = createObjective ?? '';
|
|
552
574
|
const result = await ensureManagedGitHubJob(auth.client, repo, auth.trustedAuthors, {
|
|
553
575
|
id,
|
|
554
576
|
title: arg(args, '--title') || undefined,
|
|
555
|
-
objective:
|
|
577
|
+
objective: objective || undefined,
|
|
556
578
|
priority: priority(args),
|
|
557
579
|
executionMode: requestedExecutionMode(args),
|
|
558
580
|
schedule: scheduleFromArgs(args),
|
|
@@ -562,7 +584,7 @@ async function runJobCommand(action, args) {
|
|
|
562
584
|
prNumber,
|
|
563
585
|
head: arg(args, '--head') || undefined,
|
|
564
586
|
base: arg(args, '--base', 'main'),
|
|
565
|
-
body: arg(args, '--body') || undefined
|
|
587
|
+
body: arg(args, '--body') || objective || undefined
|
|
566
588
|
});
|
|
567
589
|
console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
|
|
568
590
|
return;
|
|
@@ -606,8 +628,13 @@ async function runJobCommand(action, args) {
|
|
|
606
628
|
}
|
|
607
629
|
if (hasArg(args, '--title'))
|
|
608
630
|
job.title = arg(args, '--title');
|
|
609
|
-
if (hasArg(args, '--objective'))
|
|
610
|
-
|
|
631
|
+
if (hasArg(args, '--objective') || hasArg(args, '--objective-file')) {
|
|
632
|
+
const objective = await textOption(args, '--objective', '--objective-file');
|
|
633
|
+
if (!objective.trim())
|
|
634
|
+
throw new Error('Job objective cannot be empty');
|
|
635
|
+
job.objective = objective;
|
|
636
|
+
await store.updatePullRequestBody?.(objective);
|
|
637
|
+
}
|
|
611
638
|
if (hasArg(args, '--priority'))
|
|
612
639
|
job.priority = priority(args, job.priority ?? 'P2');
|
|
613
640
|
if (hasArg(args, '--mode') || hasArg(args, '--execution-mode'))
|
package/dist/markers.js
CHANGED
|
@@ -20,8 +20,30 @@ export function parseMarker(body, label) {
|
|
|
20
20
|
}
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
-
export function renderJobComment(job) {
|
|
24
|
-
|
|
23
|
+
export function renderJobComment(job) {
|
|
24
|
+
const objective = job.objective?.trim() || '_No objective recorded._';
|
|
25
|
+
return `${marker(JOB_MARKER, { ...job, tasks: undefined })}
|
|
26
|
+
# Agents Relay job: ${job.title}
|
|
27
|
+
|
|
28
|
+
- **State:** ${job.state}
|
|
29
|
+
- **PR:** #${job.prNumber}
|
|
30
|
+
|
|
31
|
+
## Objective
|
|
32
|
+
|
|
33
|
+
${objective}`;
|
|
34
|
+
}
|
|
35
|
+
export function renderTaskComment(task) {
|
|
36
|
+
const body = task.result?.summary ?? task.input;
|
|
37
|
+
return `${marker(TASK_MARKER, task)}
|
|
38
|
+
## Task: ${task.id}
|
|
39
|
+
|
|
40
|
+
- **State:** ${task.state}
|
|
41
|
+
- **Priority:** ${task.priority ?? 'P2'}
|
|
42
|
+
|
|
43
|
+
### Work
|
|
44
|
+
|
|
45
|
+
${body}`;
|
|
46
|
+
}
|
|
25
47
|
export function parseJob(body) {
|
|
26
48
|
const job = parseMarker(body, JOB_MARKER);
|
|
27
49
|
if (!job)
|
package/dist/pool.js
CHANGED
|
@@ -25,7 +25,6 @@ export class RepositoryWorkerPool {
|
|
|
25
25
|
running = false;
|
|
26
26
|
pending = false;
|
|
27
27
|
jobTargets = new Map();
|
|
28
|
-
jobQueues = new Map();
|
|
29
28
|
constructor(client, repository, trustedAuthors, concurrency, makeReconciler) {
|
|
30
29
|
this.client = client;
|
|
31
30
|
this.trustedAuthors = trustedAuthors;
|
|
@@ -34,44 +33,27 @@ export class RepositoryWorkerPool {
|
|
|
34
33
|
this.repositories = [...new Set(typeof repository === 'string' ? [repository] : repository)];
|
|
35
34
|
}
|
|
36
35
|
repositories;
|
|
37
|
-
async withJobQueue(jobId, work) {
|
|
38
|
-
const previous = this.jobQueues.get(jobId) ?? Promise.resolve();
|
|
39
|
-
const run = previous.catch(() => undefined).then(work);
|
|
40
|
-
const settled = run.then(() => undefined, () => undefined);
|
|
41
|
-
this.jobQueues.set(jobId, settled);
|
|
42
|
-
try {
|
|
43
|
-
return await run;
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
if (this.jobQueues.get(jobId) === settled)
|
|
47
|
-
this.jobQueues.delete(jobId);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
36
|
async reconcileDiscovered(discovered) {
|
|
51
37
|
for (const item of discovered)
|
|
52
38
|
this.jobTargets.set(item.job.id, { repository: item.job.repository, prNumber: item.job.prNumber });
|
|
53
39
|
// Terminal PRs must be reconciled before OPEN-job scheduling. Otherwise the
|
|
54
40
|
// OPEN-only runnable filter can strand durable jobs after GitHub merges/closes them.
|
|
55
41
|
for (const job of terminalManagedJobs(discovered)) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await this.makeReconciler(store, 0).reconcile(job.id);
|
|
59
|
-
});
|
|
42
|
+
const store = new GitHubStore(this.client, job.repository, job.prNumber, this.trustedAuthors);
|
|
43
|
+
await this.makeReconciler(store, 0).reconcile(job.id);
|
|
60
44
|
}
|
|
61
45
|
const jobs = runnableManagedJobs(discovered);
|
|
62
46
|
let capacity = Math.max(1, this.concurrency);
|
|
63
47
|
for (const job of jobs) {
|
|
64
48
|
if (capacity <= 0)
|
|
65
49
|
break;
|
|
50
|
+
const store = new GitHubStore(this.client, job.repository, job.prNumber, this.trustedAuthors);
|
|
66
51
|
const ready = job.tasks.filter(task => task.state === 'QUEUED' || (task.state === 'DRAFT' && task.dependencies.length > 0)).length;
|
|
67
52
|
const expiredRunning = job.tasks.some(task => task.state === 'RUNNING' && task.leaseExpiresAt !== null && new Date(task.leaseExpiresAt).getTime() <= Date.now());
|
|
68
53
|
if (!needsLifecycleReconciliation(job))
|
|
69
54
|
continue;
|
|
70
55
|
const allowance = Math.max(1, Math.min(capacity, ready || (expiredRunning ? 1 : 0) || 1));
|
|
71
|
-
await this.
|
|
72
|
-
const store = new GitHubStore(this.client, job.repository, job.prNumber, this.trustedAuthors);
|
|
73
|
-
await this.makeReconciler(store, allowance).reconcile(job.id);
|
|
74
|
-
});
|
|
56
|
+
await this.makeReconciler(store, allowance).reconcile(job.id);
|
|
75
57
|
capacity -= allowance;
|
|
76
58
|
}
|
|
77
59
|
}
|
|
@@ -96,10 +78,8 @@ export class RepositoryWorkerPool {
|
|
|
96
78
|
if (!this.repositories.includes(repository))
|
|
97
79
|
return;
|
|
98
80
|
this.jobTargets.set(jobId, { repository, prNumber });
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId);
|
|
102
|
-
});
|
|
81
|
+
const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors);
|
|
82
|
+
await this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId);
|
|
103
83
|
}
|
|
104
84
|
async handleTerminalEvent(event) {
|
|
105
85
|
const target = this.jobTargets.get(event.job_id);
|
|
@@ -107,10 +87,8 @@ export class RepositoryWorkerPool {
|
|
|
107
87
|
process.stderr.write(`warning: terminal event target unknown for job ${event.job_id}\n`);
|
|
108
88
|
return;
|
|
109
89
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
await this.makeReconciler(store, Math.max(1, this.concurrency)).handleEvent(event);
|
|
113
|
-
});
|
|
90
|
+
const store = new GitHubStore(this.client, target.repository, target.prNumber, this.trustedAuthors);
|
|
91
|
+
await this.makeReconciler(store, Math.max(1, this.concurrency)).handleEvent(event);
|
|
114
92
|
}
|
|
115
93
|
async watch(bus) {
|
|
116
94
|
return bus.subscribeAll(async (event) => {
|
package/dist/reconciler.js
CHANGED
|
@@ -61,6 +61,7 @@ The ChatGPT/Codex conversation or process exit is never the task result.
|
|
|
61
61
|
Before your final return, verify that a terminal event for this exact Job/Task was published.
|
|
62
62
|
If no terminal event has been published, publish task.failed before exiting.`;
|
|
63
63
|
}
|
|
64
|
+
const taskLaunchClaims = new Set();
|
|
64
65
|
export class Reconciler {
|
|
65
66
|
store;
|
|
66
67
|
options;
|
|
@@ -468,15 +469,34 @@ export class Reconciler {
|
|
|
468
469
|
await this.store.saveTask(task);
|
|
469
470
|
return;
|
|
470
471
|
}
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
472
|
+
const claimKey = `${job.repository}#${job.prNumber}:${task.id}`;
|
|
473
|
+
if (taskLaunchClaims.has(claimKey))
|
|
474
|
+
return;
|
|
475
|
+
taskLaunchClaims.add(claimKey);
|
|
476
|
+
let claimedTask = null;
|
|
477
|
+
try {
|
|
478
|
+
const freshJob = await this.store.load(job.id);
|
|
479
|
+
const freshTask = freshJob.tasks.find(item => item.id === task.id);
|
|
480
|
+
if (!freshTask || freshTask.state !== 'QUEUED' || !dependencyReady(freshTask, new Map(freshJob.tasks.map(item => [item.id, item]))))
|
|
481
|
+
return;
|
|
482
|
+
const leaseMs = Math.max(this.options.leaseMs ?? 300000, freshTask.timeoutMs);
|
|
483
|
+
const startedAt = (this.options.now ?? new Date()).getTime();
|
|
484
|
+
transitionTask(freshTask, 'RUNNING');
|
|
485
|
+
freshTask.attempt += 1;
|
|
486
|
+
freshTask.leaseOwner = this.options.owner;
|
|
487
|
+
freshTask.leaseExpiresAt = new Date(startedAt + leaseMs).toISOString();
|
|
488
|
+
freshTask.executionId = randomUUID();
|
|
489
|
+
await this.store.saveTask(freshTask);
|
|
490
|
+
claimedTask = freshTask;
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
taskLaunchClaims.delete(claimKey);
|
|
494
|
+
}
|
|
495
|
+
if (!claimedTask)
|
|
496
|
+
return;
|
|
497
|
+
task = claimedTask;
|
|
498
|
+
const startedAt = Date.now();
|
|
478
499
|
const durableExecutionId = task.executionId;
|
|
479
|
-
await this.store.saveTask(task);
|
|
480
500
|
const controller = new AbortController();
|
|
481
501
|
let execution;
|
|
482
502
|
const workerTask = { ...task, input: workerInput, routing: task.routing ? { ...task.routing, cwd: task.routing.cwd ?? executionContext.project.path } : task.routing };
|
package/dist/store.js
CHANGED
|
@@ -66,6 +66,7 @@ export class InMemoryStore {
|
|
|
66
66
|
}
|
|
67
67
|
async saveTask(task) { this.values.set(`${TASK_MARKER}:${task.jobId ?? ''}:${task.id}`, renderTaskComment(task)); }
|
|
68
68
|
async closePullRequest() { }
|
|
69
|
+
async updatePullRequestBody(_body) { }
|
|
69
70
|
async headSha() { const jobs = [...this.values.values()].map(parseJob).filter((x) => x !== null); return jobs[0]?.headSha ?? jobs[0]?.review?.headSha ?? null; }
|
|
70
71
|
async listAgents() { return [...this.values.values()].map(parseAgent).filter((x) => x !== null); }
|
|
71
72
|
async saveAgent(agent) { this.values.set(`${AGENT_MARKER}:${agent.id}`, renderAgentComment(agent)); }
|
|
@@ -477,6 +478,9 @@ export class GitHubStore {
|
|
|
477
478
|
async closePullRequest() {
|
|
478
479
|
await this.client.request('repos/' + this.repository + '/pulls/' + this.prNumber, { method: 'PATCH', body: JSON.stringify({ state: 'closed' }) });
|
|
479
480
|
}
|
|
481
|
+
async updatePullRequestBody(body) {
|
|
482
|
+
await this.client.request('repos/' + this.repository + '/pulls/' + this.prNumber, { method: 'PATCH', body: JSON.stringify({ body }) });
|
|
483
|
+
}
|
|
480
484
|
async headSha() {
|
|
481
485
|
const { owner, name } = this.repoParts();
|
|
482
486
|
const data = await this.graphql('query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){headRefOid}}}', { owner, name, number: this.prNumber });
|
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Agents Relay Orchestrator Workflows
|
|
2
2
|
|
|
3
|
+
## Job context capture
|
|
4
|
+
|
|
5
|
+
Before `job create`, the orchestrator converts the relevant discussion into a standalone Markdown objective. Prefer `--objective-file`; if a diagram materially clarifies workflow/state/architecture/dependencies, put Mermaid first. The objective must be sufficient for a fresh orchestrator/agent to complete the Job without access to the original conversation. Creating the Job is the context-compaction boundary: important decisions must be copied into durable Job context before work is delegated.
|
|
6
|
+
|
|
3
7
|
This contract defines orchestrator-owned exception workflows that sit above normal Agents Relay task/job execution.
|
|
4
8
|
|
|
5
9
|
## Hotfix workflow
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
This contract is the shared protocol for every component that creates, schedules, reviews, or executes Agents Relay jobs and tasks.
|
|
4
4
|
|
|
5
|
+
## Job and Task authoring format
|
|
6
|
+
|
|
7
|
+
Job objectives and Task inputs are **Markdown** and are durable execution context, not short labels. When the work has a workflow, state model, architecture, dependency graph, or relationship that is clearer visually, put a Mermaid block first and follow it with Markdown details. Otherwise use structured Markdown text directly.
|
|
8
|
+
|
|
9
|
+
For long-form content, use files instead of shell-quoted arguments:
|
|
10
|
+
|
|
11
|
+
- Job create/update: `--objective-file PATH` (or `--objective-file -` for stdin) is preferred. `--objective` is for short/simple content only. They are mutually exclusive.
|
|
12
|
+
- Task create/update: `--input-file PATH` (or `--input-file -` for stdin) is preferred. `--input` is for short/simple content only. They are mutually exclusive.
|
|
13
|
+
|
|
14
|
+
When creating a new Job from an active discussion, the orchestrator must consolidate **all context needed to finish the Job without the originating thread** into the Job objective before creation. Include, as applicable: goal, relevant background, decisions already made, requirements, constraints, lifecycle/workflow semantics, non-goals, implementation boundaries, acceptance criteria, delivery/review instructions, and any important known issues. Do not create a Job whose objective is only a one-line summary when the discussion contains material implementation context.
|
|
15
|
+
|
|
16
|
+
For a newly created PR (`job create --head`), standalone objective Markdown is required and becomes the default PR body. Updating the Job objective also updates the PR body. The trusted Job marker remains the machine-readable durable state, while the PR body and rendered Job comment expose the same objective to humans and agents.
|
|
17
|
+
|
|
5
18
|
## Task producers
|
|
6
19
|
|
|
7
20
|
Any task producer — orchestrator, planner, or nested task-producing agent — must follow this contract.
|