agents-relay 1.0.105 → 1.0.106

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.
@@ -14,6 +14,9 @@ export const dashboardHtml = `<!doctype html>
14
14
  header { position:sticky; top:0; z-index:3; background:var(--surface); border-bottom:1px solid var(--border); padding:.8rem 1rem; }
15
15
  .brand { display:flex; align-items:baseline; gap:.65rem; min-width:0; }
16
16
  .version { margin-left:auto; color:var(--muted); font-size:.72rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; white-space:nowrap; }
17
+ .runtime-summary { display:flex; gap:.4rem; align-items:center; flex-wrap:wrap; margin-top:.55rem; }
18
+ .runtime-stat { padding:.18rem .45rem; border:1px solid var(--border); border-radius:999px; background:var(--surface2); color:var(--muted); font-size:.72rem; white-space:nowrap; }
19
+ .runtime-stat strong { color:var(--text); }
17
20
  h1 { margin:0; font-size:1.08rem; letter-spacing:-.02em; }
18
21
  .tagline,.subtle { color:var(--muted); font-size:.78rem; }
19
22
  .warning-banner { margin:.65rem 0 0; padding:.5rem .65rem; border:1px solid #e6bf62; border-radius:.4rem; background:#fff4d6; color:#6f4b00; font-size:.78rem; font-weight:700; }
@@ -86,7 +89,7 @@ export const dashboardHtml = `<!doctype html>
86
89
  .event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
87
90
  .event-kind { color:var(--accent); font-weight:800; }
88
91
  .event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
89
- #usage-view,#rules-view { grid-column:1 / -1; }
92
+ #usage-view,#rules-view,#worker-rules-view { grid-column:1 / -1; }
90
93
  .rules-editor { display:grid; gap:.65rem; }
91
94
  .rules-help { margin:0; color:var(--muted); font-size:.8rem; }
92
95
  .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; }
@@ -113,11 +116,13 @@ export const dashboardHtml = `<!doctype html>
113
116
  <body>
114
117
  <header>
115
118
  <div class="brand"><h1>Agents Relay Console</h1><p class="tagline">Durable PR state is authoritative; events are observability.</p><span class="version">v${packageVersion}</span></div>
119
+ <div id="runtime-summary" class="runtime-summary" aria-label="Runtime summary"></div>
116
120
  <div id="rate-limit-warning" class="warning-banner" role="status" hidden></div>
117
121
  <nav role="tablist" aria-label="Console views">
118
122
  <button id="jobs-tab" type="button" role="tab" aria-selected="true" aria-controls="jobs-view">Jobs</button>
119
123
  <button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
120
124
  <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Model Router Rules</button>
125
+ <button id="worker-rules-tab" type="button" role="tab" aria-selected="false" aria-controls="worker-rules-view">Worker Router Rules</button>
121
126
  </nav>
122
127
  </header>
123
128
  <main>
@@ -159,6 +164,18 @@ export const dashboardHtml = `<!doctype html>
159
164
  </div>
160
165
  </div>
161
166
  </section>
167
+ <section id="worker-rules-view" role="tabpanel" aria-labelledby="worker-rules-tab" hidden>
168
+ <div class="panel">
169
+ <div class="panel-head"><h2>Worker Router Rules</h2><span id="worker-rules-meta" class="subtle"></span></div>
170
+ <div class="panel-body">
171
+ <div class="rules-editor">
172
+ <p class="rules-help">Free-text guidance loaded by the worker decider when choosing ChatGPT versus Codex. Use this for local preferences; worker legality and Model Router remain separate.</p>
173
+ <textarea id="worker-rules-text" class="rules-textarea" spellcheck="true" aria-label="Worker router rules" placeholder="Example: Prefer ChatGPT unless the task requires native local execution or computer-use that ChatGPT plus MacBridge cannot perform."></textarea>
174
+ <div class="rules-footer"><span id="worker-rules-status" class="subtle" aria-live="polite"></span><button id="worker-rules-save" class="rules-save" type="button">Save rules</button></div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </section>
162
179
  </main>
163
180
  <script>
164
181
  'use strict';
@@ -178,9 +195,11 @@ const MAX_EVENTS=60;
178
195
  const jobsTab=document.querySelector('#jobs-tab');
179
196
  const usageTab=document.querySelector('#usage-tab');
180
197
  const rulesTab=document.querySelector('#rules-tab');
198
+ const workerRulesTab=document.querySelector('#worker-rules-tab');
181
199
  const jobsView=document.querySelector('#jobs-view');
182
200
  const usageView=document.querySelector('#usage-view');
183
201
  const rulesView=document.querySelector('#rules-view');
