agents-relay 1.0.102 → 1.0.103

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.
@@ -260,7 +260,8 @@ async function refresh(force=false){jobsRefresh.disabled=true;jobsRefresh.textCo
260
260
  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';}
261
261
  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';}
262
262
  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();}
263
- function usageBar(label,windowData){const percent=Math.max(0,Math.min(100,100-windowData.usedPercent));const row=element('div','usage-row');const heading=element('div','usage-label');heading.append(element('span','',text(label)),element('span','',text(Math.round(percent)+'%')));const track=element('div','usage-track');track.setAttribute('role','progressbar');track.setAttribute('aria-label',label+' available');track.setAttribute('aria-valuemin','0');track.setAttribute('aria-valuemax','100');track.setAttribute('aria-valuenow',String(percent));const fill=element('div','usage-fill');fill.style.width=percent+'%';track.append(fill);row.append(heading,track);return row;}
263
+ function resetText(windowData){if(!Number.isFinite(windowData.resetsAt))return '';const at=new Date(windowData.resetsAt*1000);const minutes=Math.max(0,Math.ceil((at.getTime()-Date.now())/60000));const relative=minutes<60?minutes+'m':minutes<1440?Math.floor(minutes/60)+'h '+minutes%60+'m':Math.floor(minutes/1440)+'d '+Math.floor((minutes%1440)/60)+'h';return 'Resets '+at.toLocaleString()+' · in '+relative;}
264
+ function usageBar(label,windowData){const percent=Math.max(0,Math.min(100,100-windowData.usedPercent));const row=element('div','usage-row');const heading=element('div','usage-label');heading.append(element('span','',text(label)),element('span','',text(Math.round(percent)+'%')));const track=element('div','usage-track');track.setAttribute('role','progressbar');track.setAttribute('aria-label',label+' available');track.setAttribute('aria-valuemin','0');track.setAttribute('aria-valuemax','100');track.setAttribute('aria-valuenow',String(percent));const fill=element('div','usage-fill');fill.style.width=percent+'%';track.append(fill);row.append(heading,track);const reset=resetText(windowData);if(reset)row.append(element('div','subtle',text(reset)));return row;}
264
265
  function usageProvider(provider){const card=element('section','usage-provider');card.append(element('h3','',text(provider.label)));const usage=provider.usage||{};if(!usage.available||!usage.fiveHour||!usage.weekly){card.append(element('p','empty',text(usage.reason||'Account usage telemetry is unavailable.')));return card;}const bars=element('div','usage-bars');bars.append(usageBar('5-hour remaining',usage.fiveHour),usageBar('Weekly remaining',usage.weekly));card.append(bars);return card;}
265
266
  function formatCredit(value,currency){if(value===null||value===undefined||!Number.isFinite(Number(value)))return '—';const number=Number(value);if(currency==='USD')return '$'+number.toFixed(2);if(currency==='CNY')return '¥'+number.toFixed(2);return number.toFixed(2)+' '+currency;}
266
267
  function creditProvider(provider){const row=element('section','usage-provider');row.append(element('h3','',text(provider.label)));const credits=provider.credits||{};if(!credits.available||!Array.isArray(credits.balances)||!credits.balances.length){row.append(element('p','empty',text(credits.reason||'Credit balance is unavailable.')));return row;}const balance=credits.balances[0];row.append(element('div','detail-value',text(formatCredit(balance.remaining,balance.currency))));return row;}
@@ -38,7 +38,7 @@ async function readConfig(path) {
38
38
  }
39
39
  function withResource(candidate, usage) {
40
40
  const resource = usageForProvider(usage, candidate.provider);
41
- return { ...candidate, resourceHealthy: resource.healthy, resourceUsedPercent: resource.usedPercent };
41
+ return { ...candidate, resourceHealthy: resource.healthy, resourceUsedPercent: resource.usedPercent, resourceRemainingPercent: resource.remainingPercent, resourceResetAt: resource.resetAt, resourceResetInMinutes: resource.resetInMinutes, resourceWindows: resource.windows };
42
42
  }
43
43
  export async function discoverModelCandidates(worker, registered, usage, codexHome = process.env.CODEX_HOME ?? join(homedir(), '.codex')) {
44
44
  const registration = registered.find(item => item.id === worker.workerId);
@@ -105,9 +105,9 @@ export async function routeModel(worker, candidates, options = {}) {
105
105
  facts: JSON.parse(JSON.stringify({
106
106
  task: options.taskFacts ?? {},
107
107
  worker: { id: worker.workerId, adapter: worker.adapter, capabilities: [...worker.capabilities] },
108
- candidates: eligible.map(item => ({ id: item.id, provider: item.provider, model: item.model, profile: item.profile ?? null, reasoning: item.reasoning ?? null, resourceHealthy: item.resourceHealthy ?? null, resourceUsedPercent: item.resourceUsedPercent ?? null })),
108
+ candidates: eligible.map(item => ({ id: item.id, provider: item.provider, model: item.model, profile: item.profile ?? null, reasoning: item.reasoning ?? null, resourceHealthy: item.resourceHealthy ?? null, resourceUsedPercent: item.resourceUsedPercent ?? null, resourceRemainingPercent: item.resourceRemainingPercent ?? null, resourceResetAt: item.resourceResetAt ?? null, resourceResetInMinutes: item.resourceResetInMinutes ?? null, resourceWindows: item.resourceWindows ?? null })),
109
109
  })),
110
- }, { type: 'choice', question: 'Which eligible model profile is best suited to execute this task?', choices: eligible.map(item => item.id) }, () => deterministic.id);
110
+ }, { type: 'choice', question: 'Which eligible model profile is best suited to execute this task? Prefer spending otherwise-expiring paid quota on stronger reasoning when reset is near and substantial capacity remains, but consider every quota window so a tighter longer-horizon limit is not needlessly depleted; never violate task fit or continuity.', choices: eligible.map(item => item.id) }, () => deterministic.id);
111
111
  const selected = eligible.find(item => item.id === choice.choice) ?? deterministic;
112
112
  return {
113
113
  candidate: selected,
@@ -1,9 +1,31 @@
1
1
  import { chooseWithDecision } from './decision.js';
2
- export function usageForProvider(snapshot, provider) {
2
+ export function usageForProvider(snapshot, provider, now = Date.now()) {
3
3
  const usage = snapshot?.providers.find(item => item.id === provider)?.usage;
4
4
  if (usage?.available) {
5
5
  const usedPercent = Math.max(usage.fiveHour?.usedPercent ?? 0, usage.weekly?.usedPercent ?? 0);
6
- return { healthy: usedPercent < 100, usedPercent };
6
+ const resourceWindow = (window) => {
7
+ if (!window)
8
+ return undefined;
9
+ const resetAt = window.resetsAt;
10
+ return {
11
+ remainingPercent: 100 - window.usedPercent,
12
+ resetAt,
13
+ resetInMinutes: resetAt === null ? null : Math.max(0, Math.ceil((resetAt * 1000 - now) / 60_000)),
14
+ };
15
+ };
16
+ const windows = { fiveHour: resourceWindow(usage.fiveHour), weekly: resourceWindow(usage.weekly) };
17
+ const resetWindow = [usage.fiveHour, usage.weekly]
18
+ .filter((window) => window?.resetsAt !== null && window?.resetsAt !== undefined)
19
+ .filter(window => window.resetsAt * 1000 > now)
20
+ .sort((left, right) => left.resetsAt - right.resetsAt)[0];
21
+ return {
22
+ healthy: usedPercent < 100,
23
+ usedPercent,
24
+ remainingPercent: resetWindow ? 100 - resetWindow.usedPercent : undefined,
25
+ resetAt: resetWindow?.resetsAt ?? undefined,
26
+ resetInMinutes: resetWindow?.resetsAt ? Math.max(0, Math.ceil((resetWindow.resetsAt * 1000 - now) / 60_000)) : undefined,
27
+ windows,
28
+ };
7
29
  }
8
30
  const credits = snapshot?.credits.find(item => item.id === provider)?.credits;
9
31
  if (credits?.available) {
@@ -34,6 +56,10 @@ export function workerCandidates(adapters, registered, snapshot) {
34
56
  resourceHealthy: resource.healthy,
35
57
  resourceProvider: provider,
36
58
  resourceUsedPercent: resource.usedPercent,
59
+ resourceRemainingPercent: resource.remainingPercent,
60
+ resourceResetAt: resource.resetAt,
61
+ resourceResetInMinutes: resource.resetInMinutes,
62
+ resourceWindows: resource.windows,
37
63
  };
38
64
  });
39
65
  });
@@ -76,6 +102,10 @@ function selection(selected, selectedBy, decisionEvidence) {
76
102
  resourceHealthy: selected.resourceHealthy,
77
103
  resourceProvider: selected.resourceProvider,
78
104
  resourceUsedPercent: selected.resourceUsedPercent,
105
+ resourceRemainingPercent: selected.resourceRemainingPercent,
106
+ resourceResetAt: selected.resourceResetAt,
107
+ resourceResetInMinutes: selected.resourceResetInMinutes,
108
+ resourceWindows: selected.resourceWindows,
79
109
  decisionEvidence,
80
110
  };
81
111
  }
@@ -108,6 +138,10 @@ export async function routeWorker(candidates, requirements, options = {}) {
108
138
  resourceHealthy: item.resourceHealthy ?? null,
109
139
  resourceProvider: item.resourceProvider ?? null,
110
140
  resourceUsedPercent: item.resourceUsedPercent ?? null,
141
+ resourceRemainingPercent: item.resourceRemainingPercent ?? null,
142
+ resourceResetAt: item.resourceResetAt ?? null,
143
+ resourceResetInMinutes: item.resourceResetInMinutes ?? null,
144
+ resourceWindows: item.resourceWindows ?? null,
111
145
  })),
112
146
  }));
113
147
  const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', { facts }, { type: 'choice', question: 'Which eligible worker is best suited to execute this task?', choices: eligible.map(item => item.workerId) }, () => deterministic.workerId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.102",
3
+ "version": "1.0.103",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {