agents-relay 2.0.18 → 2.0.20

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.
@@ -451,7 +451,7 @@ function routeWorker(record){const id=record?.selected?.worker?.id||record?.work
451
451
  function renderAudit(records){auditLog.replaceChildren();if(!Array.isArray(records)||!records.length){auditLog.append(element('p','empty',text('No schema v5 routing decisions yet.')));return;}for(const record of records){const card=element('article','audit-entry');const head=element('div','audit-entry-head');head.append(element('strong','',text(routeWorker(record)+' · '+value(record?.selected?.model?.model))),element('span','subtle',text(new Date(record.timestamp).toLocaleString())));card.append(head);const correlation=record.correlation||{};card.append(element('div','subtle',text([correlation.job_id,correlation.task_id].filter(Boolean).join(' / ')||'uncorrelated')));const probabilities=record.decision_evidence?.probabilities||{};const gates=(record.gate_results||[]).filter(item=>!item.eligible).map(item=>item.candidate_id+': '+item.rejection_reasons.join(', '));const nl=String.fromCharCode(10);const details=document.createElement('pre');details.textContent='fallback: '+String(Boolean(record.decision_evidence?.fallback))+(Object.keys(probabilities).length?nl+'probabilities: '+JSON.stringify(probabilities,null,2):'')+(gates.length?nl+'rejected: '+gates.join(nl):'');card.append(details);auditLog.append(card);}}
452
452
  async function drawAudit(){auditRefresh.disabled=true;try{renderAudit(await loadJSON('/api/decision-audit'));}catch(error){auditLog.replaceChildren(element('p','error-state',text('Audit unavailable: '+error.message)));}finally{auditRefresh.disabled=false;}}
453
453
  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;}void drawAudit();}
454
- async function testRouter(){rulesTest.disabled=true;routerTestResults.replaceChildren(element('p','subtle',text('Running 10 Laya evals…')));try{const response=await fetch('/api/router-evals',{method:'POST'});if(!response.ok)throw new Error(response.status+' '+await response.text());const report=await response.json();routerTestResults.replaceChildren(element('strong','',text(report.passed+' / '+report.total+' passed')));for(const item of report.results||[]){const row=element('div','router-test-row');row.append(element('span','',text(item.id)),element('span',item.pass?'ok-text':'error-text',text(item.selectedWorker+' → expected '+item.expectedWorker)),element('span','subtle',text(item.pass?'PASS':'FAIL')));routerTestResults.append(row);}}catch(error){routerTestResults.replaceChildren(element('p','error-state',text('Router test failed: '+error.message)));}finally{rulesTest.disabled=false;}}
454
+ async function testRouter(){rulesTest.disabled=true;routerTestResults.replaceChildren(element('p','subtle',text('Calling router with current status…')));try{const response=await fetch('/api/router-test',{method:'POST'});if(!response.ok)throw new Error(response.status+' '+await response.text());const result=await response.json();const selected=result.selected||{};const current=result.current||{};routerTestResults.replaceChildren(element('strong','',text((selected.worker||'—')+' · '+(selected.model||'—'))));routerTestResults.append(element('div','subtle',text('route: '+(selected.route||'—'))),element('div','subtle',text('fallback: '+String(Boolean(result.fallback)))),element('pre','',text('probabilities: '+JSON.stringify(result.probabilities||{},null,2)+String.fromCharCode(10)+'browser workspace: '+JSON.stringify(current.browserWorkspace||null,null,2)+String.fromCharCode(10)+'chatgpt running: '+String(current.chatgptRunning??0))));}catch(error){routerTestResults.replaceChildren(element('p','error-state',text('Router test failed: '+error.message)));}finally{rulesTest.disabled=false;}}
455
455
  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;}}
456
456
  async function drawShortcuts(){shortcutsStatus.textContent='';shortcutsText.value=chatShortcutText();}
457
457
  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.';}
package/dist/dashboard.js CHANGED
@@ -57,7 +57,7 @@ async function eventHistory(jobId, taskId, limit) {
57
57
  }
58
58
  export { dashboardHtml, dashboardVersion } from './dashboard-html.js';
59
59
  import { dashboardHtml, dashboardVersion } from './dashboard-html.js';
60
- export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobId, usageRegistry, overviewProvider, webhook, statusProvider, githubUsageProvider, exactJobProvider, actions, routerEvalProvider, decisionAuditProvider) {
60
+ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobId, usageRegistry, overviewProvider, webhook, statusProvider, githubUsageProvider, exactJobProvider, actions, routerTestProvider, decisionAuditProvider) {
61
61
  const legacyActions = typeof exactJobProvider === 'object' && exactJobProvider !== null ? exactJobProvider : undefined;
62
62
  const legacyExactJobProvider = !exactJobProvider && githubUsageProvider ? githubUsageProvider : undefined;
63
63
  const resolvedExactJobProvider = typeof exactJobProvider === 'function' ? exactJobProvider : legacyExactJobProvider;
@@ -442,21 +442,21 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
442
442
  res.end('Method not allowed');
443
443
  return;
444
444
  }
