agents-relay 1.0.81 → 1.0.83
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
CHANGED
|
@@ -182,4 +182,6 @@ npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
|
|
|
182
182
|
|
|
183
183
|
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 PR state, durable Job lifecycle, and derived runtime status separately. A merged PR reconciles lifecycle to `COMPLETED`; a closed, unmerged PR reconciles lifecycle to `CANCELLED`.
|
|
184
184
|
|
|
185
|
+
`adapter: orchestrator` is reserved for synchronous self-work by the exact orchestrator turn that creates the Task. Relay deliberately does not auto-start these Tasks during reconciliation. The creator must perform the work before ending the turn and terminalize the same Task with `npx agents-relay task complete --task-id <id> --summary <summary> ...`. If work should continue asynchronously or in a later turn, use a real worker adapter instead.
|
|
186
|
+
|
|
185
187
|
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. For managed jobs, the GitHub PR body is the canonical Job surface: hidden Job metadata marker plus the visible Markdown objective.
|
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ Usage:
|
|
|
28
28
|
|
|
29
29
|
Commands:
|
|
30
30
|
job create|update|show|list|merge|cancel Manage durable GitHub-backed jobs
|
|
31
|
-
task create|update|show|list|retry|cancel Manage durable tasks
|
|
31
|
+
task create|update|show|list|complete|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
34
|
record Backfill a completed task into durable history
|
|
@@ -106,6 +106,7 @@ Actions:
|
|
|
106
106
|
show Show one Task
|
|
107
107
|
list List Tasks for a Job
|
|
108
108
|
retry Retry a failed or blocked Task
|
|
109
|
+
complete Complete an orchestrator-owned Task
|
|
109
110
|
cancel Cancel a Task`,
|
|
110
111
|
'task create': `Create one durable Task under an existing Job.
|
|
111
112
|
|
|
@@ -129,6 +130,9 @@ Required: --task-id ID and the normal Job storage options.`,
|
|
|
129
130
|
'task list': `List durable Tasks for one Job.
|
|
130
131
|
|
|
131
132
|
Required: the normal Job storage options.`,
|
|
133
|
+
'task complete': `Complete one orchestrator-owned Task after the creating orchestrator has finished the work.
|
|
134
|
+
|
|
135
|
+
Required: --task-id ID, --summary TEXT, and the normal Job storage options. The Task must use adapter=orchestrator and be QUEUED or RUNNING. This command is for the same orchestrator turn that created the self-task; it is not a deferred handoff mechanism.`,
|
|
132
136
|
'task retry': `Retry one durable Task.
|
|
133
137
|
|
|
134
138
|
Required: --task-id ID and the normal Job storage options.`,
|
|
@@ -402,7 +406,7 @@ async function workerTaskFromArgs(job, args, now) {
|
|
|
402
406
|
};
|
|
403
407
|
}
|
|
404
408
|
async function runTaskCommand(action, args) {
|
|
405
|
-
if (!['create', 'update', 'show', 'list', 'retry', 'cancel'].includes(action))
|
|
409
|
+
if (!['create', 'update', 'show', 'list', 'complete', 'retry', 'cancel'].includes(action))
|
|
406
410
|
throw new Error(usage);
|
|
407
411
|
const { store, job, localFile } = await storeFor(args);
|
|
408
412
|
if (action === 'list') {
|
|
@@ -437,6 +441,28 @@ async function runTaskCommand(action, args) {
|
|
|
437
441
|
console.log(JSON.stringify(task, null, 2));
|
|
438
442
|
return;
|
|
439
443
|
}
|
|
444
|
+
if (action === 'complete') {
|
|
445
|
+
if (task.adapter !== 'orchestrator')
|
|
446
|
+
throw new Error('task complete is only valid for adapter=orchestrator self-tasks');
|
|
447
|
+
if (!['QUEUED', 'RUNNING'].includes(task.state))
|
|
448
|
+
throw new Error(`Task ${id} is ${task.state}; only QUEUED or RUNNING orchestrator tasks can be completed`);
|
|
449
|
+
const summary = arg(args, '--summary');
|
|
450
|
+
if (!summary)
|
|
451
|
+
throw new Error('task complete requires --summary');
|
|
452
|
+
if (task.state === 'QUEUED')
|
|
453
|
+
await transitionTaskEvent(task, 'task.started');
|
|
454
|
+
task.attempt = Math.max(task.attempt, 1);
|
|
455
|
+
task.result = { summary };
|
|
456
|
+
task.error = null;
|
|
457
|
+
task.leaseOwner = null;
|
|
458
|
+
task.leaseExpiresAt = null;
|
|
459
|
+
task.executionId = null;
|
|
460
|
+
await transitionTaskEvent(task, 'task.completed');
|
|
461
|
+
await store.saveTask(task);
|
|
462
|
+
await saveLocal(localFile, await store.load(job.id));
|
|
463
|
+
console.log(JSON.stringify(task, null, 2));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
440
466
|
if (action === 'retry' || action === 'cancel') {
|
|
441
467
|
if (action === 'retry')
|
|
442
468
|
task.maxAttempts = extendRetryBudget(task);
|
package/dist/dashboard.js
CHANGED
|
@@ -286,7 +286,7 @@ function actionButton(job,action,label,icon){const button=document.createElement
|
|
|
286
286
|
async function runJobAction(job,action,button){button.disabled=true;try{const response=await fetch('/api/job-action?id='+encodeURIComponent(job.id)+'&action='+encodeURIComponent(action),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);await refresh(true);}catch(error){button.title=labelForAction(action)+': '+error.message;}finally{renderAll();}}
|
|
287
287
|
function labelForAction(action){return action==='start'?'Start':action==='reconcile'?'Reconcile':'Cancel';}
|
|
288
288
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
289
|
-
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(job.id===selectedJob));row.addEventListener('click',()=>selectJob(job.id));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job.id);});select.append(element('span','job-title',text(repoName(job.repository)+'/PR#'+value(job.prNumber)+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE')))
|
|
289
|
+
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(job.id===selectedJob));row.addEventListener('click',()=>selectJob(job.id));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job.id);});select.append(element('span','job-title',text(repoName(job.repository)+'/PR#'+value(job.prNumber)+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const actions=element('span','job-actions');actions.append(actionButton(job,'start','Start','▶'),actionButton(job,'reconcile','Reconcile','↻'),actionButton(job,'cancel','Cancel','×'));const primary=element('div','job-primary');primary.append(states,actions);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
290
290
|
function taskNode(task,children,depth){const li=element('li');const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{selectedTaskId=task.id;eventTaskId=task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const id=element('span','task-id',text(task.id));id.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(id,element('span','badge '+value(task.state),text(task.state)));li.append(row);const nested=children.get(task.id)||[];if(nested.length){const ul=element('ul','tree');for(const child of nested)ul.append(taskNode(child,children,depth+1));li.append(ul);}return li;}
|
|
291
291
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const known=new Set(tasks.map(task=>task.id));const children=new Map();const roots=[];for(const task of tasks){if(!task.parentTaskId||!known.has(task.parentTaskId))roots.push(task);else{const siblings=children.get(task.parentTaskId)||[];siblings.push(task);children.set(task.parentTaskId,siblings);}}for(const task of roots)taskTree.append(taskNode(task,children,0));}
|
|
292
292
|
function attemptBudget(task){const attempt=Number(task?.attempt)||0;const max=Number(task?.maxAttempts)||0;if(max>=3&&max%3===0){const effectiveMax=Math.max(max,Math.ceil(attempt/3)*3);return attempt+' / 3×'+(effectiveMax/3);}return attempt+' / '+max;}
|
package/dist/reconciler.js
CHANGED
|
@@ -187,15 +187,7 @@ export class Reconciler {
|
|
|
187
187
|
for (const task of scheduled.tasks.filter(t => t.state === 'QUEUED' && t.executionId === null && dependencyReady(t, new Map(scheduled.tasks.map(item => [item.id, item])))).sort((a, b) => compareTaskPriority(a, b, scheduled.priority ?? 'P2')).slice(0, capacity)) {
|
|
188
188
|
if (task.kind === 'planner')
|
|
189
189
|
await this.launchPlanner(scheduled, task);
|
|
190
|
-
else if (task.adapter
|
|
191
|
-
await transitionTaskEvent(task, 'task.started');
|
|
192
|
-
task.leaseOwner = null;
|
|
193
|
-
task.leaseExpiresAt = null;
|
|
194
|
-
task.executionId = null;
|
|
195
|
-
await this.store.saveTask(task);
|
|
196
|
-
await this.emit(eventFor(scheduled.id, task.id, task.parentTaskId, 'task.orchestrator_assigned', 'running', `Task ${task.id} assigned to orchestrator self`, 'orchestrator', { adapter: 'orchestrator' }, { agent: 'relay', component: 'agents-relay' }));
|
|
197
|
-
}
|
|
198
|
-
else
|
|
190
|
+
else if (task.adapter !== 'orchestrator')
|
|
199
191
|
await this.launch(scheduled, task);
|
|
200
192
|
}
|
|
201
193
|
}
|
package/package.json
CHANGED
|
@@ -23,8 +23,9 @@ A **P0 job** (alias: **self job**) is executed directly by the current active or
|
|
|
23
23
|
|
|
24
24
|
1. The orchestrator creates the durable job.
|
|
25
25
|
2. The orchestrator immediately creates the required orchestrator-owned task(s); do not leave the P0 job idle.
|
|
26
|
-
3. The current orchestrator thread starts executing those tasks immediately rather than delegating them to another worker.
|
|
27
|
-
4.
|
|
28
|
-
5.
|
|
26
|
+
3. The current orchestrator thread starts executing those tasks immediately rather than delegating them to another worker. Creating an `adapter: orchestrator` Task is never a handoff: the exact creator owns it for this turn.
|
|
27
|
+
4. The orchestrator must terminalize every orchestrator-owned Task it created before ending the turn. It must not return to the user with one of its self-tasks left `QUEUED` or `RUNNING`; successful work uses `task complete` on that same durable Task.
|
|
28
|
+
5. P0/self jobs do not require the normal reviewer gate unless the user explicitly asks for review.
|
|
29
|
+
6. After implementation and local verification, the orchestrator merges the job itself and completes normal post-merge reconciliation.
|
|
29
30
|
|
|
30
31
|
`job create` still only creates durable job state; the P0 workflow requires the orchestrator to follow it immediately with task creation and execution.
|
|
@@ -87,6 +87,16 @@ Task readiness is explicit and must use these semantics:
|
|
|
87
87
|
|
|
88
88
|
Do not use `WAITING` as a Task state or durable Job lifecycle state. `WAITING` is a derived Job runtime status for an `ACTIVE` Job whose next progress depends on a future trigger, schedule slot, dependency release, or other condition.
|
|
89
89
|
|
|
90
|
+
## Orchestrator self-task ownership
|
|
91
|
+
|
|
92
|
+
A Task with `adapter: orchestrator` is synchronous self-work owned by the exact orchestrator turn that creates it. It is not a durable handoff to Relay, another worker, a later reconcile cycle, or a future orchestrator thread.
|
|
93
|
+
|
|
94
|
+
- Creating an `adapter: orchestrator` Task is an execution commitment: the creating orchestrator must execute it in the current turn.
|
|
95
|
+
- The creating orchestrator must not end its turn while any orchestrator Task it created remains `QUEUED` or `RUNNING`; it must drive each such Task to a terminal state before returning control to the user.
|
|
96
|
+
- Relay must not auto-start, lease, route, or emit synthetic `task.started` / assignment evidence for orchestrator Tasks. A queued orchestrator Task means the creator has not yet terminalized its self-work.
|
|
97
|
+
- Successful self-work is terminalized on the same durable Task with `task complete --task-id ... --summary ...`; do not use `record` as a substitute for a Task that was created before execution. `record` remains a backfill mechanism for work that genuinely happened outside Relay.
|
|
98
|
+
- If work is intended to survive the current turn or be picked up asynchronously, use a real worker adapter rather than `orchestrator`.
|
|
99
|
+
|
|
90
100
|
## Worker lifecycle
|
|
91
101
|
|
|
92
102
|
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.
|