agents-relay 1.0.101 → 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.
package/dist/dashboard-html.js
CHANGED
|
@@ -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
|
|
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;}
|
package/dist/model-routing.js
CHANGED
|
@@ -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,
|
|
@@ -86,6 +86,27 @@ export class ReconcilerRuntimeBase {
|
|
|
86
86
|
}
|
|
87
87
|
async idle() { while (this.live.size > 0 || this.plannerLive.size > 0 || this.continuationLive.size > 0 || this.reconciling)
|
|
88
88
|
await new Promise(resolve => setTimeout(resolve, 25)); }
|
|
89
|
+
async close() {
|
|
90
|
+
for (const live of this.live.values()) {
|
|
91
|
+
live.controller.abort();
|
|
92
|
+
live.execution.cancel();
|
|
93
|
+
}
|
|
94
|
+
this.live.clear();
|
|
95
|
+
for (const timer of this.terminalTimers.values())
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
this.terminalTimers.clear();
|
|
98
|
+
for (const timer of this.terminalExitGraceTimers.values())
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
this.terminalExitGraceTimers.clear();
|
|
101
|
+
for (const planner of this.plannerLive.values()) {
|
|
102
|
+
clearTimeout(planner.timer);
|
|
103
|
+
planner.controller.abort();
|
|
104
|
+
}
|
|
105
|
+
this.plannerLive.clear();
|
|
106
|
+
for (const waiter of this.terminalWaiters.values())
|
|
107
|
+
waiter.resolve();
|
|
108
|
+
this.terminalWaiters.clear();
|
|
109
|
+
}
|
|
89
110
|
async wake(jobId) { return this.reconcile(jobId); }
|
|
90
111
|
async startAck(jobId, taskId, sourceId, launchBarrier, timeoutMs = this.options.startAckTimeoutMs ?? 60000) {
|
|
91
112
|
if (!this.options.eventBus)
|
package/dist/reconciler.js
CHANGED
|
@@ -146,8 +146,13 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
146
146
|
this.plannerLive.get(task.id)?.controller.abort();
|
|
147
147
|
}
|
|
148
148
|
const beforeSchedule = await this.store.load(jobId);
|
|
149
|
+
const taskStatesBeforeSchedule = new Map(beforeSchedule.tasks.map(task => [task.id, task.state]));
|
|
149
150
|
const scheduleBefore = JSON.stringify(beforeSchedule.schedule ?? null);
|
|
150
151
|
let scheduled = schedule(beforeSchedule);
|
|
152
|
+
for (const task of scheduled.tasks) {
|
|
153
|
+
if (taskStatesBeforeSchedule.get(task.id) !== task.state)
|
|
154
|
+
await this.store.saveTask(task);
|
|
155
|
+
}
|
|
151
156
|
if (scheduled.state !== previousState || JSON.stringify(scheduled.schedule ?? null) !== scheduleBefore)
|
|
152
157
|
await this.store.saveJob(scheduled);
|
|
153
158
|
if (prState === 'OPEN') {
|
package/dist/worker-routing.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { parseExecutableMermaid, WorkflowEngine } from './mermaid.js';
|
|
3
|
-
import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_MUTATION_NAMES } from './contracts.js';
|
|
3
|
+
import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_FACT_NAMES, WORKFLOW_MUTATION_NAMES, classifyWorkflowActions } from './contracts.js';
|
|
4
4
|
function declarativeActions() {
|
|
5
5
|
return Object.fromEntries([...WORKFLOW_MUTATION_NAMES, ...WORKFLOW_EFFECT_NAMES].map(name => [name, { declarative: true }]));
|
|
6
6
|
}
|
|
7
|
-
function
|
|
8
|
-
const url = new URL(`../../workflow/${name}.mmd`, import.meta.url);
|
|
9
|
-
const graph = parseExecutableMermaid(readFileSync(url, 'utf8'));
|
|
7
|
+
export function validateCanonicalWorkflowGraph(graph) {
|
|
10
8
|
const canonicalEvents = new Set(WORKFLOW_EVENT_NAMES);
|
|
9
|
+
const canonicalFacts = new Set(WORKFLOW_FACT_NAMES);
|
|
11
10
|
for (const transition of graph.transitions) {
|
|
12
11
|
if (!canonicalEvents.has(transition.event)) {
|
|
13
12
|
throw new Error(`Unknown canonical workflow event ${transition.event} referenced at line ${transition.line}`);
|
|
14
13
|
}
|
|
14
|
+
if (transition.guard && !canonicalFacts.has(transition.guard)) {
|
|
15
|
+
throw new Error(`Unknown canonical workflow fact ${transition.guard} referenced at line ${transition.line}`);
|
|
16
|
+
}
|
|
17
|
+
classifyWorkflowActions(transition.actions);
|
|
15
18
|
}
|
|
19
|
+
}
|
|
20
|
+
function load(name, guards = {}) {
|
|
21
|
+
const url = new URL(`../../workflow/${name}.mmd`, import.meta.url);
|
|
22
|
+
const graph = parseExecutableMermaid(readFileSync(url, 'utf8'));
|
|
23
|
+
validateCanonicalWorkflowGraph(graph);
|
|
16
24
|
return new WorkflowEngine(graph, guards, declarativeActions());
|
|
17
25
|
}
|
|
18
26
|
const booleanGuard = (name) => context => context[name] === true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-relay",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.103",
|
|
4
4
|
"description": "Durable async agent jobs coordinated through GitHub pull requests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js dist/relayd.js",
|
|
12
|
-
"test": "npm run build && node --test
|
|
12
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
13
13
|
"test:chatgpt-temp-quality": "AGENTS_RELAY_RUN_CHATGPT_TEMP_QUALITY=1 AGENTS_RELAY_CHATGPT_BROWSER_WORKER_AGENT=/Users/chengli/Workspace/neo/skills/chatgpt-browser-worker/agents/browser-worker.agent.md sh -c \"npm run build && node test/quality/run-temporary-chat.mjs\"",
|
|
14
14
|
"dev": "tsx src/cli.ts"
|
|
15
15
|
},
|