445
- if (req.url === '/api/router-evals') {
445
+ if (req.url === '/api/router-test') {
446
446
  if (req.method !== 'POST') {
447
447
  res.writeHead(405, { allow: 'POST' });
448
448
  res.end('Method not allowed');
449
449
  return;
450
450
  }
451
- if (!routerEvalProvider) {
451
+ if (!routerTestProvider) {
452
452
  res.writeHead(503);
453
- res.end('Router evals unavailable');
453
+ res.end('Router test unavailable');
454
454
  return;
455
455
  }
456
456
  try {
457
- const report = await routerEvalProvider();
457
+ const result = await routerTestProvider();
458
458
  res.writeHead(200, { 'content-type': 'application/json' });
459
- res.end(JSON.stringify(report));
459
+ res.end(JSON.stringify(result));
460
460
  }
461
461
  catch (error) {
462
462
  res.writeHead(500);
@@ -145,11 +145,18 @@ export async function routeExecution(workers, requirements, options = {}) {
145
145
  profile: route.model.profile ?? null,
146
146
  reasoning: route.model.reasoning ?? null,
147
147
  resourceHealthy: route.model.resourceHealthy ?? null,
148
- resourceUsedPercent: route.model.resourceUsedPercent ?? null,
149
- resourceRemainingPercent: route.model.resourceRemainingPercent ?? null,
150
- resourceResetAt: route.model.resourceResetAt ?? null,
151
- resourceResetInMinutes: route.model.resourceResetInMinutes ?? null,
152
- resourceWindows: route.model.resourceWindows ?? null,
148
+ usage: {
149
+ five_hour: route.model.resourceWindows?.fiveHour ? {
150
+ remaining_percent: route.model.resourceWindows.fiveHour.remainingPercent ?? null,
151
+ reset_at: route.model.resourceWindows.fiveHour.resetAt ?? null,
152
+ reset_in_minutes: route.model.resourceWindows.fiveHour.resetInMinutes ?? null,
153
+ } : null,
154
+ weekly: route.model.resourceWindows?.weekly ? {
155
+ remaining_percent: route.model.resourceWindows.weekly.remainingPercent ?? null,
156
+ reset_at: route.model.resourceWindows.weekly.resetAt ?? null,
157
+ reset_in_minutes: route.model.resourceWindows.weekly.resetInMinutes ?? null,
158
+ } : null,
159
+ },
153
160
  },
154
161
  })),
155
162
  }));
package/dist/relayd.js CHANGED
@@ -24,7 +24,8 @@ import { decisionMode, LayaCliDecisionEngine } from './decision.js';
24
24
  import { JsonlDecisionAuditStore } from './decision-audit.js';
25
25
  import { summarizeRuntime } from './runtime-summary.js';
26
26
  import { executionRouterRulesPath } from './routing-policy.js';
27
- import { runRouterEvals } from './router-evals.js';
27
+ import { workerCandidates } from './worker-routing.js';
28
+ import { routeExecution } from './execution-routing.js';
28
29
  export async function overlayOperationalOverviews(overviews, storeFor) {
29
30
  return await Promise.all(overviews.map(async (item) => {
30
31
  const store = storeFor(item.job.repository, item.job.prNumber);
@@ -347,7 +348,42 @@ export async function runDaemon(argv) {
347
348
  taskCancel: workspaceTaskAction('cancel'),
348
349
  taskComplete: workspaceTaskComplete,
349
350
  taskSetState: workspaceTaskSetState,
350
- }, async () => await runRouterEvals(new LayaCliDecisionEngine()), async () => (await decisionAuditStore.read()).filter((record) => record.record_type === 'decision' && record.schema_version === 5));
351
+ }, async () => {
352
+ const usage = await usageRegistry.snapshot();
353
+ const runtime = summarizeRuntime(cachedOverviews.map(item => item.job));
354
+ const workers = workerCandidates([
355
+ { name: 'codex', id: 'codex', capabilities: ['model', 'codex'] },
356
+ { name: 'chatgpt', id: 'chatgpt/browser-worker', capabilities: ['model', 'chatgpt', 'browser-harness'] },
357
+ ], [], usage);
358
+ const result = await routeExecution(workers, { capabilities: [] }, {
359
+ decisionEngine: new LayaCliDecisionEngine(),
360
+ decisionMode: 'active',
361
+ taskFacts: { storySize: 'M', role: 'router-test' },
362
+ usage,
363
+ chatgptRunning: runtime.workers.chatgpt,
364
+ });
365
+ return {
366
+ selected: {
367
+ route: result.audit?.selected?.id,
368
+ worker: result.selection.workerId,
369
+ adapter: result.selection.adapter,
370
+ provider: result.candidate.routing.provider,
371
+ model: result.candidate.routing.model,
372
+ profile: result.candidate.routing.profile,
373
+ reasoning: result.candidate.routing.reasoning,
374
+ },
375
+ probabilities: result.candidate.routing.decisionEvidence?.probabilities ?? {},
376
+ fallback: result.candidate.routing.decisionEvidence?.fallback ?? false,
377
+ current: {
378
+ chatgptRunning: runtime.workers.chatgpt,
379
+ browserWorkspace: usage.browserWorkspace ?? null,
380
+ providers: usage.providers,
381
+ credits: usage.credits,
382
+ },
383
+ decisionInput: result.audit?.decision_input ?? null,
384
+ gates: result.audit?.gate_results ?? [],
385
+ };
386
+ }, async () => (await decisionAuditStore.read()).filter((record) => record.record_type === 'decision' && record.schema_version === 5));
351
387
  await once(server, 'listening');
352
388
  const reconcileSafely = async () => {
353
389
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {