agents-relay 2.0.7 → 2.0.9
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 +2 -0
- package/dist/dashboard-html.js +55 -7
- package/dist/dashboard.js +24 -1
- package/dist/relayd.js +10 -0
- package/dist/routing-policy.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,6 +193,8 @@ External product feedback uses the reusable `src/feedback.ts` contract. See [doc
|
|
|
193
193
|
|
|
194
194
|
reconcile leases and launches ready tasks without waiting inside the runtime scheduler; the CLI waits only long enough for its launched workers to persist results. agents-relayd (or the equivalent npx agents-relay serve) is the watchdog/dashboard mode. Model-backed workers require --events nats; the event transport carries authoritative terminal worker events as well as wake/progress traffic. See [docs/service.md](docs/service.md). --file PATH is explicit local demo/test mode only.
|
|
195
195
|
|
|
196
|
+
Router Rules are stored at `~/.agents-relay/execution-router-rules.md` by default. `agents-relayd --execution-router-rules PATH` selects a different file for that daemon process; the explicit CLI path takes precedence over `AGENTS_RELAY_EXECUTION_ROUTER_RULES`, which remains supported for compatibility.
|
|
197
|
+
|
|
196
198
|
`agents-relayd` without `--pr`/`--id` runs in workspace mode by default: it discovers nested Git repositories under `~/Workspace`, keeps only GitHub `origin` remotes, and aggregates their managed PR jobs into one dashboard and worker pool. Pass `--workspace PATH` to use another root. `--repo OWNER/REPO` remains the single-repository pool mode.
|
|
197
199
|
|
|
198
200
|
For automation, Agents Relay can use GitHub App installation authentication via `AGENTS_RELAY_GITHUB_APP_ID`, `AGENTS_RELAY_GITHUB_APP_INSTALLATION_ID`, and `AGENTS_RELAY_GITHUB_APP_PRIVATE_KEY_FILE` (or matching CLI flags). Webhook mode is local-first: targeted webhook reconciliation is primary, dashboard auto-refresh reads the disposable in-process view restored from SQLite RelayStore state, and the broad watchdog drops to a 30-minute fallback. No separate persisted dashboard Job cache is maintained. See [docs/github-app-rate-limit.md](docs/github-app-rate-limit.md).
|
package/dist/dashboard-html.js
CHANGED
|
@@ -91,7 +91,20 @@ export const dashboardHtml = `<!doctype html>
|
|
|
91
91
|
.event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
|
|
92
92
|
.event-kind { color:var(--accent); font-weight:800; }
|
|
93
93
|
.event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
|
|
94
|
-
#usage-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
|
|
94
|
+
#usage-view,#graphs-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
|
|
95
|
+
.graphs-browser { display:grid; grid-template-columns:minmax(190px,260px) minmax(0,1fr); gap:.75rem; min-height:60vh; }
|
|
96
|
+
.graph-list { display:grid; align-content:start; gap:.35rem; }
|
|
97
|
+
.graph-button { width:100%; display:grid; gap:.08rem; padding:.48rem .55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--text); text-align:left; font:inherit; cursor:pointer; }
|
|
98
|
+
.graph-button[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
|
|
99
|
+
.graph-button strong { font-size:.78rem; }
|
|
100
|
+
.graph-button span { color:var(--muted); font:.66rem ui-monospace,SFMono-Regular,Menlo,monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
101
|
+
.graph-preview { min-width:0; display:grid; align-content:start; gap:.55rem; }
|
|
102
|
+
.graph-preview-head { display:flex; justify-content:space-between; gap:.7rem; align-items:baseline; flex-wrap:wrap; }
|
|
103
|
+
.graph-preview-head h3 { margin:0; font-size:.95rem; }
|
|
104
|
+
.graph-canvas { min-height:24rem; display:grid; place-items:center; overflow:auto; padding:1rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
|
|
105
|
+
.graph-canvas svg { max-width:none; height:auto; }
|
|
106
|
+
.graph-source { margin:0; max-height:22rem; overflow:auto; padding:.7rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--muted); font:.72rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; }
|
|
107
|
+
.graph-source-details summary { cursor:pointer; color:var(--muted); font-size:.74rem; font-weight:700; }
|
|
95
108
|
.rules-editor { display:grid; gap:.65rem; }
|
|
96
109
|
.rules-help { margin:0; color:var(--muted); font-size:.8rem; }
|
|
97
110
|
.rules-textarea { width:100%; min-height:55vh; resize:vertical; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); color:var(--text); padding:.8rem; font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
@@ -135,6 +148,9 @@ export const dashboardHtml = `<!doctype html>
|
|
|
135
148
|
#jobs-workspace .panel-body { max-height:none; }
|
|
136
149
|
#usage-view .panel-head { padding:.6rem .7rem .35rem; }
|
|
137
150
|
#usage-view .panel-head .subtle { display:none; }
|
|
151
|
+
.graphs-browser { grid-template-columns:1fr; min-height:0; }
|
|
152
|
+
.graph-list { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
|
153
|
+
.graph-canvas { min-height:18rem; }
|
|
138
154
|
#usage-view .panel-body { padding:.45rem .5rem .65rem; max-height:none; }
|
|
139
155
|
#usage-view .usage-providers { gap:.45rem; }
|
|
140
156
|
#usage-view .usage-provider {
|
|
@@ -170,7 +186,8 @@ export const dashboardHtml = `<!doctype html>
|
|
|
170
186
|
<nav role="tablist" aria-label="Console views">
|
|
171
187
|
<button id="jobs-tab" type="button" role="tab" aria-selected="true" aria-controls="jobs-view">Jobs</button>
|
|
172
188
|
<button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
|
|
173
|
-
<button id="
|
|
189
|
+
<button id="graphs-tab" type="button" role="tab" aria-selected="false" aria-controls="graphs-view">Graphs</button>
|
|
190
|
+
<button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Router Rules</button>
|
|
174
191
|
<button id="shortcuts-tab" type="button" role="tab" aria-selected="false" aria-controls="shortcuts-view">Shortcuts</button>
|
|
175
192
|
</nav>
|
|
176
193
|
</header>
|
|
@@ -201,9 +218,24 @@ export const dashboardHtml = `<!doctype html>
|
|
|
201
218
|
<div class="panel-body" id="usage-root" aria-live="polite"><div class="skeleton"></div></div>
|
|
202
219
|
</div>
|
|
203
220
|
</section>
|
|
221
|
+
<section id="graphs-view" role="tabpanel" aria-labelledby="graphs-tab" hidden>
|
|
222
|
+
<div class="panel">
|
|
223
|
+
<div class="panel-head"><h2>Workflow Graphs</h2><span id="graphs-meta" class="subtle"></span></div>
|
|
224
|
+
<div class="panel-body">
|
|
225
|
+
<div class="graphs-browser">
|
|
226
|
+
<div id="graph-list" class="graph-list" aria-label="Workflow graphs"></div>
|
|
227
|
+
<div class="graph-preview">
|
|
228
|
+
<div class="graph-preview-head"><h3 id="graph-title">Select a graph</h3><span id="graph-path" class="subtle"></span></div>
|
|
229
|
+
<div id="graph-canvas" class="graph-canvas"><p class="empty">Select a graph to inspect it.</p></div>
|
|
230
|
+
<details class="graph-source-details"><summary>Mermaid source</summary><pre id="graph-source" class="graph-source"></pre></details>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</section>
|
|
204
236
|
<section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden>
|
|
205
237
|
<div class="panel">
|
|
206
|
-
<div class="panel-head"><h2>
|
|
238
|
+
<div class="panel-head"><h2>Router Rules</h2><span id="rules-meta" class="subtle"></span></div>
|
|
207
239
|
<div class="panel-body">
|
|
208
240
|
<div class="rules-editor">
|
|
209
241
|
<p class="rules-help">Free-text soft policy loaded by the Execution Router for every worker+model routing decision. Impossible routes remain excluded by code; Codex remains the deterministic fallback if decision fails.</p>
|
|
@@ -226,6 +258,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
226
258
|
</div>
|
|
227
259
|
</section>
|
|
228
260
|
</main>
|
|
261
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
229
262
|
<script>
|
|
230
263
|
'use strict';
|
|
231
264
|
// Compatibility contracts retained for dashboard tests:
|
|
@@ -244,10 +277,12 @@ const MAX_EVENTS=60;
|
|
|
244
277
|
const DASHBOARD_VERSION=${JSON.stringify(dashboardVersion)};
|
|
245
278
|
const jobsTab=document.querySelector('#jobs-tab');
|
|
246
279
|
const usageTab=document.querySelector('#usage-tab');
|
|
280
|
+
const graphsTab=document.querySelector('#graphs-tab');
|
|
247
281
|
const rulesTab=document.querySelector('#rules-tab');
|
|
248
282
|
const shortcutsTab=document.querySelector('#shortcuts-tab');
|
|
249
283
|
const jobsView=document.querySelector('#jobs-view');
|
|
250
284
|
const usageView=document.querySelector('#usage-view');
|
|
285
|
+
const graphsView=document.querySelector('#graphs-view');
|
|
251
286
|
const rulesView=document.querySelector('#rules-view');
|
|
252
287
|
const shortcutsView=document.querySelector('#shortcuts-view');
|
|
253
288
|
const jobsWorkspace=document.querySelector('#jobs-workspace');
|
|
@@ -271,6 +306,12 @@ const connection=document.querySelector('#connection');
|
|
|
271
306
|
const selectedJobTitle=document.querySelector('#selected-job-title');
|
|
272
307
|
const usageRoot=document.querySelector('#usage-root');
|
|
273
308
|
const usageMeta=document.querySelector('#usage-meta');
|
|
309
|
+
const graphsMeta=document.querySelector('#graphs-meta');
|
|
310
|
+
const graphList=document.querySelector('#graph-list');
|
|
311
|
+
const graphTitle=document.querySelector('#graph-title');
|
|
312
|
+
const graphPath=document.querySelector('#graph-path');
|
|
313
|
+
const graphCanvas=document.querySelector('#graph-canvas');
|
|
314
|
+
const graphSource=document.querySelector('#graph-source');
|
|
274
315
|
const rulesText=document.querySelector('#rules-text');
|
|
275
316
|
const rulesMeta=document.querySelector('#rules-meta');
|
|
276
317
|
const rulesStatus=document.querySelector('#rules-status');
|
|
@@ -287,6 +328,8 @@ let selectedRepo=sessionStorage.getItem('agents-relay:repo-filter')||'';
|
|
|
287
328
|
let quickMode=sessionStorage.getItem('agents-relay:quick-filter')||'jobs';
|
|
288
329
|
let selectedRuntimeStatus=sessionStorage.getItem('agents-relay:runtime-status-filter')||'';
|
|
289
330
|
let searchQuery=sessionStorage.getItem('agents-relay:job-search')||'';
|
|
331
|
+
let workflowGraphs=[];
|
|
332
|
+
let selectedGraphId=sessionStorage.getItem('agents-relay:selected-graph')||'';
|
|
290
333
|
let eventTaskId=selectedTaskId;
|
|
291
334
|
const collapsedTasks=new Set();
|
|
292
335
|
const expandedTasks=new Set();
|
|
@@ -408,11 +451,15 @@ function githubUsageCard(effective,status){
|
|
|
408
451
|
return card;
|
|
409
452
|
}
|
|
410
453
|
async function drawUsage(){try{const overview=await loadJSON('/api/usage');usageRoot.replaceChildren();usageMeta.textContent='Real account telemetry when available';const providers=Array.isArray(overview.providers)?overview.providers:[];const credits=Array.isArray(overview.credits)?overview.credits:[];const grid=element('div','usage-providers');if(overview.githubEffective||overview.githubStatus)grid.append(githubUsageCard(overview.githubEffective,overview.githubStatus));for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));if(!grid.children.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
|
|
454
|
+
function selectedWorkflowGraph(){return workflowGraphs.find(graph=>graph.id===selectedGraphId)||workflowGraphs[0]||null;}
|
|
455
|
+
function renderGraphList(){graphList.replaceChildren();if(!workflowGraphs.length){graphList.append(element('p','empty',text('No workflow graphs found.')));return;}for(const graph of workflowGraphs){const button=document.createElement('button');button.type='button';button.className='graph-button';button.setAttribute('aria-current',String(graph.id===selectedGraphId));button.append(element('strong','',text(graph.name)),element('span','',text(graph.path)));button.addEventListener('click',()=>{selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);renderGraphList();void renderSelectedGraph();});graphList.append(button);}}
|
|
456
|
+
async function renderSelectedGraph(){const graph=selectedWorkflowGraph();if(!graph){graphTitle.textContent='Select a graph';graphPath.textContent='';graphSource.textContent='';graphCanvas.replaceChildren(element('p','empty',text('No workflow graphs found.')));return;}selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);graphTitle.textContent=graph.name;graphPath.textContent=graph.path;graphSource.textContent=graph.source;graphCanvas.replaceChildren(element('p','subtle',text('Rendering…')));try{if(!window.mermaid)throw new Error('Mermaid renderer unavailable');if(!window.__relayMermaidReady){window.mermaid.initialize({startOnLoad:false,securityLevel:'strict'});window.__relayMermaidReady=true;}const rendered=await window.mermaid.render('relay-graph-'+graph.id.replace(/[^A-Za-z0-9_-]/g,'-'),graph.source);const parsed=new DOMParser().parseFromString(rendered.svg,'image/svg+xml');const svg=parsed.documentElement;if(svg.nodeName.toLowerCase()!=='svg')throw new Error('Mermaid did not return SVG');graphCanvas.replaceChildren(document.importNode(svg,true));}catch(error){graphCanvas.replaceChildren(element('p','error-state',text('Visual preview unavailable: '+error.message)));}}
|
|
457
|
+
async function drawGraphs(){try{const graphs=await loadJSON('/api/graphs');workflowGraphs=Array.isArray(graphs)?graphs:[];if(!workflowGraphs.some(graph=>graph.id===selectedGraphId))selectedGraphId=workflowGraphs[0]?.id||'';graphsMeta.textContent=workflowGraphs.length+' graph'+(workflowGraphs.length===1?'':'s')+' · workflow/*.mmd';renderGraphList();await renderSelectedGraph();}catch(error){graphsMeta.textContent='Unavailable';workflowGraphs=[];renderGraphList();graphCanvas.replaceChildren(element('p','error-state',text('Graphs unavailable: '+error.message)));}}
|
|
411
458
|
async function drawRules(){rulesStatus.textContent='';try{const rules=await loadJSON('/api/execution-router-rules');rulesText.value=typeof rules.text==='string'?rules.text:'';rulesMeta.textContent=rules.hash&&rules.hash!=='none'?'Loaded · '+rules.hash.slice(0,12):'No custom rules saved';}catch(error){rulesMeta.textContent='Unavailable';rulesStatus.textContent='Could not load rules: '+error.message;}}
|
|
412
459
|
async function saveRules(){rulesSave.disabled=true;rulesStatus.textContent='Saving…';try{const response=await fetch('/api/execution-router-rules',{method:'PUT',headers:{'content-type':'text/plain; charset=utf-8'},body:rulesText.value});if(!response.ok)throw new Error(response.status+' '+await response.text());const rules=await response.json();rulesMeta.textContent='Saved · '+rules.hash.slice(0,12);rulesStatus.textContent='Saved. New routing decisions will load these rules.';}catch(error){rulesStatus.textContent='Save failed: '+error.message;}finally{rulesSave.disabled=false;}}
|
|
413
460
|
async function drawShortcuts(){shortcutsStatus.textContent='';shortcutsText.value=chatShortcutText();}
|
|
414
461
|
function saveShortcuts(){const value=shortcutsText.value.trim();if(value)localStorage.setItem(CHAT_SHORTCUTS_KEY,value);else localStorage.removeItem(CHAT_SHORTCUTS_KEY);shortcutsText.value=chatShortcutText();shortcutsStatus.textContent='Saved.';}
|
|
415
|
-
function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isRules=view==='rules';const isShortcuts=view==='shortcuts';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isRules)void drawRules();else void drawShortcuts();}
|
|
462
|
+
function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isGraphs=view==='graphs';const isRules=view==='rules';const isShortcuts=view==='shortcuts';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;graphsView.hidden=!isGraphs;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));graphsTab.setAttribute('aria-selected',String(isGraphs));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isGraphs)void drawGraphs();else if(isRules)void drawRules();else void drawShortcuts();}
|
|
416
463
|
jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
|
|
417
464
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
418
465
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
@@ -426,19 +473,20 @@ mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJo
|
|
|
426
473
|
mobileJobs.addEventListener('change',syncMobileJobsView);
|
|
427
474
|
window.addEventListener('hashchange',syncMobileJobsView);
|
|
428
475
|
usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
|
|
476
|
+
graphsTab.addEventListener('click',()=>{location.hash='#/graphs';showView('graphs');});
|
|
429
477
|
rulesTab.addEventListener('click',()=>{location.hash='#/execution-router-rules';showView('rules');});
|
|
430
478
|
shortcutsTab.addEventListener('click',()=>{location.hash='#/shortcuts';showView('shortcuts');});
|
|
431
479
|
rulesSave.addEventListener('click',()=>void saveRules());
|
|
432
480
|
shortcutsSave.addEventListener('click',saveShortcuts);
|
|
433
|
-
window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const
|
|
481
|
+
window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','shortcuts'];const current=location.hash==='#/usage'?'usage':location.hash==='#/graphs'?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':'jobs';const next=views[(views.indexOf(current)+1)%views.length];location.hash=next==='usage'?'#/usage':next==='graphs'?'#/graphs':next==='rules'?'#/execution-router-rules':next==='shortcuts'?'#/shortcuts':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
|
|
434
482
|
function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
|
|
435
483
|
const stream=new EventSource('/events');
|
|
436
484
|
stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
|
|
437
485
|
stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
|
|
438
486
|
stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);renderAll();void drawRuntimeSummary();}};
|
|
439
487
|
stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);}if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
|
|
440
|
-
if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
|
|
441
|
-
setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/execution-router-rules'&&location.hash!=='#/shortcuts')void refresh();},300000);
|
|
488
|
+
if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/graphs')showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
|
|
489
|
+
setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/graphs'&&location.hash!=='#/execution-router-rules'&&location.hash!=='#/shortcuts')void refresh();},300000);
|
|
442
490
|
setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
|
|
443
491
|
setInterval(()=>void refreshVersion(),30000);
|
|
444
492
|
async function refreshSelected(){if(!selectedJob){await refresh();return;}selectedJobRefresh.disabled=true;selectedJobRefresh.textContent='Refreshing…';try{const updated=await loadJSON('/api/job?id='+encodeURIComponent(selectedJob));const index=jobs.findIndex(job=>job.id===selectedJob);if(index>=0)jobs[index]=updated;else jobs.push(updated);rememberSelection();renderAll();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';}finally{selectedJobRefresh.disabled=false;selectedJobRefresh.textContent='Refresh';}}
|
package/dist/dashboard.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { readUsageSnapshot } from './usage.js';
|
|
6
7
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
7
8
|
import { DeliveryDeduper, parseGitHubWebhook, verifyGitHubWebhook } from './github-webhook.js';
|
|
@@ -10,6 +11,16 @@ import { summarizeRuntime } from './runtime-summary.js';
|
|
|
10
11
|
const GITHUB_REPOSITORY_PATTERN = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
|
|
11
12
|
const SSE_HEARTBEAT_MS = 15000;
|
|
12
13
|
const EVENTS_HISTORY_FILE = process.env.EVENTS_BUS_DATA_DIR ? join(process.env.EVENTS_BUS_DATA_DIR, 'events.jsonl') : join(homedir(), 'Library', 'Application Support', 'EventsBus', 'events.jsonl');
|
|
14
|
+
const WORKFLOW_DIR = fileURLToPath(new URL('../workflow/', import.meta.url));
|
|
15
|
+
export async function dashboardWorkflowGraphs() {
|
|
16
|
+
const names = (await readdir(WORKFLOW_DIR)).filter(name => name.endsWith('.mmd')).sort();
|
|
17
|
+
return Promise.all(names.map(async (name) => ({
|
|
18
|
+
id: name.replace(/\.mmd$/, ''),
|
|
19
|
+
name: name.replace(/\.mmd$/, '').split('-').map(part => part === 'v2' ? 'V2' : part.charAt(0).toUpperCase() + part.slice(1)).join(' '),
|
|
20
|
+
path: `workflow/${name}`,
|
|
21
|
+
source: await readFile(join(WORKFLOW_DIR, name), 'utf8'),
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
13
24
|
function effectiveGitHubUsage(snapshot, runtime) {
|
|
14
25
|
const candidates = [snapshot?.resources.core, snapshot?.resources.graphql, snapshot?.resources.search, runtime?.resources.core, runtime?.resources.graphql, runtime?.resources.search]
|
|
15
26
|
.filter((candidate) => candidate !== undefined && candidate !== null)
|
|
@@ -171,6 +182,18 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
171
182
|
res.end(JSON.stringify({ version: dashboardVersion }));
|
|
172
183
|
return;
|
|
173
184
|
}
|
|
185
|
+
if (req.url === '/api/graphs') {
|
|
186
|
+
try {
|
|
187
|
+
const graphs = await dashboardWorkflowGraphs();
|
|
188
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
189
|
+
res.end(JSON.stringify(graphs));
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
res.writeHead(500);
|
|
193
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
174
197
|
if (req.url === '/webhooks/github' && req.method === 'POST') {
|
|
175
198
|
const chunks = [];
|
|
176
199
|
for await (const chunk of req)
|
package/dist/relayd.js
CHANGED
|
@@ -23,6 +23,7 @@ import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
|
23
23
|
import { decisionMode, LayaCliDecisionEngine } from './decision.js';
|
|
24
24
|
import { JsonlDecisionAuditStore } from './decision-audit.js';
|
|
25
25
|
import { summarizeRuntime } from './runtime-summary.js';
|
|
26
|
+
import { executionRouterRulesPath } from './routing-policy.js';
|
|
26
27
|
export async function overlayOperationalOverviews(overviews, storeFor) {
|
|
27
28
|
return await Promise.all(overviews.map(async (item) => {
|
|
28
29
|
const store = storeFor(item.job.repository, item.job.prNumber);
|
|
@@ -51,6 +52,8 @@ Options:
|
|
|
51
52
|
--concurrency N Maximum concurrent workers (default: 4)
|
|
52
53
|
--interval MS Recovery interval
|
|
53
54
|
--projection-ms MS GitHub projection flush interval (default: 1000)
|
|
55
|
+
--execution-router-rules PATH
|
|
56
|
+
Router Rules file (default: ~/.agents-relay/execution-router-rules.md)
|
|
54
57
|
--events nats Enable event wakeups
|
|
55
58
|
|
|
56
59
|
Legacy single-job mode also accepts --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
@@ -95,11 +98,18 @@ export function recoveryScanRepositories(repositories, cached, dormantProbeIndex
|
|
|
95
98
|
return activeOrUnknown;
|
|
96
99
|
return [...activeOrUnknown, dormant[Math.abs(dormantProbeIndex) % dormant.length]];
|
|
97
100
|
}
|
|
101
|
+
export function configureExecutionRouterRulesPath(argv) {
|
|
102
|
+
const explicitRulesPath = value(argv, '--execution-router-rules') || undefined;
|
|
103
|
+
if (explicitRulesPath)
|
|
104
|
+
process.env.AGENTS_RELAY_EXECUTION_ROUTER_RULES = executionRouterRulesPath(explicitRulesPath);
|
|
105
|
+
return executionRouterRulesPath();
|
|
106
|
+
}
|
|
98
107
|
export async function runDaemon(argv) {
|
|
99
108
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
100
109
|
console.log(daemonHelp);
|
|
101
110
|
return;
|
|
102
111
|
}
|
|
112
|
+
configureExecutionRouterRulesPath(argv);
|
|
103
113
|
const normalized = normalizeDaemonArguments(argv);
|
|
104
114
|
if (normalized.pullRequest && normalized.jobId) {
|
|
105
115
|
await startService([...argv]);
|
package/dist/routing-policy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
5
|
const EMPTY_POLICY = {};
|
|
5
6
|
function safePolicy(value) {
|
|
@@ -45,7 +46,7 @@ async function saveRouterRules(path, text) {
|
|
|
45
46
|
return { path, hash: createHash('sha256').update(text).digest('hex'), text };
|
|
46
47
|
}
|
|
47
48
|
export function executionRouterRulesPath(explicit) {
|
|
48
|
-
return explicit ?? process.env.AGENTS_RELAY_EXECUTION_ROUTER_RULES ?? join(
|
|
49
|
+
return explicit ?? process.env.AGENTS_RELAY_EXECUTION_ROUTER_RULES ?? join(homedir(), '.agents-relay', 'execution-router-rules.md');
|
|
49
50
|
}
|
|
50
51
|
export async function loadExecutionRouterRules(explicit) {
|
|
51
52
|
return await loadRouterRules(executionRouterRulesPath(explicit));
|