agents-relay 1.0.127 → 1.0.129
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 +1 -1
- package/dist/reconciler-review.js +43 -43
- package/dist/worker-routing.js +7 -9
- package/package.json +1 -1
- package/dist/reviewer-routing.js +0 -50
package/dist/dashboard-html.js
CHANGED
|
@@ -368,7 +368,7 @@ function usageProvider(provider){const card=element('section','usage-provider');
|
|
|
368
368
|
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;}
|
|
369
369
|
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;}
|
|
370
370
|
function githubBucket(label,bucket){const row=element('div','usage-row');const heading=element('div','usage-label');heading.append(element('span','',text(label)),element('span','',text(bucket.remaining+' remaining')));row.append(heading);row.append(element('div','subtle',text(bucket.used+' used / '+bucket.limit+' limit')));const reset=resetText({resetsAt:bucket.reset});if(reset)row.append(element('div','subtle',text(reset)));return row;}
|
|
371
|
-
function githubUsageCard(snapshot,status){const card=element('section','usage-provider');card.append(element('h3','',text('GitHub API')));const resources=snapshot?.resources||{};const bars=element('div','usage-bars');if(resources.core)bars.append(githubBucket('REST core',resources.core));if(resources.graphql)bars.append(githubBucket('GraphQL',resources.graphql));if(resources.search)bars.append(githubBucket('Search',resources.search));if(!bars.children.length)bars.append(element('p','empty',text('GitHub quota telemetry is unavailable.')));if(status?.limited)bars.append(element('div','error-state',text('Relay cooldown active'+(status.cooldownUntil?' until '+new Date(status.cooldownUntil).toLocaleString():'')));else bars.append(element('div','subtle',text('Relay cooldown inactive')));if(status?.calls)bars.append(element('div','subtle',text('Calls since start · GraphQL '+(status.calls.graphql||0)+' · REST '+(status.calls.rest||0)+' · quota '+(status.calls.rateLimit||0))));if(status?.readPaths)bars.append(element('div','subtle',text('Read paths · workspace scans '+(status.readPaths.workspaceScans||0)+' · PR refreshes '+(status.readPaths.prRefreshes||0))));card.append(bars);return card;}
|
|
371
|
+
function githubUsageCard(snapshot,status){const card=element('section','usage-provider');card.append(element('h3','',text('GitHub API')));const resources=snapshot?.resources||{};const bars=element('div','usage-bars');if(resources.core)bars.append(githubBucket('REST core',resources.core));if(resources.graphql)bars.append(githubBucket('GraphQL',resources.graphql));if(resources.search)bars.append(githubBucket('Search',resources.search));if(!bars.children.length)bars.append(element('p','empty',text('GitHub quota telemetry is unavailable.')));if(status?.limited)bars.append(element('div','error-state',text('Relay cooldown active'+(status.cooldownUntil?' until '+new Date(status.cooldownUntil).toLocaleString():''))));else bars.append(element('div','subtle',text('Relay cooldown inactive')));if(status?.calls)bars.append(element('div','subtle',text('Calls since start · GraphQL '+(status.calls.graphql||0)+' · REST '+(status.calls.rest||0)+' · quota '+(status.calls.rateLimit||0))));if(status?.readPaths)bars.append(element('div','subtle',text('Read paths · workspace scans '+(status.readPaths.workspaceScans||0)+' · PR refreshes '+(status.readPaths.prRefreshes||0))));card.append(bars);return card;}
|
|
372
372
|
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.github)grid.append(githubUsageCard(overview.github,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)));}}
|
|
373
373
|
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;}}
|
|
374
374
|
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;}}
|
|
@@ -2,7 +2,6 @@ import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
|
2
2
|
import { schedule, transitionTaskEvent, transitionJobEvent } from './scheduler.js';
|
|
3
3
|
import { implementationComplete, implementationTasks } from './runtime-status.js';
|
|
4
4
|
import { eventFor } from './events.js';
|
|
5
|
-
import { selectReviewer } from './reviewer-routing.js';
|
|
6
5
|
import { routeWorker, workerCandidates } from './worker-routing.js';
|
|
7
6
|
import { discoverModelCandidates, routeModel } from './model-routing.js';
|
|
8
7
|
import { ReconcilerRuntimeBase } from './reconciler-runtime.js';
|
|
@@ -51,56 +50,57 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
51
50
|
const configured = this.options.reviewer;
|
|
52
51
|
const registered = await this.store.listAgents();
|
|
53
52
|
const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
|
|
54
|
-
|
|
53
|
+
let workers = workerCandidates(this.options.adapters, registered, usage).filter(candidate => {
|
|
55
54
|
if (configured?.workerId && configured.workerId !== candidate.workerId)
|
|
56
55
|
return false;
|
|
57
56
|
if (configured?.adapter && configured.adapter !== candidate.adapter)
|
|
58
57
|
return false;
|
|
59
58
|
return true;
|
|
60
59
|
});
|
|
61
|
-
// Explicit reviewer routing is a caller-owned hard constraint and remains
|
|
62
|
-
//
|
|
60
|
+
// Explicit reviewer runtime/routing is a caller-owned hard constraint and remains
|
|
61
|
+
// usable in isolated/test runtimes that do not instantiate that adapter. It still
|
|
62
|
+
// flows through the same Worker Router path as every other executable task.
|
|
63
63
|
if (workers.length === 0 && configured?.adapter && configured.routing) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
return {
|
|
72
|
-
candidate,
|
|
73
|
-
selection: { workerId: candidate.workerId, adapter: candidate.adapter, capabilities: [...candidate.capabilities], selectedBy: 'worker-router:reviewer-diversity:eligible-fallback', selectedAt: new Date().toISOString() },
|
|
74
|
-
reason: 'eligible-fallback',
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
const previews = new Map();
|
|
78
|
-
for (const worker of workers) {
|
|
79
|
-
try {
|
|
80
|
-
const models = await discoverModelCandidates(worker, registered, usage);
|
|
81
|
-
const preview = await routeModel(worker, models, { decisionMode: 'off', constraints: configured?.routing });
|
|
82
|
-
previews.set(worker.workerId, { ...worker, routing: preview.routing });
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const result = selectReviewer([...previews.values()], implementationTasks, configured?.requirements ?? { capabilities: ['model'] });
|
|
89
|
-
const selectedWorker = workers.find(worker => worker.workerId === result.selection.workerId);
|
|
90
|
-
if (selectedWorker) {
|
|
91
|
-
result.selection.resourceHealthy = selectedWorker.resourceHealthy;
|
|
92
|
-
result.selection.resourceProvider = selectedWorker.resourceProvider;
|
|
93
|
-
result.selection.resourceUsedPercent = selectedWorker.resourceUsedPercent;
|
|
94
|
-
const models = await discoverModelCandidates(selectedWorker, registered, usage);
|
|
95
|
-
const routedModel = await routeModel(selectedWorker, models, {
|
|
96
|
-
decisionEngine: this.options.decisionEngine,
|
|
97
|
-
decisionMode: this.options.decisionMode,
|
|
98
|
-
constraints: configured?.routing,
|
|
99
|
-
taskFacts: { role: 'reviewer', implementationWorkers: implementationTasks.map(task => task.workerSelection?.workerId ?? task.adapter) },
|
|
100
|
-
});
|
|
101
|
-
result.candidate.routing = routedModel.routing;
|
|
64
|
+
workers = [{
|
|
65
|
+
workerId: configured.workerId ?? configured.adapter,
|
|
66
|
+
adapter: configured.adapter,
|
|
67
|
+
capabilities: ['model'],
|
|
68
|
+
runtime: configured.adapter,
|
|
69
|
+
}];
|
|
102
70
|
}
|
|
103
|
-
|
|
71
|
+
const implementationWorkers = implementationTasks.map(task => task.workerSelection?.workerId ?? task.adapter);
|
|
72
|
+
const implementationRoutes = implementationTasks.filter(task => task.routing).map(task => ({
|
|
73
|
+
provider: task.routing.provider,
|
|
74
|
+
model: task.routing.model,
|
|
75
|
+
}));
|
|
76
|
+
const taskFacts = {
|
|
77
|
+
role: 'reviewer',
|
|
78
|
+
reviewer_diversity: {
|
|
79
|
+
preference: 'Prefer a worker/provider-model identity different from implementation when otherwise suitable; this is a soft preference below user environment rules.',
|
|
80
|
+
implementation_workers: implementationWorkers,
|
|
81
|
+
implementation_routes: implementationRoutes,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const runtimeSummary = this.options.runtimeSummaryProvider?.();
|
|
85
|
+
const routedWorker = await routeWorker(workers, configured?.requirements ?? { capabilities: ['model'] }, {
|
|
86
|
+
decisionEngine: this.options.decisionEngine,
|
|
87
|
+
decisionMode: this.options.decisionMode,
|
|
88
|
+
taskFacts,
|
|
89
|
+
usage,
|
|
90
|
+
chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
|
|
91
|
+
});
|
|
92
|
+
const models = await discoverModelCandidates(routedWorker.candidate, registered, usage);
|
|
93
|
+
const routedModel = await routeModel(routedWorker.candidate, models, {
|
|
94
|
+
decisionEngine: this.options.decisionEngine,
|
|
95
|
+
decisionMode: this.options.decisionMode,
|
|
96
|
+
constraints: configured?.routing,
|
|
97
|
+
taskFacts,
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
candidate: { ...routedWorker.candidate, routing: routedModel.routing },
|
|
101
|
+
selection: routedWorker.selection,
|
|
102
|
+
reason: 'worker-router',
|
|
103
|
+
};
|
|
104
104
|
}
|
|
105
105
|
async routeExecutableTasks(job) {
|
|
106
106
|
const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
|
package/dist/worker-routing.js
CHANGED
|
@@ -2,6 +2,7 @@ import { chooseWithDecision } from './decision.js';
|
|
|
2
2
|
import { loadWorkerRouterRules } from './routing-policy.js';
|
|
3
3
|
export const DEFAULT_CHATGPT_PREFERRED_CONCURRENCY = 10;
|
|
4
4
|
export const DEFAULT_WORKER_ROUTER_PRINCIPLES = [
|
|
5
|
+
'User-provided environment_rules are higher-priority routing guidance than these built-in defaults. Follow user guidance when it conflicts with a default unless doing so would violate an authoritative hard constraint.',
|
|
5
6
|
'Worker Router chooses only the execution worker. Model Router chooses provider/model/profile after worker selection.',
|
|
6
7
|
'Prefer ChatGPT when both workers can execute the task because it does not consume Codex coding-plan tokens and normally has access to a stronger ChatGPT Sol model.',
|
|
7
8
|
'Prefer Codex when the task requires native/local execution or computer-use abilities that are unavailable through ChatGPT plus MacBridge.',
|
|
@@ -86,12 +87,8 @@ export function satisfiesWorker(candidate, requirements) {
|
|
|
86
87
|
export function eligibleWorkerCandidates(candidates, requirements) {
|
|
87
88
|
return candidates.filter(candidate => satisfiesWorker(candidate, requirements));
|
|
88
89
|
}
|
|
89
|
-
function deterministicWorker(eligible
|
|
90
|
-
|
|
91
|
-
return [...eligible].map((candidate, index) => ({ candidate, index })).sort((left, right) => {
|
|
92
|
-
const preference = (item) => chatgptAtPreferenceLimit ? (item.adapter === 'codex' ? 0 : 1) : (item.adapter === 'chatgpt' ? 0 : 1);
|
|
93
|
-
return preference(left.candidate) - preference(right.candidate) || left.index - right.index;
|
|
94
|
-
})[0].candidate;
|
|
90
|
+
function deterministicWorker(eligible) {
|
|
91
|
+
return eligible.find(candidate => candidate.adapter === 'codex') ?? eligible[0];
|
|
95
92
|
}
|
|
96
93
|
function selection(selected, selectedBy, decisionEvidence) {
|
|
97
94
|
return {
|
|
@@ -125,7 +122,7 @@ export async function routeWorker(candidates, requirements, options = {}) {
|
|
|
125
122
|
throw new Error('No eligible worker satisfies the authoritative worker constraints');
|
|
126
123
|
const preferredChatgptConcurrency = options.preferredChatgptConcurrency ?? DEFAULT_CHATGPT_PREFERRED_CONCURRENCY;
|
|
127
124
|
const chatgptRunning = options.chatgptRunning ?? 0;
|
|
128
|
-
const deterministic = deterministicWorker(eligible
|
|
125
|
+
const deterministic = deterministicWorker(eligible);
|
|
129
126
|
if (eligible.length === 1) {
|
|
130
127
|
const evidence = { mode: options.decisionMode ?? 'off', fallback: true };
|
|
131
128
|
return { candidate: deterministic, selection: selection(deterministic, 'worker-router:constraint', evidence) };
|
|
@@ -134,8 +131,9 @@ export async function routeWorker(candidates, requirements, options = {}) {
|
|
|
134
131
|
const facts = JSON.parse(JSON.stringify({
|
|
135
132
|
task: options.taskFacts ?? {},
|
|
136
133
|
explicit_constraints: { runtime: requirements.runtime ?? null },
|
|
137
|
-
|
|
134
|
+
decision_precedence: ['authoritative_hard_constraints', 'environment_rules', 'routing_principles', 'resource_and_runtime_preferences'],
|
|
138
135
|
environment_rules: rules.text,
|
|
136
|
+
routing_principles: DEFAULT_WORKER_ROUTER_PRINCIPLES,
|
|
139
137
|
usage_snapshot: options.usage ?? null,
|
|
140
138
|
runtime_load: {
|
|
141
139
|
chatgpt_running: chatgptRunning,
|
|
@@ -158,7 +156,7 @@ export async function routeWorker(candidates, requirements, options = {}) {
|
|
|
158
156
|
}));
|
|
159
157
|
const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', { facts }, {
|
|
160
158
|
type: 'choice',
|
|
161
|
-
question: 'Choose the best execution worker for this task.
|
|
159
|
+
question: 'Choose the best execution worker for this task. Authoritative hard constraints come first. Then follow user-provided environment rules ahead of built-in routing principles when they conflict. Use task facts, reviewer-diversity context when present, current usage, and runtime load as supporting evidence. Do not choose a provider or model here.',
|
|
162
160
|
choices: eligible.map(item => item.workerId),
|
|
163
161
|
}, () => deterministic.workerId);
|
|
164
162
|
const selected = eligible.find(item => item.workerId === choice.choice) ?? deterministic;
|
package/package.json
CHANGED
package/dist/reviewer-routing.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
function satisfies(candidate, requirements) {
|
|
2
|
-
if (candidate.availability === 'offline')
|
|
3
|
-
return false;
|
|
4
|
-
if (requirements?.runtime && candidate.runtime !== requirements.runtime)
|
|
5
|
-
return false;
|
|
6
|
-
return true;
|
|
7
|
-
}
|
|
8
|
-
function producedWorkerIds(tasks) {
|
|
9
|
-
return new Set(tasks
|
|
10
|
-
.filter(task => task.agentName !== 'reviewer')
|
|
11
|
-
.map(task => task.workerSelection?.workerId ?? task.adapter));
|
|
12
|
-
}
|
|
13
|
-
function producedRoutes(tasks) {
|
|
14
|
-
return new Set(tasks
|
|
15
|
-
.filter(task => task.agentName !== 'reviewer' && task.routing)
|
|
16
|
-
.map(task => `${task.routing?.provider}\0${task.routing?.model}`));
|
|
17
|
-
}
|
|
18
|
-
/** Select a reviewer after authoritative worker constraints, then prefer execution diversity. */
|
|
19
|
-
export function selectReviewer(candidates, implementationTasks, requirements) {
|
|
20
|
-
const eligible = candidates.filter(candidate => satisfies(candidate, requirements));
|
|
21
|
-
if (eligible.length === 0)
|
|
22
|
-
throw new Error('No eligible reviewer worker satisfies the declared requirements');
|
|
23
|
-
const usedWorkers = producedWorkerIds(implementationTasks);
|
|
24
|
-
const usedRoutes = producedRoutes(implementationTasks);
|
|
25
|
-
const ranked = eligible.map((candidate, index) => ({
|
|
26
|
-
candidate,
|
|
27
|
-
index,
|
|
28
|
-
differentWorker: !usedWorkers.has(candidate.workerId),
|
|
29
|
-
differentRoute: !usedRoutes.has(`${candidate.routing.provider}\0${candidate.routing.model}`),
|
|
30
|
-
})).sort((left, right) => {
|
|
31
|
-
if (left.differentWorker !== right.differentWorker)
|
|
32
|
-
return left.differentWorker ? -1 : 1;
|
|
33
|
-
if (left.differentRoute !== right.differentRoute)
|
|
34
|
-
return left.differentRoute ? -1 : 1;
|
|
35
|
-
return left.index - right.index;
|
|
36
|
-
});
|
|
37
|
-
const selected = ranked[0];
|
|
38
|
-
const reason = selected.differentWorker ? 'worker-diversity' : selected.differentRoute ? 'provider-model-diversity' : 'eligible-fallback';
|
|
39
|
-
return {
|
|
40
|
-
candidate: selected.candidate,
|
|
41
|
-
selection: {
|
|
42
|
-
workerId: selected.candidate.workerId,
|
|
43
|
-
adapter: selected.candidate.adapter,
|
|
44
|
-
capabilities: [...selected.candidate.capabilities],
|
|
45
|
-
selectedBy: `worker-router:reviewer-diversity:${reason}`,
|
|
46
|
-
selectedAt: new Date().toISOString(),
|
|
47
|
-
},
|
|
48
|
-
reason,
|
|
49
|
-
};
|
|
50
|
-
}
|