202
+ const workerRulesView=document.querySelector('#worker-rules-view');
184
203
  const jobsWorkspace=document.querySelector('#jobs-workspace');
185
204
  const jobList=document.querySelector('#job-list');
186
205
  const jobsRefresh=document.querySelector('#jobs-refresh');
@@ -204,7 +223,12 @@ const rulesText=document.querySelector('#rules-text');
204
223
  const rulesMeta=document.querySelector('#rules-meta');
205
224
  const rulesStatus=document.querySelector('#rules-status');
206
225
  const rulesSave=document.querySelector('#rules-save');
226
+ const workerRulesText=document.querySelector('#worker-rules-text');
227
+ const workerRulesMeta=document.querySelector('#worker-rules-meta');
228
+ const workerRulesStatus=document.querySelector('#worker-rules-status');
229
+ const workerRulesSave=document.querySelector('#worker-rules-save');
207
230
  const rateLimitWarning=document.querySelector('#rate-limit-warning');
231
+ const runtimeSummary=document.querySelector('#runtime-summary');
208
232
  let jobs=[];
209
233
  let selectedJob=sessionStorage.getItem('agents-relay:selected-job');
210
234
  let selectedTaskId=sessionStorage.getItem('agents-relay:selected-task');
@@ -283,8 +307,10 @@ function renderRateLimitStatus(status){
283
307
  rateLimitWarning.textContent='GitHub API rate limit reached. Showing cached job data.'+suffix;
284
308
  rateLimitWarning.hidden=false;
285
309
  }
310
+ function runtimePill(label,value){const pill=element('span','runtime-stat');pill.append(document.createTextNode(label+' '),element('strong','',text(value)));return pill;}
311
+ async function drawRuntimeSummary(){try{const summary=await loadJSON('/api/runtime-summary');runtimeSummary.replaceChildren(runtimePill('Active Jobs',summary.activeJobs||0),runtimePill('Running Tasks',summary.runningTasks||0),runtimePill('ChatGPT',summary.workers?.chatgpt||0),runtimePill('Codex',summary.workers?.codex||0));for(const item of Array.isArray(summary.models)?summary.models:[])runtimeSummary.append(runtimePill(item.model,item.count));}catch{runtimeSummary.replaceChildren();}}
286
312
  async function refreshStatus(){try{renderRateLimitStatus(await loadJSON('/api/status'));}catch{renderRateLimitStatus(null);}}
287
- async function refresh(force=false){jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';try{const discovered=await loadJSON('/api/jobs'+(force?'?refresh=1':''));jobs=Array.isArray(discovered)?discovered:[];jobs.sort(compareJobs);if(selectedJob&&!jobs.some(job=>job.id===selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;selectedJob=jobs.some(job=>job.id===fromHash)?fromHash:(jobs[0]?.id||null);}rememberSelection();renderAll();void loadEventHistory();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';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{jobsRefresh.disabled=false;jobsRefresh.textContent='Refresh';}}
313
+ async function refresh(force=false){jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';try{const discovered=await loadJSON('/api/jobs'+(force?'?refresh=1':''));jobs=Array.isArray(discovered)?discovered:[];void drawRuntimeSummary();jobs.sort(compareJobs);if(selectedJob&&!jobs.some(job=>job.id===selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;selectedJob=jobs.some(job=>job.id===fromHash)?fromHash:(jobs[0]?.id||null);}rememberSelection();renderAll();void loadEventHistory();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';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{jobsRefresh.disabled=false;jobsRefresh.textContent='Refresh';}}
288
314
  function renderEventScope(){const task=eventTaskId&&selected()?.tasks?.find(candidate=>candidate.id===eventTaskId);eventsAllTasks.disabled=!task;eventsAllTasks.setAttribute('aria-pressed',String(!task));eventsScope.textContent=task?'Task '+task.id+' · newest first · 60 max':'Job-wide · newest first · 60 max';}
289
315
  function eventLabel(type){return ({'task.process.launched':'Process launched','task.started':'Worker started','task.process.exited':'Sync process exited','task.process.async_exited':'Async process exited','task.completed':'Worker completed','task.failed':'Worker failed','task.blocked':'Worker blocked','review.started':'Review started','review.approved':'Review approved','review.changes_required':'Review changes required','review.blocked':'Review blocked'})[type]||type||'Event';}
