agents-relay 1.0.50 → 1.0.52
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 +9 -10
- package/dist/cli.js +87 -88
- package/dist/planner.js +0 -4
- package/dist/pool.js +1 -1
- package/dist/reconciler.js +2 -2
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +10 -12
- package/skills/agents-relay/agents/planner.agent.md +19 -11
- package/skills/agents-relay/contracts/orchestrator-workflows.md +2 -2
- package/skills/agents-relay/contracts/task-job-contract.md +12 -3
package/README.md
CHANGED
|
@@ -11,11 +11,11 @@ npx agents-relay job create --repo OWNER/REPO --head feat/example --base main \
|
|
|
11
11
|
--id job-1 --title "Objective" --body "PR description" --mode fixed
|
|
12
12
|
|
|
13
13
|
# The orchestrator decides the initial batch separately
|
|
14
|
-
npx agents-relay
|
|
14
|
+
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
|
|
15
15
|
--task-id initial --adapter codex --provider openai --model MODEL --output task-pr \
|
|
16
16
|
--input "Implement the objective and publish a terminal event."
|
|
17
17
|
|
|
18
|
-
npx agents-relay
|
|
18
|
+
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
|
|
19
19
|
--task-id child --adapter codex --provider openai --model MODEL --output task-pr \
|
|
20
20
|
--input "Implement the described child task and publish a terminal event."
|
|
21
21
|
|
|
@@ -33,9 +33,8 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
|
|
|
33
33
|
|
|
34
34
|
Jobs default to `fixed`. `job create` persists only the durable job and never
|
|
35
35
|
creates executable tasks. The top-level orchestrator decides whether the user's
|
|
36
|
-
request should start immediately and, if so,
|
|
37
|
-
separately.
|
|
38
|
-
the provenance root for those tasks. A zero-task autonomous job remains idle;
|
|
36
|
+
request should start immediately and, if so, creates the first task batch
|
|
37
|
+
separately. Direct orchestrator-created tasks are persisted directly and are not wrapped in a synthetic Plan. A zero-task autonomous job remains idle;
|
|
39
38
|
reconciliation does not invent its first planner. After an autonomous job has
|
|
40
39
|
been explicitly started, later planning rounds may extend the work. Fixed jobs
|
|
41
40
|
never grow another planning round.
|
|
@@ -55,7 +54,7 @@ A repository heartbeat invokes `tick` at 10-minute resolution. See
|
|
|
55
54
|
[Scheduled autonomous jobs](docs/scheduled-autonomous-jobs.md) for the exact
|
|
56
55
|
operational contract.
|
|
57
56
|
|
|
58
|
-
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. Job creation is always create-only. Task execution options belong to `
|
|
57
|
+
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. Job creation is always create-only. Task execution options belong to `task create`, and the orchestrator may create zero, one, or multiple initial Tasks according to user intent.
|
|
59
58
|
|
|
60
59
|
To create a Job on an existing unmanaged PR, use the same `job create` action with `--pr` instead of `--head`:
|
|
61
60
|
|
|
@@ -71,7 +70,7 @@ npx agents-relay job update --repo OWNER/REPO --pr 6 --id job-6 --priority P0 --
|
|
|
71
70
|
|
|
72
71
|
`job update` validates the trusted Job marker first. Missing, malformed, duplicated, mismatched, or conflicting bindings fail with a precise diagnostic and are not repaired implicitly. The orchestrator handles exceptional repair explicitly according to the Job contract, then retries the update. `job show` reads one Job and `job list` lists managed Jobs in a repository.
|
|
73
72
|
|
|
74
|
-
Managed GitHub
|
|
73
|
+
Managed GitHub task create/update/show/list/retry/cancel and reconcile/serve commands also require a durable Job marker. Orchestrators do not need raw GitHub commands for normal Job creation or metadata updates: Agents Relay uses its authenticated GitHub client internally.
|
|
75
74
|
|
|
76
75
|
The original init command remains supported for backward compatibility:
|
|
77
76
|
|
|
@@ -91,7 +90,7 @@ For explicit local demo/test mode:
|
|
|
91
90
|
npm install
|
|
92
91
|
npm run build
|
|
93
92
|
node dist/cli.js init --file .agents-relay.json --title "Dogfood" --id demo
|
|
94
|
-
node dist/cli.js
|
|
93
|
+
node dist/cli.js task create --file .agents-relay.json --task-id hello --adapter codex --provider openai --model MODEL --output task-pr --input "Complete the described task"
|
|
95
94
|
node dist/cli.js reconcile --file .agents-relay.json
|
|
96
95
|
npx agents-relay serve --file .agents-relay.json
|
|
97
96
|
~~~
|
|
@@ -143,7 +142,7 @@ For Codex and ChatGPT, **events are authoritative task state**. Event publicatio
|
|
|
143
142
|
The ChatGPT adapter uses the `chatgpt-browser-worker` path selected by the shared execution-context resolver; Relay does not bundle its own copy. The resolver applies project-local > Neo shared > global `~/.agents` precedence at execution time, so durable tasks store names/intent rather than stale absolute skill paths. It opens a fresh Temporary Chat tab, uses the account defaults, submits the prompt, verifies acceptance, and applies `--chatgpt-tab-close-policy` (or `AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY`): `after-start` by default, `never` for test/debug, or `after-terminal` after a new exact-task terminal event. It never closes a user tab and does not poll for the assistant response, resume/reopen a thread, or use conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
|
|
144
143
|
|
|
145
144
|
~~~sh
|
|
146
|
-
npx agents-relay
|
|
145
|
+
npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
|
|
147
146
|
--task-id research-ui --adapter chatgpt \
|
|
148
147
|
--capabilities model,chatgpt,browser-harness \
|
|
149
148
|
--provider openai --model MODEL \
|
|
@@ -153,4 +152,4 @@ npx agents-relay submit --repo OWNER/REPO --pr 5 --id job-1 \
|
|
|
153
152
|
|
|
154
153
|
GitHub PR state is authoritative for terminal lifecycle: a merged PR reconciles its managed job to `COMPLETED`; a closed, unmerged PR reconciles to `CANCELLED` and cannot launch queued work. The dashboard exposes `/api/jobs` and shows repository-wide managed PR state beside each durable Agents Relay job state so stale markers are visible instead of being mistaken for current truth.
|
|
155
154
|
|
|
156
|
-
For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be
|
|
155
|
+
For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be created as a durable Task before execution. Managed jobs also retain the GitHub PR body as the job description for dashboard display.
|
package/dist/cli.js
CHANGED
|
@@ -14,12 +14,12 @@ import { NatsEventBus, eventFor } from './events.js';
|
|
|
14
14
|
import { serveDashboard } from './dashboard.js';
|
|
15
15
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
16
|
import { discoverAgents } from './registry.js';
|
|
17
|
-
import { AgentObjectivePlanner, CommandObjectivePlanner
|
|
17
|
+
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
18
18
|
import { extendRetryBudget, parseJobSchedule } from './scheduler.js';
|
|
19
19
|
import { dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
|
|
20
20
|
import { resolveExecutionContext } from './execution-context.js';
|
|
21
21
|
import { defaultWorkspaceRoot, resolveManagedProjectPath } from './workspace.js';
|
|
22
|
-
const usage = 'agents-relay <job|task|context|init|
|
|
22
|
+
const usage = 'agents-relay <job|task|context|init|record|agent-register|agent-discover|reconcile|tick|serve> [options]';
|
|
23
23
|
const help = {
|
|
24
24
|
root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
|
|
25
25
|
|
|
@@ -28,16 +28,14 @@ Usage:
|
|
|
28
28
|
|
|
29
29
|
Commands:
|
|
30
30
|
job create|update|show|list Manage durable GitHub-backed jobs
|
|
31
|
-
task update
|
|
31
|
+
task create|update|show|list|retry|cancel Manage durable tasks
|
|
32
32
|
context resolve Resolve shared project instructions, skills, agents, and runtimes
|
|
33
33
|
init Create a local demo job or initialize a GitHub job
|
|
34
|
-
submit Queue a task for a managed job
|
|
35
34
|
record Backfill a completed task into durable history
|
|
36
35
|
agent-register Register an agent role and its capabilities
|
|
37
36
|
agent-discover Find registered agents matching hard filters
|
|
38
37
|
reconcile Run reconciliation once
|
|
39
38
|
tick Run the indexed autonomous scheduler heartbeat
|
|
40
|
-
retry|cancel Change a task state
|
|
41
39
|
serve Run the dashboard and reconciliation service
|
|
42
40
|
|
|
43
41
|
Run 'npx agents-relay <command> --help' for command options and examples.
|
|
@@ -89,21 +87,46 @@ Required: --repo OWNER/REPO.
|
|
|
89
87
|
|
|
90
88
|
Example:
|
|
91
89
|
npx agents-relay job list --repo OWNER/REPO`,
|
|
92
|
-
task: `Manage
|
|
90
|
+
task: `Manage durable tasks.
|
|
93
91
|
|
|
94
92
|
Usage:
|
|
95
|
-
npx agents-relay task update [options]
|
|
93
|
+
npx agents-relay task <create|update|show|list|retry|cancel> [options]
|
|
96
94
|
|
|
97
95
|
Actions:
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
create Create one executable Task under an existing Job
|
|
97
|
+
update Update mutable Task fields
|
|
98
|
+
show Show one Task
|
|
99
|
+
list List Tasks for a Job
|
|
100
|
+
retry Retry a failed or blocked Task
|
|
101
|
+
cancel Cancel a Task`,
|
|
102
|
+
'task create': `Create one executable Task under an existing Job.
|
|
103
|
+
|
|
104
|
+
Required: normal Job storage options plus --input TEXT and --output task-pr|file. Codex/ChatGPT tasks also require --model MODEL.
|
|
105
|
+
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID.
|
|
106
|
+
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
100
107
|
|
|
101
|
-
|
|
108
|
+
Example:
|
|
109
|
+
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 "Implement the requested change."`,
|
|
110
|
+
'task update': `Update mutable durable Task fields in place.
|
|
111
|
+
|
|
112
|
+
Required: --task-id ID and the normal Job storage options.
|
|
102
113
|
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.
|
|
103
|
-
Use --clear-routing to remove routing metadata.
|
|
114
|
+
Use --clear-routing to remove routing metadata.
|
|
104
115
|
|
|
105
116
|
Example:
|
|
106
117
|
npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter chatgpt --provider chatgpt --model gpt-5.6-sol --reasoning high`,
|
|
118
|
+
'task show': `Show one durable Task.
|
|
119
|
+
|
|
120
|
+
Required: --task-id ID and the normal Job storage options.`,
|
|
121
|
+
'task list': `List durable Tasks for one Job.
|
|
122
|
+
|
|
123
|
+
Required: the normal Job storage options.`,
|
|
124
|
+
'task retry': `Retry one durable Task.
|
|
125
|
+
|
|
126
|
+
Required: --task-id ID and the normal Job storage options.`,
|
|
127
|
+
'task cancel': `Cancel one durable Task.
|
|
128
|
+
|
|
129
|
+
Required: --task-id ID and the normal Job storage options.`,
|
|
107
130
|
context: `Resolve the shared execution context used by XChat and Relay.
|
|
108
131
|
|
|
109
132
|
Usage:
|
|
@@ -119,13 +142,6 @@ Root overrides for testing/configuration: --workspace-root PATH, --neo-root PATH
|
|
|
119
142
|
|
|
120
143
|
Example:
|
|
121
144
|
npx agents-relay context resolve --project-path ~/Workspace/agents-relay --role orchestrator`,
|
|
122
|
-
submit: `Queue a task for a managed job.
|
|
123
|
-
|
|
124
|
-
Required: --repo OWNER/REPO, --pr NUMBER, --id JOB_ID, and --input TEXT.
|
|
125
|
-
Required for worker tasks: --adapter codex|chatgpt|orchestrator and --output task-pr|file. Codex/ChatGPT also require --model MODEL. 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.
|
|
126
|
-
|
|
127
|
-
Example:
|
|
128
|
-
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."`,
|
|
129
145
|
record: `Backfill work completed outside the relay into durable history.
|
|
130
146
|
|
|
131
147
|
Required: --file PATH or --repo/--pr/--id, plus --summary TEXT.
|
|
@@ -173,18 +189,6 @@ Common: --capabilities a,b, --runtime NAME, --availability available,busy, --tas
|
|
|
173
189
|
|
|
174
190
|
Example:
|
|
175
191
|
npx agents-relay agent-discover --file .agents-relay.json --capabilities shell`,
|
|
176
|
-
retry: `Retry a task by setting it READY.
|
|
177
|
-
|
|
178
|
-
Required: --task-id ID and the normal job storage options.
|
|
179
|
-
|
|
180
|
-
Example:
|
|
181
|
-
npx agents-relay retry --repo OWNER/REPO --pr 12 --id job-1 --task-id build`,
|
|
182
|
-
cancel: `Cancel a task.
|
|
183
|
-
|
|
184
|
-
Required: --task-id ID and the normal job storage options.
|
|
185
|
-
|
|
186
|
-
Example:
|
|
187
|
-
npx agents-relay cancel --repo OWNER/REPO --pr 12 --id job-1 --task-id build`
|
|
188
192
|
};
|
|
189
193
|
export function helpText(command, action) {
|
|
190
194
|
if (command === 'job')
|
|
@@ -316,19 +320,6 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
316
320
|
await syncScheduleLabels(client, repositoryName, job);
|
|
317
321
|
return { job: await store.load(options.id), pr };
|
|
318
322
|
}
|
|
319
|
-
async function appendPlannedTask(store, job, task) {
|
|
320
|
-
const existing = job.tasks.find(item => item.id === task.id);
|
|
321
|
-
if (existing)
|
|
322
|
-
return existing;
|
|
323
|
-
const decisionId = `plan-${task.id}`;
|
|
324
|
-
const decision = orchestratorPlanningDecision(job, [task], decisionId, task.parentTaskId, job.tasks.filter(item => item.kind === 'planner').length, 'orchestrator');
|
|
325
|
-
if (!job.tasks.some(item => item.id === decision.id))
|
|
326
|
-
await store.appendTask(decision);
|
|
327
|
-
const planned = { ...task, parentTaskId: decision.id, planningDecisionId: decision.id, planningSource: 'orchestrator', planningRound: decision.planningRound };
|
|
328
|
-
await store.appendTask(planned);
|
|
329
|
-
job.tasks.push(planned, decision);
|
|
330
|
-
return planned;
|
|
331
|
-
}
|
|
332
323
|
function workerTaskFromArgs(job, args, now) {
|
|
333
324
|
const model = arg(args, '--model');
|
|
334
325
|
const adapterValue = arg(args, '--adapter');
|
|
@@ -336,12 +327,14 @@ function workerTaskFromArgs(job, args, now) {
|
|
|
336
327
|
const outputKind = arg(args, '--output');
|
|
337
328
|
const outputPath = arg(args, '--output-path');
|
|
338
329
|
const input = arg(args, '--input');
|
|
330
|
+
if (!input)
|
|
331
|
+
throw new Error('task create requires --input');
|
|
339
332
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
|
|
340
|
-
throw new Error('
|
|
333
|
+
throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
|
|
341
334
|
if (adapter !== 'orchestrator' && !model)
|
|
342
|
-
throw new Error('
|
|
335
|
+
throw new Error('task create requires --model for codex/chatgpt tasks');
|
|
343
336
|
if (outputKind !== 'task-pr' && outputKind !== 'file')
|
|
344
|
-
throw new Error('
|
|
337
|
+
throw new Error('task create requires --output task-pr|file');
|
|
345
338
|
if (outputKind === 'file' && !outputPath)
|
|
346
339
|
throw new Error('--output file requires --output-path');
|
|
347
340
|
const routing = adapter === 'orchestrator' ? undefined : { 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 };
|
|
@@ -381,15 +374,57 @@ function workerTaskFromArgs(job, args, now) {
|
|
|
381
374
|
};
|
|
382
375
|
}
|
|
383
376
|
async function runTaskCommand(action, args) {
|
|
384
|
-
if (
|
|
377
|
+
if (!['create', 'update', 'show', 'list', 'retry', 'cancel'].includes(action))
|
|
385
378
|
throw new Error(usage);
|
|
386
379
|
const { store, job, localFile } = await storeFor(args);
|
|
387
|
-
|
|
380
|
+
if (action === 'list') {
|
|
381
|
+
console.log(JSON.stringify(job.tasks, null, 2));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const id = arg(args, '--task-id', action === 'create' ? randomUUID() : '');
|
|
388
385
|
if (!id)
|
|
389
|
-
throw new Error(
|
|
386
|
+
throw new Error(`task ${action} requires --task-id`);
|
|
387
|
+
if (action === 'create') {
|
|
388
|
+
const now = new Date().toISOString();
|
|
389
|
+
const task = workerTaskFromArgs(job, args, now);
|
|
390
|
+
task.id = id;
|
|
391
|
+
await store.appendTask(task);
|
|
392
|
+
const refreshed = await store.load(job.id);
|
|
393
|
+
if (refreshed.state !== 'RUNNING') {
|
|
394
|
+
refreshed.state = 'RUNNING';
|
|
395
|
+
refreshed.updatedAt = now;
|
|
396
|
+
await store.saveJob(refreshed);
|
|
397
|
+
}
|
|
398
|
+
const durableTask = (await store.load(job.id)).tasks.find(item => item.id === task.id) ?? task;
|
|
399
|
+
await saveLocal(localFile, await store.load(job.id));
|
|
400
|
+
await publishWake(eventBus(args), refreshed, task.id, `Task ${task.id} created`);
|
|
401
|
+
console.log(JSON.stringify(durableTask, null, 2));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
390
404
|
const task = job.tasks.find(item => item.id === id);
|
|
391
405
|
if (!task)
|
|
392
406
|
throw new Error(`Task ${id} not found`);
|
|
407
|
+
if (action === 'show') {
|
|
408
|
+
console.log(JSON.stringify(task, null, 2));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (action === 'retry' || action === 'cancel') {
|
|
412
|
+
if (action === 'retry')
|
|
413
|
+
task.maxAttempts = extendRetryBudget(task);
|
|
414
|
+
task.state = action === 'retry' ? 'READY' : 'CANCELLED';
|
|
415
|
+
task.error = action === 'retry' ? null : 'Cancelled by user';
|
|
416
|
+
task.updatedAt = new Date().toISOString();
|
|
417
|
+
await store.saveTask(task);
|
|
418
|
+
if (action === 'retry' && job.state !== 'RUNNING') {
|
|
419
|
+
job.state = 'RUNNING';
|
|
420
|
+
job.updatedAt = task.updatedAt;
|
|
421
|
+
await store.saveJob(job);
|
|
422
|
+
}
|
|
423
|
+
await saveLocal(localFile, job);
|
|
424
|
+
await publishWake(eventBus(args), job, task.id, `${task.id} ${action} requested`);
|
|
425
|
+
console.log(JSON.stringify(task, null, 2));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
393
428
|
if (['RUNNING', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
|
|
394
429
|
throw new Error(`Task ${id} is ${task.state}; durable execution fields are immutable in this state`);
|
|
395
430
|
if (hasArg(args, '--adapter')) {
|
|
@@ -438,8 +473,7 @@ async function runTaskCommand(action, args) {
|
|
|
438
473
|
reasoning: hasArg(args, '--reasoning') ? arg(args, '--reasoning') || undefined : current?.reasoning,
|
|
439
474
|
cwd: hasArg(args, '--cwd') ? arg(args, '--cwd') || undefined : current?.cwd,
|
|
440
475
|
projectId: hasArg(args, '--chatgpt-project') ? arg(args, '--chatgpt-project') || undefined : current?.projectId,
|
|
441
|
-
decidedBy: 'cli:update',
|
|
442
|
-
decidedAt: new Date().toISOString()
|
|
476
|
+
decidedBy: 'cli:update', decidedAt: new Date().toISOString()
|
|
443
477
|
};
|
|
444
478
|
if (hasArg(args, '--reasoning') && !hasArg(args, '--timeout'))
|
|
445
479
|
task.timeoutMs = taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
|
|
@@ -714,51 +748,16 @@ async function main() {
|
|
|
714
748
|
throw new Error('record requires --summary');
|
|
715
749
|
const commit = arg(args, '--commit');
|
|
716
750
|
const task = { jobId: job.id, id, projectName: arg(args, '--project') || undefined, agentName: arg(args, '--agent', 'orchestrator'), parentTaskId: arg(args, '--parent') || null, dependencies: arg(args, '--deps').split(',').filter(Boolean), capabilities: [], adapter: 'orchestrator', input: arg(args, '--input', summary), continuation: null, continuationDeliveredAt: null, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: commit || null, threadId: null, result: { summary, data: commit ? { commit } : undefined }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now };
|
|
717
|
-
await
|
|
751
|
+
await store.appendTask(task);
|
|
752
|
+
job.tasks.push(task);
|
|
718
753
|
await saveLocal(localFile, job);
|
|
719
754
|
console.log(`Recorded ${id}`);
|
|
720
755
|
return;
|
|
721
756
|
}
|
|
722
|
-
if (command === 'submit') {
|
|
723
|
-
const now = new Date().toISOString();
|
|
724
|
-
const task = workerTaskFromArgs(job, args, now);
|
|
725
|
-
task.id = arg(args, '--task-id', randomUUID());
|
|
726
|
-
const planned = await appendPlannedTask(store, job, task);
|
|
727
|
-
if (job.state !== 'RUNNING') {
|
|
728
|
-
job.state = 'RUNNING';
|
|
729
|
-
job.updatedAt = now;
|
|
730
|
-
await store.saveJob(job);
|
|
731
|
-
}
|
|
732
|
-
await saveLocal(localFile, job);
|
|
733
|
-
await publishWake(bus, job, planned.id, `Planning decision recorded and task ${planned.id} submitted`);
|
|
734
|
-
console.log(`Submitted ${planned.id}`);
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
757
|
if (command === 'status') {
|
|
738
758
|
console.log(JSON.stringify(await store.load(job.id), null, 2));
|
|
739
759
|
return;
|
|
740
760
|
}
|
|
741
|
-
if (command === 'retry' || command === 'cancel') {
|
|
742
|
-
const id = arg(args, '--task-id');
|
|
743
|
-
const task = job.tasks.find(item => item.id === id);
|
|
744
|
-
if (!task)
|
|
745
|
-
throw new Error(`Task ${id} not found`);
|
|
746
|
-
if (command === 'retry')
|
|
747
|
-
task.maxAttempts = extendRetryBudget(task);
|
|
748
|
-
task.state = command === 'retry' ? 'READY' : 'CANCELLED';
|
|
749
|
-
task.error = command === 'retry' ? null : 'Cancelled by user';
|
|
750
|
-
task.updatedAt = new Date().toISOString();
|
|
751
|
-
await store.saveTask(task);
|
|
752
|
-
if (command === 'retry' && job.state !== 'RUNNING') {
|
|
753
|
-
job.state = 'RUNNING';
|
|
754
|
-
job.updatedAt = task.updatedAt;
|
|
755
|
-
await store.saveJob(job);
|
|
756
|
-
}
|
|
757
|
-
await saveLocal(localFile, job);
|
|
758
|
-
await publishWake(bus, job, task.id, `${task.id} ${command} requested`);
|
|
759
|
-
console.log(`${id}: ${task.state}`);
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
761
|
const reconciler = runtime(store, args, bus);
|
|
763
762
|
if (command === 'reconcile') {
|
|
764
763
|
await reconciler.reconcile(job.id);
|
package/dist/planner.js
CHANGED
|
@@ -11,10 +11,6 @@ export function planningSpec(task) {
|
|
|
11
11
|
throw new Error(`Initial task ${task.id} requires a supported adapter`);
|
|
12
12
|
return { id: task.id, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs };
|
|
13
13
|
}
|
|
14
|
-
export function orchestratorPlanningDecision(job, tasks, id, parentTaskId, round, source = 'orchestrator') {
|
|
15
|
-
const now = new Date().toISOString();
|
|
16
|
-
return { jobId: job.id, id, kind: 'planner', priority: job.priority, storySize: 'S', agentName: source === 'orchestrator' ? 'orchestrator' : 'planner', parentTaskId, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: `Planning decision supplied by ${source} for ${tasks.length} task(s)`, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: { summary: source === 'orchestrator' ? 'Initial task set supplied by orchestrator' : 'Planning decision completed' }, plannerResult: { objective_status: 'in_progress', assessment: source === 'orchestrator' ? 'Initial task set supplied by orchestrator' : 'Planning decision completed', next_tasks: tasks.map(planningSpec) }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now, planningDecisionId: id, planningSource: source, planningRound: round };
|
|
17
|
-
}
|
|
18
14
|
function record(value) {
|
|
19
15
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
20
16
|
throw new Error('Planner result must be an object');
|
package/dist/pool.js
CHANGED
|
@@ -4,7 +4,7 @@ const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
|
4
4
|
export function compareJobPriority(a, b) { return RANK[a.priority ?? 'P2'] - RANK[b.priority ?? 'P2'] || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
|
|
5
5
|
export function runnableManagedJobs(items) { return items.filter(item => item.githubState === 'OPEN' && !item.draft && !['COMPLETED', 'CANCELLED'].includes(item.job.state)).map(item => item.job).sort(compareJobPriority); }
|
|
6
6
|
export function needsLifecycleReconciliation(job, now = Date.now()) {
|
|
7
|
-
// Create-only
|
|
7
|
+
// Create-only Jobs stay OPEN until a caller explicitly creates work.
|
|
8
8
|
if (job.tasks.length === 0)
|
|
9
9
|
return false;
|
|
10
10
|
if (job.tasks.some(task => ['READY', 'QUEUED', 'WAITING'].includes(task.state)))
|
package/dist/reconciler.js
CHANGED
|
@@ -386,7 +386,7 @@ export class Reconciler {
|
|
|
386
386
|
if (job.initialPlanning?.status === 'pending')
|
|
387
387
|
return job;
|
|
388
388
|
const allPlanners = job.tasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
389
|
-
if (allPlanners.length === 0)
|
|
389
|
+
if (allPlanners.length === 0 && job.tasks.length === 0)
|
|
390
390
|
return job;
|
|
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;
|
|
@@ -395,7 +395,7 @@ export class Reconciler {
|
|
|
395
395
|
return job;
|
|
396
396
|
if (scopedTasks.some(task => task.state === 'FAILED' || task.state === 'BLOCKED'))
|
|
397
397
|
return job;
|
|
398
|
-
if (scopedTasks.length > 0 && !planners.some(task => task.state === 'SUCCEEDED'))
|
|
398
|
+
if (scopedTasks.length > 0 && planners.length > 0 && !planners.some(task => task.state === 'SUCCEEDED'))
|
|
399
399
|
return job;
|
|
400
400
|
if (scopedTasks.some(task => task.kind !== 'planner' && !['SUCCEEDED', 'CANCELLED'].includes(task.state)))
|
|
401
401
|
return job;
|
package/dist/types.js
CHANGED
|
@@ -2,7 +2,7 @@ export const STORY_SIZE_TIMEOUT_MS = { S: 300000, M: 900000, L: 2700000 };
|
|
|
2
2
|
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
|
-
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'] };
|
|
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
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', '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}`); }
|
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@ Example:
|
|
|
62
62
|
~~~sh
|
|
63
63
|
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id JOB_ID --title "Objective"
|
|
64
64
|
|
|
65
|
-
npx agents-relay
|
|
65
|
+
npx agents-relay task create --repo OWNER/REPO --pr 12 --id JOB_ID --task-id implement --adapter codex --provider openai --model MODEL --output task-pr --input "Implement the requested change, validate it, and update the managed task/PR."
|
|
66
66
|
|
|
67
67
|
npx agents-relayd --repo OWNER/REPO --events nats
|
|
68
68
|
~~~
|
|
@@ -96,17 +96,15 @@ ChatGPT thread identity is diagnostic only. It is not persisted as a resumable t
|
|
|
96
96
|
|
|
97
97
|
## Autonomous planner
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Planner-created child tasks must also be descriptive agent work. They must select `codex` or `chatgpt` and include an explicit output contract. A planner must never generate shell-command worker tasks.
|
|
99
|
+
Direct orchestrator-created Tasks do not require a Plan. The orchestrator may create the initial batch with `task create`; those Tasks remain top-level unless the orchestrator explicitly gives them normal task parentage.
|
|
100
|
+
|
|
101
|
+
For autonomous continuation, Relay invokes the planner agent as a pure decision function. The planner receives durable Job/Task state, previous planner rounds, and resolved execution context, then returns structured `{ objective_status, assessment, next_tasks }` output. The planner does not call Relay CLI and does not persist Tasks itself.
|
|
102
|
+
|
|
103
|
+
Each successful planner invocation is one real planner round. Relay persists that planner result, then creates the returned Tasks with lineage back to that planner round. Only planner execution creates planner rounds; Relay never manufactures synthetic `plan-*` records for direct Task creation. In the dashboard, planner rounds are organizational/grouping nodes for the Tasks they produced.
|
|
104
|
+
|
|
105
|
+
A zero-task autonomous Job remains idle. After a direct orchestrator-created initial batch completes, Relay may start the first real planner round. Fixed Jobs do not automatically create planner rounds.
|
|
106
|
+
|
|
107
|
+
Planner-created child Tasks remain descriptive agent work and must include worker requirements, output contract, and `storySize`; worker-router/model-router choose execution routing afterward.
|
|
110
108
|
|
|
111
109
|
## Durable truth
|
|
112
110
|
|
|
@@ -4,22 +4,30 @@ You are the default objective planner for Agents Relay autonomous jobs.
|
|
|
4
4
|
|
|
5
5
|
Before producing tasks, read and follow `../contracts/task-job-contract.md`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You are a pure planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. You return one structured planning result. You do not call the Agents Relay CLI, persist state, create Jobs, or mutate Tasks yourself.
|
|
8
|
+
|
|
9
|
+
Relay persists each successful planner invocation as one planner round and creates the returned Tasks under that round.
|
|
8
10
|
|
|
9
11
|
## Rules
|
|
10
12
|
|
|
11
|
-
- Treat durable
|
|
13
|
+
- Treat durable Job/Task state as authoritative.
|
|
12
14
|
- Never invent completed work or evidence.
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Every child
|
|
15
|
+
- First decide whether the objective is already satisfied.
|
|
16
|
+
- If satisfied, return `objective_status: "satisfied"` and `next_tasks: []`.
|
|
17
|
+
- Otherwise return only the next useful executable batch.
|
|
18
|
+
- Do not repeat completed work or recreate an existing Task unless replacement work is explicitly needed.
|
|
19
|
+
- Prefer small independently verifiable Tasks with stable IDs.
|
|
20
|
+
- A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
|
|
21
|
+
- Use dependencies only when execution order actually matters.
|
|
22
|
+
- Every child Task must declare `workerRequirements`; worker-router selects the worker and model-router selects provider/model/reasoning afterward.
|
|
23
|
+
- Do not choose `codex`/`chatgpt`, provider/model, or usage probes.
|
|
24
|
+
- Every child Task must declare exactly one output: `task_pr` or `file`.
|
|
25
|
+
- Every child Task must include `storySize` (`S`, `M`, or `L`); use `M` when uncertain.
|
|
26
|
+
- Do not create bookkeeping, waiting, status-checking, wrapper, or planning Tasks. Your response itself is the planning round.
|
|
21
27
|
- Return no prose outside the JSON result.
|
|
22
28
|
|
|
23
29
|
## Output
|
|
24
30
|
|
|
25
|
-
Return exactly one JSON object with `objective_status`, `assessment`, and `next_tasks`.
|
|
31
|
+
Return exactly one JSON object with `objective_status`, `assessment`, and `next_tasks`.
|
|
32
|
+
|
|
33
|
+
Each item in `next_tasks` contains `id`, `input`, `workerRequirements`, `output`, and `storySize`. It may also contain priority, projectName, agentName, dependencies, capabilities, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
|
|
@@ -20,7 +20,7 @@ A **P0 job** (alias: **self job**) is executed directly by the current active or
|
|
|
20
20
|
1. The orchestrator creates the durable job.
|
|
21
21
|
2. The orchestrator immediately creates the required orchestrator-owned task(s); do not leave the P0 job idle.
|
|
22
22
|
3. The current orchestrator thread starts executing those tasks immediately rather than delegating them to another worker.
|
|
23
|
-
4.
|
|
24
|
-
5.
|
|
23
|
+
4. P0/self jobs do not require the normal reviewer gate unless the user explicitly asks for review.
|
|
24
|
+
5. After implementation and local verification, the orchestrator merges the job itself and completes normal post-merge reconciliation.
|
|
25
25
|
|
|
26
26
|
`job create` still only creates durable job state; the P0 workflow requires the orchestrator to follow it immediately with task creation and execution.
|
|
@@ -40,10 +40,19 @@ A normal task never merges a PR. After all non-superseded work gates pass, Relay
|
|
|
40
40
|
|
|
41
41
|
Final-review routing follows the same worker-selection boundary as executable work. Capability, availability, explicit reviewer constraints, and resource health are hard gates. Among eligible workers, Relay prefers a reviewer with a different worker identity from the implementation tasks; when that is unavailable it prefers a different provider/model, then falls back to the first eligible worker. The selected worker, `selectedBy` reason, and `reviewerRouting` event data are durable evidence. No provider is a universal reviewer default.
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Job, Task, and planner ownership
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
The public CLI resource surface is:
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
- `job create|update|show|list`
|
|
48
|
+
- `task create|update|show|list|retry|cancel`
|
|
49
|
+
|
|
50
|
+
`job create` persists only durable Job state. It never creates executable work. `task create` creates a real executable Task directly under the Job; direct orchestrator-created Tasks are not wrapped in synthetic planner Tasks and have no planner-round provenance.
|
|
51
|
+
|
|
52
|
+
Planner rounds are internal planner output, not a general public CLI resource. Relay invokes the planner agent, validates its structured result, persists that invocation as one planner round, then materializes `next_tasks` under that planner round. The planner agent itself does not call Relay CLI or persist Tasks. Only an actual planner invocation creates a planner round.
|
|
53
|
+
|
|
54
|
+
A zero-task autonomous Job stays idle. An autonomous Job may begin with one or more direct orchestrator-created Tasks and no Plan. After that direct batch completes, Relay may create the first real planner round. Later planner rounds continue until the objective is satisfied. Fixed Jobs never create autonomous planner rounds.
|
|
55
|
+
|
|
56
|
+
Planner-produced Tasks obey this same contract, including explicit `storySize`.
|
|
48
57
|
|
|
49
58
|
A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker, plus the `agents-relay:autonomous` label and one bounded time-of-day `agents-relay:next-run:*` index label. The GitHub heartbeat discovers only indexed due candidates, validates the marker's exact `nextRunAt`, claims the slot idempotently, advances the durable next run, and pulses `agents-relay:schedule-wake`. GitHub webhook delivery then wakes local Relay for planner/worker execution. While the schedule is active, each satisfied run returns the job to `WAITING` instead of entering final-review/auto-merge; older-cycle failures are historical evidence and do not block a later slot.
|