agents-relay 1.0.74 → 1.0.75
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 +28 -0
- package/dist/dashboard.js +26 -1
- package/dist/relayd.js +10 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -673,6 +673,32 @@ async function runJobCommand(action, args) {
|
|
|
673
673
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
674
674
|
export function runtimeReviewer() { return { requirements: { capabilities: ['model'] } }; }
|
|
675
675
|
export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegistry()) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, usageSnapshotProvider: () => usageRegistry.snapshot(), reviewer: runtimeReviewer(), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: job => resolveManagedProjectPath(job.repository, arg(args, '--project-path'), arg(args, '--workspace-root') || defaultWorkspaceRoot()) }); }
|
|
676
|
+
export function dashboardTaskAction(store, reconciler, action) {
|
|
677
|
+
return async (jobId, taskId) => {
|
|
678
|
+
const job = await store.load(jobId);
|
|
679
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
680
|
+
if (!task)
|
|
681
|
+
throw new Error(`Task ${taskId} not found`);
|
|
682
|
+
if (action === 'retry') {
|
|
683
|
+
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
684
|
+
throw new Error(`Task ${task.id} is not restartable (${task.state})`);
|
|
685
|
+
task.maxAttempts = extendRetryBudget(task);
|
|
686
|
+
await transitionTaskEvent(task, 'retry');
|
|
687
|
+
task.error = null;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
if (!['DRAFT', 'QUEUED', 'BLOCKED', 'FAILED'].includes(task.state))
|
|
691
|
+
throw new Error(`Task ${task.id} is not cancellable (${task.state})`);
|
|
692
|
+
await transitionTaskEvent(task, 'cancel');
|
|
693
|
+
task.error = 'Cancelled by user';
|
|
694
|
+
}
|
|
695
|
+
task.leaseOwner = null;
|
|
696
|
+
task.leaseExpiresAt = null;
|
|
697
|
+
task.executionId = null;
|
|
698
|
+
await store.saveTask(task);
|
|
699
|
+
return await reconciler.wake(job.id);
|
|
700
|
+
};
|
|
701
|
+
}
|
|
676
702
|
export function dashboardJobAction(store, reconciler, action) {
|
|
677
703
|
return async (jobId) => {
|
|
678
704
|
const job = await store.load(jobId);
|
|
@@ -724,6 +750,8 @@ export async function createService(args) {
|
|
|
724
750
|
start: dashboardJobAction(loaded.store, reconciler, 'start'),
|
|
725
751
|
reconcile: dashboardJobAction(loaded.store, reconciler, 'reconcile'),
|
|
726
752
|
cancel: dashboardJobAction(loaded.store, reconciler, 'cancel'),
|
|
753
|
+
taskRetry: dashboardTaskAction(loaded.store, reconciler, 'retry'),
|
|
754
|
+
taskCancel: dashboardTaskAction(loaded.store, reconciler, 'cancel'),
|
|
727
755
|
});
|
|
728
756
|
await once(dashboardServer, 'listening');
|
|
729
757
|
const stopWatcher = await reconciler.watch(loaded.job.id);
|
package/dist/dashboard.js
CHANGED
|
@@ -124,6 +124,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
124
124
|
.task-row { width:100%; display:flex; align-items:center; gap:.4rem; min-width:0; padding:.32rem .38rem; border:0; border-radius:.35rem; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
|
|
125
125
|
.task-row[aria-current="true"] { background:var(--accent2); }
|
|
126
126
|
.task-id { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font: .77rem ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
127
|
+
.task-actions { display:flex; justify-content:flex-end; gap:.25rem; margin-bottom:.5rem; }
|
|
127
128
|
.event-list { display:grid; gap:.45rem; }
|
|
128
129
|
.event-item { padding-bottom:.4rem; border-bottom:1px solid var(--border); }
|
|
129
130
|
.event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
|
|
@@ -274,7 +275,10 @@ function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();
|
|
|
274
275
|
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;}
|
|
275
276
|
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));}
|
|
276
277
|
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;}
|
|
277
|
-
function
|
|
278
|
+
function taskActionEnabled(task,action){if(action==='retry')return ['FAILED','BLOCKED'].includes(task.state);if(action==='cancel')return ['DRAFT','QUEUED','BLOCKED','FAILED'].includes(task.state);return false;}
|
|
279
|
+
function taskActionButton(task,action,label,icon){const button=document.createElement('button');button.type='button';button.className='job-action';button.textContent=icon;button.title=label;button.setAttribute('aria-label',label);button.disabled=!taskActionEnabled(task,action);button.addEventListener('click',()=>void runTaskAction(task,action,button));return button;}
|
|
280
|
+
async function runTaskAction(task,action,button){const job=selected();if(!job)return;button.disabled=true;try{const response=await fetch('/api/task-action?id='+encodeURIComponent(job.id)+'&task='+encodeURIComponent(task.id)+'&action='+encodeURIComponent(action),{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){button.title=(action==='retry'?'Restart':'Cancel')+': '+error.message;}finally{renderAll();}}
|
|
281
|
+
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const actions=element('div','task-actions');actions.append(taskActionButton(task,'retry','Restart task','↻'),taskActionButton(task,'cancel','Cancel task','×'));const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Task',task.id),field('Kind',task.kind||'work'),field('State',task.state),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'),chatField(task));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(actions,groups);}
|
|
278
282
|
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();}
|
|
279
283
|
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'),field('Lifecycle state',job.state),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Review',reviewStatus(job)),field('Review head SHA',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();}
|
|
280
284
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();}
|
|
@@ -485,6 +489,27 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
485
489
|
}
|
|
486
490
|
return;
|
|
487
491
|
}
|
|
492
|
+
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/task-action') {
|
|
493
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
494
|
+
const requestedJobId = url.searchParams.get('id') ?? '';
|
|
495
|
+
const requestedTaskId = url.searchParams.get('task') ?? '';
|
|
496
|
+
const action = url.searchParams.get('action') ?? '';
|
|
497
|
+
const handler = action === 'retry' ? actions?.taskRetry : action === 'cancel' ? actions?.taskCancel : undefined;
|
|
498
|
+
if (req.method !== 'POST' || !/^[A-Za-z0-9_-]+$/.test(requestedJobId) || !/^[A-Za-z0-9_-]+$/.test(requestedTaskId) || !handler) {
|
|
499
|
+
res.writeHead(404);
|
|
500
|
+
res.end('Task action unavailable');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
505
|
+
res.end(JSON.stringify(await handler(requestedJobId, requestedTaskId)));
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
res.writeHead(409);
|
|
509
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
488
513
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job') {
|
|
489
514
|
try {
|
|
490
515
|
const requestedJobId = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('id') ?? undefined;
|
package/dist/relayd.js
CHANGED
|
@@ -11,7 +11,7 @@ import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
|
|
|
11
11
|
import { Reconciler } from './reconciler.js';
|
|
12
12
|
import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestGraphql } from './store.js';
|
|
13
13
|
import { githubAuthContext } from './github-auth.js';
|
|
14
|
-
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
14
|
+
import { dashboardJobAction, dashboardTaskAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
15
15
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
16
|
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath } from './workspace.js';
|
|
17
17
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
@@ -127,6 +127,13 @@ export async function runDaemon(argv) {
|
|
|
127
127
|
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
128
128
|
return await dashboardJobAction(store, makeReconciler(store, concurrency), action)(jobId);
|
|
129
129
|
};
|
|
130
|
+
const workspaceTaskAction = (action) => async (jobId, taskId) => {
|
|
131
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
132
|
+
if (!overview)
|
|
133
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
134
|
+
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
135
|
+
return await dashboardTaskAction(store, makeReconciler(store, concurrency), action)(jobId, taskId);
|
|
136
|
+
};
|
|
130
137
|
const secretFile = value(argv, '--github-webhook-secret-file');
|
|
131
138
|
const secret = value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
|
|
132
139
|
const webhook = secret ? {
|
|
@@ -170,6 +177,8 @@ export async function runDaemon(argv) {
|
|
|
170
177
|
start: workspaceJobAction('start'),
|
|
171
178
|
reconcile: workspaceJobAction('reconcile'),
|
|
172
179
|
cancel: workspaceJobAction('cancel'),
|
|
180
|
+
taskRetry: workspaceTaskAction('retry'),
|
|
181
|
+
taskCancel: workspaceTaskAction('cancel'),
|
|
173
182
|
});
|
|
174
183
|
await once(server, 'listening');
|
|
175
184
|
const reconcileSafely = async () => {
|