290
316
  function addEvent(event,level){if(!event||selectedJob&&event.job_id&&event.job_id!==selectedJob||eventTaskId&&event.task_id!==eventTaskId)return;if(event.event_id&&displayedEventIds.has(event.event_id))return;if(event.event_id)displayedEventIds.add(event.event_id);const item=element('li','event-item');const meta=element('div','event-meta');meta.append(element('span','event-kind',text(eventLabel(event.type))),element('span','event-type',text(event.type||'event')),element('span','',text(relativeTime(event.timestamp))),element('span','',text('task: '+value(event.task_id))));item.append(meta);const message=element('p','event-message',text(event.message||'Event received'));if(level==='error'||event.level==='error')message.classList.add('error-text');item.append(message);eventFeed.querySelector('.empty')?.remove();eventFeed.prepend(item);while(eventFeed.children.length>MAX_EVENTS)eventFeed.lastElementChild?.remove();}
@@ -296,7 +322,9 @@ function creditProvider(provider){const row=element('section','usage-provider');
296
322
  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:[];if(!providers.length&&!credits.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}const grid=element('div','usage-providers');for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
297
323
  async function drawRules(){rulesStatus.textContent='';try{const rules=await loadJSON('/api/model-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;}}
298
324
  async function saveRules(){rulesSave.disabled=true;rulesStatus.textContent='Saving…';try{const response=await fetch('/api/model-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;}}
299
- function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isRules=view==='rules';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;rulesView.hidden=!isRules;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));rulesTab.setAttribute('aria-selected',String(isRules));if(isJobs)void refresh();else if(isUsage)void drawUsage();else void drawRules();}
325
+ async function drawWorkerRules(){workerRulesStatus.textContent='';try{const rules=await loadJSON('/api/worker-router-rules');workerRulesText.value=typeof rules.text==='string'?rules.text:'';workerRulesMeta.textContent=rules.hash&&rules.hash!=='none'?'Loaded · '+rules.hash.slice(0,12):'No custom rules saved';}catch(error){workerRulesMeta.textContent='Unavailable';workerRulesStatus.textContent='Could not load rules: '+error.message;}}
326
+ async function saveWorkerRules(){workerRulesSave.disabled=true;workerRulesStatus.textContent='Saving…';try{const response=await fetch('/api/worker-router-rules',{method:'PUT',headers:{'content-type':'text/plain; charset=utf-8'},body:workerRulesText.value});if(!response.ok)throw new Error(response.status+' '+await response.text());const rules=await response.json();workerRulesMeta.textContent='Saved · '+rules.hash.slice(0,12);workerRulesStatus.textContent='Saved. New worker decisions will load these rules.';}catch(error){workerRulesStatus.textContent='Save failed: '+error.message;}finally{workerRulesSave.disabled=false;}}
327
+ function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isRules=view==='rules';const isWorkerRules=view==='worker-rules';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;rulesView.hidden=!isRules;workerRulesView.hidden=!isWorkerRules;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));rulesTab.setAttribute('aria-selected',String(isRules));workerRulesTab.setAttribute('aria-selected',String(isWorkerRules));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isRules)void drawRules();else void drawWorkerRules();}
300
328
  jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
301
329
  jobsRefresh.addEventListener('click',()=>void refresh(true));
302
330
  eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
@@ -308,16 +336,18 @@ quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status
308
336
  selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
309
337
  usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
310
338
  rulesTab.addEventListener('click',()=>{location.hash='#/model-router-rules';showView('rules');});
339
+ workerRulesTab.addEventListener('click',()=>{location.hash='#/worker-router-rules';showView('worker-rules');});
311
340
  rulesSave.addEventListener('click',()=>void saveRules());
312
- window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const next=location.hash==='#/usage'?'rules':location.hash==='#/model-router-rules'?'jobs':'usage';location.hash=next==='usage'?'#/usage':next==='rules'?'#/model-router-rules':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
341
+ workerRulesSave.addEventListener('click',()=>void saveWorkerRules());
342
+ window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const next=location.hash==='#/usage'?'rules':location.hash==='#/model-router-rules'?'worker-rules':location.hash==='#/worker-router-rules'?'jobs':'usage';location.hash=next==='usage'?'#/usage':next==='rules'?'#/model-router-rules':next==='worker-rules'?'#/worker-router-rules':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
313
343
  function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
314
344
  const stream=new EventSource('/events');
315
345
  stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
316
346
  stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
317
- 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();}};
318
- 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();});
319
- if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/model-router-rules')showView('rules');else showView('jobs');
320
- setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/model-router-rules')void refresh();},300000);
347
+ 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();}};
348
+ 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();});
349
+ if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/model-router-rules')showView('rules');else if(location.hash==='#/worker-router-rules')showView('worker-rules');else showView('jobs');
350
+ setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/model-router-rules'&&location.hash!=='#/worker-router-rules')void refresh();},300000);
321
351
  setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
