agents-relay 1.0.64 → 1.0.66
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 +34 -1
- package/dist/dashboard.js +38 -6
- package/dist/relayd.js +13 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -668,6 +668,35 @@ async function runJobCommand(action, args) {
|
|
|
668
668
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
669
669
|
export function runtimeReviewer() { return { requirements: { capabilities: ['model'] } }; }
|
|
670
670
|
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()) }); }
|
|
671
|
+
export function dashboardJobAction(store, reconciler, action) {
|
|
672
|
+
return async (jobId) => {
|
|
673
|
+
const job = await store.load(jobId);
|
|
674
|
+
if (action === 'start') {
|
|
675
|
+
if (job.state !== 'DRAFT')
|
|
676
|
+
throw new Error(`Job ${job.id} is not startable (${job.state})`);
|
|
677
|
+
job.state = 'ACTIVE';
|
|
678
|
+
job.updatedAt = new Date().toISOString();
|
|
679
|
+
await store.saveJob(job);
|
|
680
|
+
}
|
|
681
|
+
else if (action === 'cancel') {
|
|
682
|
+
if (job.state === 'COMPLETED' || job.state === 'CANCELLED')
|
|
683
|
+
throw new Error(`Job ${job.id} is not cancellable (${job.state})`);
|
|
684
|
+
for (const task of job.tasks.filter(item => !['SUCCEEDED', 'CANCELLED'].includes(item.state))) {
|
|
685
|
+
transitionTask(task, 'CANCELLED');
|
|
686
|
+
task.error = 'Job cancelled';
|
|
687
|
+
task.leaseOwner = null;
|
|
688
|
+
task.leaseExpiresAt = null;
|
|
689
|
+
await store.saveTask(task);
|
|
690
|
+
}
|
|
691
|
+
await store.closePullRequest?.();
|
|
692
|
+
job.state = 'CANCELLED';
|
|
693
|
+
job.updatedAt = new Date().toISOString();
|
|
694
|
+
await store.saveJob(job);
|
|
695
|
+
return await store.load(job.id);
|
|
696
|
+
}
|
|
697
|
+
return await reconciler.wake(job.id);
|
|
698
|
+
};
|
|
699
|
+
}
|
|
671
700
|
export async function createService(args) {
|
|
672
701
|
const loaded = await storeFor(args);
|
|
673
702
|
const upstream = eventBus(args);
|
|
@@ -687,7 +716,11 @@ export async function createService(args) {
|
|
|
687
716
|
void bus.publish(wakeEvent);
|
|
688
717
|
else
|
|
689
718
|
void reconciler.reconcile(loaded.job.id); } } : undefined;
|
|
690
|
-
const dashboardServer = serveDashboard(loaded.store, Number(arg(args, '--port', String(SERVICE_DEFAULTS.port))), bus, Number(arg(args, '--refresh-ms', String(SERVICE_DEFAULTS.dashboardRefreshMs))), loaded.job.id, usageRegistry, overviewProvider, webhook
|
|
719
|
+
const dashboardServer = serveDashboard(loaded.store, Number(arg(args, '--port', String(SERVICE_DEFAULTS.port))), bus, Number(arg(args, '--refresh-ms', String(SERVICE_DEFAULTS.dashboardRefreshMs))), loaded.job.id, usageRegistry, overviewProvider, webhook, undefined, undefined, {
|
|
720
|
+
start: dashboardJobAction(loaded.store, reconciler, 'start'),
|
|
721
|
+
reconcile: dashboardJobAction(loaded.store, reconciler, 'reconcile'),
|
|
722
|
+
cancel: dashboardJobAction(loaded.store, reconciler, 'cancel'),
|
|
723
|
+
});
|
|
691
724
|
await once(dashboardServer, 'listening');
|
|
692
725
|
const stopWatcher = await reconciler.watch(loaded.job.id);
|
|
693
726
|
const watchdog = setInterval(() => void reconciler.reconcile(loaded.job.id), Number(arg(args, '--interval', String(SERVICE_DEFAULTS.watchdogMs))));
|
package/dist/dashboard.js
CHANGED
|
@@ -87,13 +87,21 @@ export const dashboardHtml = `<!doctype html>
|
|
|
87
87
|
.workspace { display:grid; gap:1rem; }
|
|
88
88
|
.job-list,.tree,.event-list { list-style:none; margin:0; padding:0; }
|
|
89
89
|
.job-list { display:grid; gap:.35rem; }
|
|
90
|
-
.job-
|
|
91
|
-
.job-
|
|
92
|
-
.job-
|
|
93
|
-
.job-
|
|
90
|
+
.job-row { display:grid; gap:.2rem; padding:.35rem .45rem; border:1px solid transparent; border-radius:.4rem; }
|
|
91
|
+
.job-row[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
|
|
92
|
+
.job-row.complete-unmerged { border-color:#d69a1b; background:#fff8df; }
|
|
93
|
+
.job-row.complete-unmerged[aria-current="true"] { border-color:#b77900; background:#fff0bd; }
|
|
94
|
+
.job-button { min-width:0; display:grid; gap:.2rem; padding:.12rem 0; border:0; background:transparent; color:inherit; text-align:left; font:inherit; cursor:pointer; }
|
|
94
95
|
.job-title { font-weight:750; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
95
96
|
.job-meta { display:flex; gap:.4rem; flex-wrap:wrap; color:var(--muted); font-size:.7rem; }
|
|
96
97
|
.job-meta span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
98
|
+
.job-primary { display:flex; align-items:center; justify-content:space-between; gap:.45rem; min-width:0; }
|
|
99
|
+
.job-status-line { display:flex; align-items:center; gap:.3rem; min-width:0; white-space:nowrap; }
|
|
100
|
+
.job-status-line .badge { flex:0 0 auto; }
|
|
101
|
+
.job-actions { display:flex; align-items:center; gap:.2rem; }
|
|
102
|
+
.job-action { width:1.8rem; height:1.8rem; display:inline-grid; place-items:center; padding:0; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); font:inherit; font-size:.95rem; line-height:1; cursor:pointer; }
|
|
103
|
+
.job-action:hover:not(:disabled) { color:var(--accent); border-color:var(--accent); }
|
|
104
|
+
.job-action:disabled { cursor:default; opacity:.4; }
|
|
97
105
|
.status-pill,.badge { display:inline-flex; align-items:center; border-radius:999px; padding:.06rem .42rem; min-height:1.35rem; font-size:.68rem; font-weight:800; }
|
|
98
106
|
.status-pill.ok,.badge.ACTIVE,.badge.RUNNING,.badge.SUCCEEDED,.badge.COMPLETED,.badge.MERGED { background:#e5f3e9; color:var(--ok); }
|
|
99
107
|
.status-pill.warn,.badge.BLOCKED,.badge.DRAFT,.badge.QUEUED,.badge.WAITING,.badge.OPEN { background:#fff1d2; color:var(--warn); }
|
|
@@ -237,7 +245,11 @@ function syncFilters(){
|
|
|
237
245
|
selectedRepo=repos.includes(previousRepo)?previousRepo:'';
|
|
238
246
|
repoFilter.value=selectedRepo;
|
|
239
247
|
}
|
|
240
|
-
function
|
|
248
|
+
function actionEnabled(job,action){if(action==='start')return job.state==='DRAFT'&&job.githubState!=='CLOSED'&&job.githubState!=='MERGED';if(action==='reconcile')return job.state==='ACTIVE'&&job.githubState==='OPEN';if(action==='cancel')return job.state!=='COMPLETED'&&job.state!=='CANCELLED'&&job.githubState==='OPEN';return false;}
|
|
249
|
+
function actionButton(job,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=!actionEnabled(job,action);button.addEventListener('click',event=>{event.stopPropagation();void runJobAction(job,action,button);});return button;}
|
|
250
|
+
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();}}
|
|
251
|
+
function labelForAction(action){return action==='start'?'Start':action==='reconcile'?'Reconcile':'Cancel';}
|
|
252
|
+
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));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',()=>selectJob(job.id));select.append(element('span','job-title',text('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'))),element('span','badge '+value(job.githubState||'UNKNOWN'),text(value(job.githubState||'UNKNOWN'))));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 meta=element('span','job-meta');meta.append(element('span','badge mode-'+value(job.executionMode||'fixed'),text(value(job.executionMode||'fixed').toUpperCase())),element('span',job.draft?'badge DRAFT':'',text(job.draft?'DRAFT':value(job.githubState||'UNKNOWN'))));if(isCompleteUnmerged(job))meta.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));meta.append(element('span','',text(job.repository||'unknown repo')),element('span','',text(relativeTime(job.updatedAt))));content.append(select,primary,meta,element('span','subtle',text(compactCounts(counts(job.tasks)))));row.append(content);jobList.append(element('li','',row));}}
|
|
241
253
|
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;}
|
|
242
254
|
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));}
|
|
243
255
|
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;}
|
|
@@ -287,7 +299,7 @@ async function refreshSelected(){if(!selectedJob){await refresh();return;}select
|
|
|
287
299
|
</script>
|
|
288
300
|
</body>
|
|
289
301
|
</html>`;
|
|
290
|
-
export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobId, usageRegistry, overviewProvider, webhook, statusProvider, exactJobProvider) {
|
|
302
|
+
export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobId, usageRegistry, overviewProvider, webhook, statusProvider, exactJobProvider, actions) {
|
|
291
303
|
const webhookDeliveries = new DeliveryDeduper();
|
|
292
304
|
const clients = new Set();
|
|
293
305
|
const subscribedJobs = new Set();
|
|
@@ -428,6 +440,26 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
428
440
|
}
|
|
429
441
|
return;
|
|
430
442
|
}
|
|
443
|
+
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job-action') {
|
|
444
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
445
|
+
const requestedJobId = url.searchParams.get('id') ?? '';
|
|
446
|
+
const action = url.searchParams.get('action') ?? '';
|
|
447
|
+
const handler = action === 'start' ? actions?.start : action === 'reconcile' ? actions?.reconcile : action === 'cancel' ? actions?.cancel : undefined;
|
|
448
|
+
if (req.method !== 'POST' || !/^[A-Za-z0-9_-]+$/.test(requestedJobId) || !handler) {
|
|
449
|
+
res.writeHead(404);
|
|
450
|
+
res.end('Job action unavailable');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
455
|
+
res.end(JSON.stringify(await handler(requestedJobId)));
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
res.writeHead(409);
|
|
459
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
431
463
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job') {
|
|
432
464
|
try {
|
|
433
465
|
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 { isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
14
|
+
import { dashboardJobAction, 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';
|
|
@@ -120,6 +120,13 @@ export async function runDaemon(argv) {
|
|
|
120
120
|
const initial = cachedOverviews;
|
|
121
121
|
const fallback = initial[0]?.job;
|
|
122
122
|
const dashboardStore = fallback ? new GitHubStore(client, fallback.repository, fallback.prNumber, trusted) : new InMemoryStore();
|
|
123
|
+
const workspaceJobAction = (action) => async (jobId) => {
|
|
124
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
125
|
+
if (!overview)
|
|
126
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
127
|
+
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
128
|
+
return await dashboardJobAction(store, makeReconciler(store, concurrency), action)(jobId);
|
|
129
|
+
};
|
|
123
130
|
const secretFile = value(argv, '--github-webhook-secret-file');
|
|
124
131
|
const secret = value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
|
|
125
132
|
const webhook = secret ? {
|
|
@@ -159,7 +166,11 @@ export async function runDaemon(argv) {
|
|
|
159
166
|
return null;
|
|
160
167
|
}
|
|
161
168
|
};
|
|
162
|
-
const server = serveDashboard(dashboardStore, Number(value(argv, '--port', String(SERVICE_DEFAULTS.port))), bus, Number(value(argv, '--refresh-ms', String(SERVICE_DEFAULTS.dashboardRefreshMs))), fallback?.id, usageRegistry, overviews, webhook, () => client.rateLimitStatus(), exactJob
|
|
169
|
+
const server = serveDashboard(dashboardStore, Number(value(argv, '--port', String(SERVICE_DEFAULTS.port))), bus, Number(value(argv, '--refresh-ms', String(SERVICE_DEFAULTS.dashboardRefreshMs))), fallback?.id, usageRegistry, overviews, webhook, () => client.rateLimitStatus(), exactJob, {
|
|
170
|
+
start: workspaceJobAction('start'),
|
|
171
|
+
reconcile: workspaceJobAction('reconcile'),
|
|
172
|
+
cancel: workspaceJobAction('cancel'),
|
|
173
|
+
});
|
|
163
174
|
await once(server, 'listening');
|
|
164
175
|
const reconcileSafely = async () => {
|
|
165
176
|
if (client.rateLimitStatus().limited)
|