agents-relay 1.0.158 → 1.0.160
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3 -2
- package/dist/dashboard-actions.js +19 -1
- package/dist/dashboard-html.js +4 -1
- package/dist/dashboard.js +25 -0
- package/dist/relayd.js +11 -1
- package/package.json +1 -1
- package/skills/agents-relay/contracts/task-job-contract.md +42 -3
package/dist/cli.js
CHANGED
|
@@ -7,8 +7,8 @@ import { dashboardManagedJobs, InMemoryStore, GitHubStore, resolvePullRequest, l
|
|
|
7
7
|
import { githubAuthContext } from './github-auth.js';
|
|
8
8
|
import { taskOverallTimeoutMs } from './types.js';
|
|
9
9
|
import { Reconciler } from './reconciler.js';
|
|
10
|
-
import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
11
|
-
export { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
10
|
+
import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
11
|
+
export { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
12
12
|
import { CodexAdapter, ChatGptAdapter } from './adapters.js';
|
|
13
13
|
import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
|
|
14
14
|
import { eventFor } from './events.js';
|
|
@@ -488,6 +488,7 @@ export async function createService(args) {
|
|
|
488
488
|
start: dashboardJobAction(loaded.store, reconciler, 'start'),
|
|
489
489
|
reconcile: dashboardJobAction(loaded.store, reconciler, 'reconcile'),
|
|
490
490
|
cancel: dashboardJobAction(loaded.store, reconciler, 'cancel'),
|
|
491
|
+
jobSetState: dashboardJobSetState(loaded.store),
|
|
491
492
|
taskRetry: dashboardTaskAction(loaded.store, reconciler, 'retry'),
|
|
492
493
|
taskCancel: dashboardTaskAction(loaded.store, reconciler, 'cancel'),
|
|
493
494
|
taskSetState: dashboardTaskSetState(loaded.store),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extendRetryBudget, transitionTaskEvent } from './scheduler.js';
|
|
2
|
-
import { applyTaskWorkflowDecision } from './workflow/runtime.js';
|
|
2
|
+
import { applyJobWorkflowDecision, applyTaskWorkflowDecision } from './workflow/runtime.js';
|
|
3
3
|
export function dashboardTaskAction(store, reconciler, action) {
|
|
4
4
|
return async (jobId, taskId) => {
|
|
5
5
|
const job = await store.load(jobId);
|
|
@@ -26,6 +26,24 @@ export function dashboardTaskAction(store, reconciler, action) {
|
|
|
26
26
|
return await reconciler.wake(job.id);
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
export function dashboardJobSetState(store) {
|
|
30
|
+
return async (jobId, state) => {
|
|
31
|
+
const job = await store.load(jobId);
|
|
32
|
+
if (job.state === state)
|
|
33
|
+
return job;
|
|
34
|
+
applyJobWorkflowDecision(job, {
|
|
35
|
+
accepted: true,
|
|
36
|
+
target: state,
|
|
37
|
+
guard: null,
|
|
38
|
+
actions: [],
|
|
39
|
+
mutations: [],
|
|
40
|
+
effects: [],
|
|
41
|
+
reason: 'accepted',
|
|
42
|
+
});
|
|
43
|
+
await store.saveJob(job);
|
|
44
|
+
return await store.load(job.id);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
29
47
|
export function dashboardTaskSetState(store) {
|
|
30
48
|
return async (jobId, taskId, state) => {
|
|
31
49
|
const job = await store.load(jobId);
|
package/dist/dashboard-html.js
CHANGED
|
@@ -324,13 +324,16 @@ function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[tas
|
|
|
324
324
|
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key);const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next)collapsedTasks.add(key);else collapsedTasks.delete(key);});li.append(line,ul);}else li.append(line);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',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null: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)));const tag=taskTagDisplay(selected(),task);if(tag){const badge=element('span','task-tag',text(tag));badge.title=(selected()?.tag?.description||'Task tag')+': '+task.tag;row.append(badge);}line.append(row);return li;}
|
|
325
325
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const work=tasks.filter(task=>task.kind!=='planner');if(!work.length){taskTree.append(element('li','empty',text('No work tasks in durable state.')));return;}const known=new Set(work.map(task=>task.id));const children=new Map();const roots=[];for(const task of work){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));}
|
|
326
326
|
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;}
|
|
327
|
+
const JOB_STATES=['DRAFT','ACTIVE','COMPLETED','CANCELLED'];
|
|
328
|
+
async function setJobState(job,state,select){if(!job||state===job.state)return;select.disabled=true;try{const response=await fetch('/api/job-state?id='+encodeURIComponent(job.id)+'&state='+encodeURIComponent(state),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>candidate.id===job.id);if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){select.title='Set job lifecycle state: '+error.message;select.value=job.state;}finally{select.disabled=false;}}
|
|
329
|
+
function jobStateField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('Lifecycle state')));const select=document.createElement('select');select.className='job-filter job-state-select';select.setAttribute('aria-label','Job lifecycle state');for(const state of JOB_STATES)select.append(new Option(state,state));select.value=job.state;select.addEventListener('change',()=>void setJobState(job,select.value,select));box.append(select);return box;}
|
|
327
330
|
const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','SUCCEEDED','FAILED','CANCELLED'];
|
|
328
331
|
async function setTaskState(task,state,select){const job=selected();if(!job||state===task.state)return;select.disabled=true;try{const response=await fetch('/api/task-state?id='+encodeURIComponent(job.id)+'&task='+encodeURIComponent(task.id)+'&state='+encodeURIComponent(state),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>candidate.id===job.id);if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){select.title='Set task state: '+error.message;select.value=task.state;}finally{select.disabled=false;}}
|
|
329
332
|
function taskStateField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('State')));const select=document.createElement('select');select.className='job-filter task-state-select';select.setAttribute('aria-label','Task state');for(const state of TASK_STATES)select.append(new Option(state,state));select.value=task.state;select.addEventListener('change',()=>void setTaskState(task,select.value,select));box.append(select);return box;}
|
|
330
333
|
function manualOverrideField(task){const rows=Array.isArray(task.manualStateOverrides)?task.manualStateOverrides:[];const last=rows.at(-1);return field('Manual override',last?last.from+' → '+last.to+' · '+relativeTime(last.at):'—');}
|
|
331
334
|
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Task',task.id),field('Kind',task.kind||'work'),taskStateField(task),manualOverrideField(task),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),taskCommentField(task),chatField(task));const job=selected();if(task.reviewHeadSha&&job)groups.append(commitField('HEAD commit',job,task.reviewHeadSha));const graph=agentGraphField(task);if(graph)groups.append(graph);if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));taskDetailRoot.append(groups);}
|
|
332
335
|
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}
|
|
333
|
-
function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),
|
|
336
|
+
function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),jobStateField(job),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Cadence',cadence(job)),field('Review',reviewStatus(job)),commitField('Review head SHA',job,job.review?.headSha||job.headSha||''),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}
|
|
334
337
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
|
335
338
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
336
339
|
function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
|
package/dist/dashboard.js
CHANGED
|
@@ -269,6 +269,31 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
269
269
|
}
|
|
270
270
|
return;
|
|
271
271
|
}
|
|
272
|
+
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job-state') {
|
|
273
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
274
|
+
const requestedJobId = url.searchParams.get('id') ?? '';
|
|
275
|
+
const requestedState = url.searchParams.get('state') ?? '';
|
|
276
|
+
const validState = ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'].includes(requestedState);
|
|
277
|
+
if (req.method !== 'POST' || !/^[A-Za-z0-9_-]+$/.test(requestedJobId) || !validState || !resolvedActions?.jobSetState) {
|
|
278
|
+
res.writeHead(404);
|
|
279
|
+
res.end('Job state override unavailable');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const result = await resolvedActions.jobSetState(requestedJobId, requestedState);
|
|
284
|
+
if (!res.headersSent)
|
|
285
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
286
|
+
if (!res.writableEnded)
|
|
287
|
+
res.end(JSON.stringify(result));
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (!res.headersSent)
|
|
291
|
+
res.writeHead(409);
|
|
292
|
+
if (!res.writableEnded)
|
|
293
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
272
297
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/task-state') {
|
|
273
298
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
274
299
|
const requestedJobId = url.searchParams.get('id') ?? '';
|
package/dist/relayd.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Reconciler } from './reconciler.js';
|
|
|
12
12
|
import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestSnapshotGraphql, withGitHubRequestContext } from './store.js';
|
|
13
13
|
import { githubAuthContext } from './github-auth.js';
|
|
14
14
|
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
15
|
-
import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
15
|
+
import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
16
16
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
17
17
|
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
18
18
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
@@ -200,6 +200,15 @@ export async function runDaemon(argv) {
|
|
|
200
200
|
}
|
|
201
201
|
return await dashboardJobAction(store, makeReconciler(store, concurrency), action)(jobId);
|
|
202
202
|
};
|
|
203
|
+
const workspaceJobSetState = async (jobId, state) => {
|
|
204
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
205
|
+
if (!overview)
|
|
206
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
207
|
+
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
208
|
+
const result = await dashboardJobSetState(store)(jobId, state);
|
|
209
|
+
await refreshPullRequest(overview.job.repository, overview.job.prNumber, 'dashboard.job-state', { force: true });
|
|
210
|
+
return result;
|
|
211
|
+
};
|
|
203
212
|
const workspaceTaskAction = (action) => async (jobId, taskId) => {
|
|
204
213
|
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
205
214
|
if (!overview)
|
|
@@ -255,6 +264,7 @@ export async function runDaemon(argv) {
|
|
|
255
264
|
start: workspaceJobAction('start'),
|
|
256
265
|
reconcile: workspaceJobAction('reconcile'),
|
|
257
266
|
cancel: workspaceJobAction('cancel'),
|
|
267
|
+
jobSetState: workspaceJobSetState,
|
|
258
268
|
taskRetry: workspaceTaskAction('retry'),
|
|
259
269
|
taskCancel: workspaceTaskAction('cancel'),
|
|
260
270
|
taskSetState: workspaceTaskSetState,
|
package/package.json
CHANGED
|
@@ -52,9 +52,48 @@ Keep these concepts distinct: observation is not root cause; hypothesis is not r
|
|
|
52
52
|
|
|
53
53
|
### Job-defined Task tag
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
`Task.state` is reserved for Relay lifecycle only: `DRAFT`, `QUEUED`, `RUNNING`, `BLOCKED`, `SUCCEEDED`, `FAILED`, and `CANCELLED`. A Job may additionally define one optional `tag` contract for a Job-specific result or classification that should be visible on each Task without changing its lifecycle state.
|
|
56
|
+
|
|
57
|
+
The Job owns the meaning of the tag. Its trusted Job marker declares a human-readable `description` explaining what the tag means and when workers should set it, plus the allowed durable values and their dashboard display strings:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"tag": {
|
|
62
|
+
"description": "Deterministic E2E outcome. Set pass when the fixed E2E exits 0; set fail when it still fails after the required retry.",
|
|
63
|
+
"values": {
|
|
64
|
+
"pass": "✅",
|
|
65
|
+
"fail": "❌"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A worker does **not** edit the Task marker directly. When the Job defines a tag and the tag applies to the work just performed, the worker reports one allowed durable value in the canonical terminal event as `data.tag`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"type": "task.completed",
|
|
76
|
+
"data": {
|
|
77
|
+
"tag": "pass",
|
|
78
|
+
"exit_code": 0
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Relay validates `data.tag` against `Job.tag.values`. If the value is allowed, Relay persists it into the trusted Task marker as the Task's `tag`; undeclared or invalid values are ignored. The transport `data.tag` is not duplicated into `result.data`. The resulting Task metadata may therefore contain both independent concepts at once:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"state": "SUCCEEDED",
|
|
88
|
+
"tag": "pass"
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The dashboard always keeps the normal lifecycle badge. When a valid Task tag is present, it additionally renders the display string from `Job.tag.values` after the lifecycle status. For the example above this appears as `SUCCEEDED ✅`.
|
|
93
|
+
|
|
94
|
+
A tag must describe something specific to the Job contract, not Relay lifecycle. Examples include an E2E result (`pass` / `fail`), publication result, validation classification, or another small Job-defined outcome. The Job description must tell the worker when to set the tag and what each allowed value means. Jobs without a `tag` declaration behave exactly as before.
|
|
95
|
+
|
|
96
|
+
For the ChatGPT Browser Worker guardian, for example, the Job defines `pass -> ✅` and `fail -> ❌`; each hourly E2E child Task reports `data.tag` after running the fixed deterministic E2E, while the Task lifecycle independently records whether Relay execution itself completed, failed, or became blocked.
|
|
58
97
|
|
|
59
98
|
|
|
60
99
|
### Optional Review Chair Tasks
|