322
352
  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';}}
323
353
  </script>
package/dist/dashboard.js CHANGED
@@ -5,7 +5,8 @@ import { join } from 'node:path';
5
5
  import { readUsageSnapshot } from './usage.js';
6
6
  import { deriveJobRuntimeStatus } from './runtime-status.js';
7
7
  import { DeliveryDeduper, parseGitHubWebhook, verifyGitHubWebhook } from './github-webhook.js';
8
- import { loadModelRouterRules, saveModelRouterRules } from './routing-policy.js';
8
+ import { loadModelRouterRules, loadWorkerRouterRules, saveModelRouterRules, saveWorkerRouterRules } from './routing-policy.js';
9
+ import { summarizeRuntime } from './runtime-summary.js';
9
10
  const GITHUB_REPOSITORY_PATTERN = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
10
11
  const SSE_HEARTBEAT_MS = 15000;
11
12
  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');
@@ -309,6 +310,52 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
309
310
  res.end('Method not allowed');
310
311
  return;
311
312
  }
313
+ if (req.url === '/api/worker-router-rules') {
314
+ if (req.method === 'GET') {
315
+ try {
316
+ const rules = await loadWorkerRouterRules();
317
+ res.writeHead(200, { 'content-type': 'application/json' });
318
+ res.end(JSON.stringify(rules));
319
+ }
320
+ catch (error) {
321
+ res.writeHead(500);
322
+ res.end(error instanceof Error ? error.message : String(error));
323
+ }
324
+ return;
325
+ }
326
+ if (req.method === 'PUT') {
327
+ const chunks = [];
328
+ let size = 0;
329
+ for await (const chunk of req) {
330
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
331
+ size += buffer.length;
332
+ if (size > 64 * 1024) {
333
+ res.writeHead(413);
334
+ res.end('Worker router rules exceed 64 KiB');
335
+ return;
336
+ }
337
+ chunks.push(buffer);
338
+ }
339
+ try {
340
+ const rules = await saveWorkerRouterRules(Buffer.concat(chunks).toString('utf8'));
341
+ res.writeHead(200, { 'content-type': 'application/json' });
342
+ res.end(JSON.stringify(rules));
343
+ }
344
+ catch (error) {
345
+ res.writeHead(500);
346
+ res.end(error instanceof Error ? error.message : String(error));
347
+ }
348
+ return;
349
+ }
350
+ res.writeHead(405, { allow: 'GET, PUT' });
351
+ res.end('Method not allowed');
352
+ return;
353
+ }
354
+ if (req.url === '/api/runtime-summary') {
355
+ res.writeHead(200, { 'content-type': 'application/json' });
356
+ res.end(JSON.stringify(summarizeRuntime(lastKnownJobs ?? [])));
357
+ return;
358
+ }
312
359
  if (req.url === '/api/status') {
313
360
  const status = statusProvider ? statusProvider() : { limited: false, cooldownUntil: null };
314
361
  res.writeHead(200, { 'content-type': 'application/json' });
@@ -0,0 +1,38 @@
1
+ function runningWorker(task) {
2
+ if (task.state !== 'RUNNING')
3
+ return null;
4
+ const adapter = task.workerSelection?.adapter ?? task.adapter;
5
+ return adapter === 'chatgpt' || adapter === 'codex' ? adapter : null;
6
+ }
7
+ export function summarizeRuntime(jobs) {
8
+ let activeJobs = 0;
9
+ let runningTasks = 0;
10
+ let chatgpt = 0;
11
+ let codex = 0;
12
+ const models = new Map();
13
+ for (const job of jobs) {
14
+ if (job.state === 'ACTIVE')
15
+ activeJobs += 1;
16
+ for (const task of job.tasks) {
17
+ if (task.state !== 'RUNNING')
18
+ continue;
19
+ runningTasks += 1;
20
+ const worker = runningWorker(task);
21
+ if (worker === 'chatgpt')
22
+ chatgpt += 1;
23
+ if (worker === 'codex')
24
+ codex += 1;
25
+ const model = task.routing?.model;
26
+ if (model)
27
+ models.set(model, (models.get(model) ?? 0) + 1);
28
+ }
29
+ }
30
+ return {
31
+ activeJobs,
32
+ runningTasks,
33
+ workers: { chatgpt, codex },
34
+ models: [...models.entries()]
35
+ .map(([model, count]) => ({ model, count }))
36
+ .sort((left, right) => right.count - left.count || left.model.localeCompare(right.model)),
37
+ };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.105",
3
+ "version": "1.0.106",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {