agents-relay 2.0.27 → 2.0.28
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 +3 -1
- package/dist/execution-routing.js +156 -29
- package/dist/relayd.js +1 -0
- package/dist/router-evals.js +28 -23
- package/package.json +1 -1
package/dist/dashboard-html.js
CHANGED
|
@@ -329,6 +329,8 @@ const displayedEventIds=new Set();
|
|
|
329
329
|
function value(input){return input===null||input===undefined||input===''?'—':String(input);}
|
|
330
330
|
function text(input){return document.createTextNode(value(input));}
|
|
331
331
|
function element(name,className,child){const node=document.createElement(name);if(className)node.className=className;if(child)node.append(child);return node;}
|
|
332
|
+
function yamlScalar(input){if(input===null)return 'null';if(typeof input==='boolean'||typeof input==='number')return String(input);return JSON.stringify(String(input));}
|
|
333
|
+
function yamlDump(input,indent=0){const pad=' '.repeat(indent);if(Array.isArray(input)){if(!input.length)return pad+'[]';return input.map(item=>{if(item&&typeof item==='object'){return pad+'-\\n'+yamlDump(item,indent+2);}return pad+'- '+yamlScalar(item);}).join('\\n');}if(input&&typeof input==='object'){const entries=Object.entries(input);if(!entries.length)return pad+'{}';const lines=[];for(const [key,item] of entries){if(typeof item==='string'&&item.includes('\\n')){lines.push(pad+key+': |');for(const line of item.replace(/\\n$/,'').split('\\n'))lines.push(' '.repeat(indent+2)+line);continue;}if(item&&typeof item==='object'){lines.push(pad+key+':');lines.push(yamlDump(item,indent+2));}else{lines.push(pad+key+': '+yamlScalar(item));}}return lines.join('\\n');}return pad+yamlScalar(input);}
|
|
332
334
|
function field(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),element('p','detail-value',text(input)));return box;}
|
|
333
335
|
function markdownField(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),markdownValue(input));return box;}
|
|
334
336
|
function rememberSelection(){if(selectedJob)sessionStorage.setItem('agents-relay:selected-job',selectedJob);else sessionStorage.removeItem('agents-relay:selected-job');if(selectedTaskId)sessionStorage.setItem('agents-relay:selected-task',selectedTaskId);else sessionStorage.removeItem('agents-relay:selected-task');}
|
|
@@ -451,7 +453,7 @@ function routeWorker(record){const id=record?.selected?.worker?.id||record?.work
|
|
|
451
453
|
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
454
|
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
455
|
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('Calling router with current status…')));try{if(!selectedJob||!selectedTaskId)throw new Error('Select a Job and Task first');const response=await fetch('/api/router-test',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jobId:selectedJob,taskId:selectedTaskId})});if(!response.ok)throw new Error(response.status+' '+await response.text());const result=await response.json();const selected=result.selected||{};const payload={selected,probabilities:result.probabilities||{},fallback:Boolean(result.fallback),hard_gates:result.gates||[],laya_input:result.decisionInput||null};routerTestResults.replaceChildren(element('strong','',text((selected.worker||'—')+' · '+(selected.model||'—'))));routerTestResults.append(element('div','subtle',text('route: '+(selected.route||'—'))),element('pre','',text(
|
|
456
|
+
async function testRouter(){rulesTest.disabled=true;routerTestResults.replaceChildren(element('p','subtle',text('Calling router with current status…')));try{if(!selectedJob||!selectedTaskId)throw new Error('Select a Job and Task first');const response=await fetch('/api/router-test',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({jobId:selectedJob,taskId:selectedTaskId})});if(!response.ok)throw new Error(response.status+' '+await response.text());const result=await response.json();const selected=result.selected||{};const payload={selected,proposed_fields:result.proposedFields||{},probabilities:result.probabilities||{},fallback:Boolean(result.fallback),hard_gates:result.gates||[],laya_input:result.decisionInput||null};routerTestResults.replaceChildren(element('strong','',text((selected.worker||'—')+' · '+(selected.model||'—'))));routerTestResults.append(element('div','subtle',text('route: '+(selected.route||'—'))),element('pre','',text(yamlDump(payload))));}catch(error){routerTestResults.replaceChildren(element('p','error-state',text('Router test failed: '+error.message)));}finally{rulesTest.disabled=false;}}
|
|
455
457
|
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
458
|
async function drawShortcuts(){shortcutsStatus.textContent='';shortcutsText.value=chatShortcutText();}
|
|
457
459
|
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.';}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { chooseWithDecision } from './decision.js';
|
|
2
1
|
import { makeExecutionDecisionAudit } from './decision-audit.js';
|
|
3
2
|
import { discoverModelCandidates } from './model-routing.js';
|
|
4
3
|
import { loadExecutionRouterRules, loadRoutingPolicy, policyAllows } from './routing-policy.js';
|
|
@@ -60,6 +59,115 @@ function modelGateReasons(candidate, constraints, excludedRoutes, policy) {
|
|
|
60
59
|
reasons.push('reasoning_constraint');
|
|
61
60
|
return reasons;
|
|
62
61
|
}
|
|
62
|
+
const REASONING_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
63
|
+
function setupName(worker, model) {
|
|
64
|
+
if (worker.adapter === 'chatgpt')
|
|
65
|
+
return 'account-defaults';
|
|
66
|
+
if (model.id.startsWith('default:') || model.id.startsWith('catalog:'))
|
|
67
|
+
return 'default';
|
|
68
|
+
return model.profile ?? model.provider;
|
|
69
|
+
}
|
|
70
|
+
function setupKey(worker, model) {
|
|
71
|
+
return worker.workerId + '/' + setupName(worker, model);
|
|
72
|
+
}
|
|
73
|
+
function configuredDefault(models, setup) {
|
|
74
|
+
if (setup === 'account-defaults')
|
|
75
|
+
return models[0];
|
|
76
|
+
if (setup === 'default')
|
|
77
|
+
return models.find(model => model.id.startsWith('default:'));
|
|
78
|
+
return models.find(model => model.profile === setup && model.id.startsWith(setup + ':'));
|
|
79
|
+
}
|
|
80
|
+
function resolveModelAlias(raw, models, familyHint) {
|
|
81
|
+
const normalized = raw.trim().toLowerCase();
|
|
82
|
+
const exact = models.find(model => model.model.toLowerCase() === normalized);
|
|
83
|
+
if (exact)
|
|
84
|
+
return exact.model;
|
|
85
|
+
if (familyHint) {
|
|
86
|
+
const sameFamily = models.find(model => model.model.toLowerCase() === familyHint.toLowerCase() + '-' + normalized);
|
|
87
|
+
if (sameFamily)
|
|
88
|
+
return sameFamily.model;
|
|
89
|
+
}
|
|
90
|
+
const suffix = [...new Set(models.filter(model => model.model.toLowerCase().endsWith('-' + normalized)).map(model => model.model))];
|
|
91
|
+
return suffix.length === 1 ? suffix[0] : raw.trim();
|
|
92
|
+
}
|
|
93
|
+
function userModelOverrides(rulesText, defaults, models) {
|
|
94
|
+
const result = [];
|
|
95
|
+
for (const line of rulesText.split(/\r?\n/)) {
|
|
96
|
+
const match = line.match(/^\s*([a-z0-9._-]+)\s*:\s*(.+)$/i);
|
|
97
|
+
if (!match)
|
|
98
|
+
continue;
|
|
99
|
+
const label = match[1].toLowerCase();
|
|
100
|
+
const base = defaults.find(model => (model.profile ?? (model.provider === 'openai' ? 'default' : model.provider)).toLowerCase() === label || model.provider.toLowerCase() === label);
|
|
101
|
+
if (!base)
|
|
102
|
+
continue;
|
|
103
|
+
const sameSetupModels = models.filter(model => model.provider === base.provider && (model.profile ?? null) === (base.profile ?? null));
|
|
104
|
+
const familyHint = match[2].match(/\b(gpt-\d+(?:\.\d+)?)-[a-z0-9.-]+/i)?.[1];
|
|
105
|
+
for (const segment of match[2].split(',')) {
|
|
106
|
+
const item = segment.trim().match(/^(.+?)\s+(low|medium|high|xhigh|max|ultra)(?:\s|$)/i);
|
|
107
|
+
if (!item)
|
|
108
|
+
continue;
|
|
109
|
+
const model = resolveModelAlias(item[1].trim(), sameSetupModels, familyHint);
|
|
110
|
+
const reasoning = item[2].toLowerCase();
|
|
111
|
+
if (result.some(candidate => candidate.provider === base.provider && candidate.model === model && (candidate.profile ?? null) === (base.profile ?? null) && candidate.reasoning === reasoning))
|
|
112
|
+
continue;
|
|
113
|
+
result.push({
|
|
114
|
+
...base,
|
|
115
|
+
id: (base.profile ?? 'default') + ':user/' + model,
|
|
116
|
+
model,
|
|
117
|
+
reasoning,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function codePlan(model) {
|
|
124
|
+
return {
|
|
125
|
+
five_hour: model.resourceWindows?.fiveHour ? {
|
|
126
|
+
remaining_percent: model.resourceWindows.fiveHour.remainingPercent ?? null,
|
|
127
|
+
reset_at: model.resourceWindows.fiveHour.resetAt ?? null,
|
|
128
|
+
reset_in_minutes: model.resourceWindows.fiveHour.resetInMinutes ?? null,
|
|
129
|
+
} : null,
|
|
130
|
+
weekly: model.resourceWindows?.weekly ? {
|
|
131
|
+
remaining_percent: model.resourceWindows.weekly.remainingPercent ?? null,
|
|
132
|
+
reset_at: model.resourceWindows.weekly.resetAt ?? null,
|
|
133
|
+
reset_in_minutes: model.resourceWindows.weekly.resetInMinutes ?? null,
|
|
134
|
+
} : null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function workerProviderFacts(routes) {
|
|
138
|
+
const workers = new Map();
|
|
139
|
+
for (const route of routes) {
|
|
140
|
+
const list = workers.get(route.worker.workerId) ?? [];
|
|
141
|
+
list.push(route);
|
|
142
|
+
workers.set(route.worker.workerId, list);
|
|
143
|
+
}
|
|
144
|
+
return [...workers.entries()].map(([workerId, workerRoutes]) => {
|
|
145
|
+
const providers = new Map();
|
|
146
|
+
for (const route of workerRoutes) {
|
|
147
|
+
const list = providers.get(route.model.provider) ?? [];
|
|
148
|
+
list.push(route);
|
|
149
|
+
providers.set(route.model.provider, list);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
id: workerId,
|
|
153
|
+
adapter: workerRoutes[0].worker.adapter,
|
|
154
|
+
providers: Object.fromEntries([...providers.entries()].map(([provider, providerRoutes]) => {
|
|
155
|
+
const models = providerRoutes.map(route => route.model);
|
|
156
|
+
const configured = models.find(model => !model.id.includes(':user/')) ?? models[0];
|
|
157
|
+
return [provider, {
|
|
158
|
+
default: { model: configured.model, reasoning: configured.reasoning ?? null },
|
|
159
|
+
code_plan: codePlan(configured),
|
|
160
|
+
}];
|
|
161
|
+
})),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function decisionRouteLabel(route) {
|
|
166
|
+
const prefix = route.worker.workerId + '/' + route.model.provider + '/';
|
|
167
|
+
if (!route.model.id.includes(':user/'))
|
|
168
|
+
return prefix + 'default';
|
|
169
|
+
return prefix + route.model.model + (route.model.reasoning ? '@' + route.model.reasoning : '');
|
|
170
|
+
}
|
|
63
171
|
function routeId(worker, model) {
|
|
64
172
|
return worker.workerId + '::' + model.id + (model.reasoning ? '@' + model.reasoning : '');
|
|
65
173
|
}
|
|
@@ -95,7 +203,11 @@ export async function discoverExecutionRoutes(workers, requirements, options = {
|
|
|
95
203
|
const evaluations = [];
|
|
96
204
|
for (const worker of workers) {
|
|
97
205
|
const workerReasons = workerGateEvaluations.find(item => item.workerId === worker.workerId)?.rejectionReasons ?? [];
|
|
98
|
-
const
|
|
206
|
+
const discoveredModels = await discoverModelCandidates(worker, registered, options.usage, undefined, options.rulesPath);
|
|
207
|
+
const configuredModels = discoveredModels.filter(model => !model.id.startsWith('catalog:'));
|
|
208
|
+
const defaults = configuredModels.filter(model => model.id.startsWith('default:') || (model.profile && model.id.startsWith(model.profile + ':')) || worker.adapter === 'chatgpt');
|
|
209
|
+
const advertised = userModelOverrides(policy.rules.text, defaults, discoveredModels);
|
|
210
|
+
const models = [...configuredModels, ...advertised];
|
|
99
211
|
for (const model of models) {
|
|
100
212
|
const rejectionReasons = [...workerReasons, ...modelGateReasons(model, options.routingConstraints, options.excludedRoutes, policy)];
|
|
101
213
|
const route = { id: routeId(worker, model), worker, model };
|
|
@@ -136,36 +248,51 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
136
248
|
chatgpt_running: chatgptRunning,
|
|
137
249
|
preferred_chatgpt_concurrency: preferredChatgptConcurrency,
|
|
138
250
|
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
worker: route.worker.adapter,
|
|
142
|
-
provider: route.model.provider,
|
|
143
|
-
model: route.model.model,
|
|
144
|
-
profile: route.model.profile ?? null,
|
|
145
|
-
reasoning: route.model.reasoning ?? null,
|
|
146
|
-
code_plan: {
|
|
147
|
-
five_hour: route.model.resourceWindows?.fiveHour ? {
|
|
148
|
-
remaining_percent: route.model.resourceWindows.fiveHour.remainingPercent ?? null,
|
|
149
|
-
reset_at: route.model.resourceWindows.fiveHour.resetAt ?? null,
|
|
150
|
-
reset_in_minutes: route.model.resourceWindows.fiveHour.resetInMinutes ?? null,
|
|
151
|
-
} : null,
|
|
152
|
-
weekly: route.model.resourceWindows?.weekly ? {
|
|
153
|
-
remaining_percent: route.model.resourceWindows.weekly.remainingPercent ?? null,
|
|
154
|
-
reset_at: route.model.resourceWindows.weekly.resetAt ?? null,
|
|
155
|
-
reset_in_minutes: route.model.resourceWindows.weekly.resetInMinutes ?? null,
|
|
156
|
-
} : null,
|
|
157
|
-
},
|
|
158
|
-
})),
|
|
251
|
+
worker_candidates: workerProviderFacts(routes),
|
|
252
|
+
routing_instruction: 'For each selected provider, use its default model and reasoning unless user_input_rules explicitly provides another model/reasoning option. Any override must be compatible with the selected worker/provider and pass runtime validation.',
|
|
159
253
|
}));
|
|
254
|
+
const decisionChoices = routes.map(decisionRouteLabel);
|
|
160
255
|
const decisionRequest = { facts };
|
|
161
256
|
const decisionContract = {
|
|
162
257
|
type: 'choice',
|
|
163
|
-
question: 'Choose one
|
|
164
|
-
choices:
|
|
258
|
+
question: 'Choose one legal route. Each provider default is available. Additional model/reasoning routes exist only when explicitly provided by user_input_rules. Use context, code-plan quota, runtime load, and user_input_rules.',
|
|
259
|
+
choices: decisionChoices,
|
|
165
260
|
};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
261
|
+
let selected = fallback;
|
|
262
|
+
let evidence = { mode: options.decisionMode ?? 'off', fallback: true };
|
|
263
|
+
if ((options.decisionMode ?? 'off') !== 'off' && options.decisionEngine) {
|
|
264
|
+
try {
|
|
265
|
+
const decision = await options.decisionEngine.decide(decisionRequest, decisionContract);
|
|
266
|
+
if (decision.type !== 'choice')
|
|
267
|
+
throw new Error('Execution router expected a route choice result');
|
|
268
|
+
const index = decisionChoices.indexOf(decision.choice);
|
|
269
|
+
if (index < 0)
|
|
270
|
+
throw new Error('Execution router returned an unknown route');
|
|
271
|
+
selected = routes[index];
|
|
272
|
+
evidence = {
|
|
273
|
+
mode: options.decisionMode ?? 'active',
|
|
274
|
+
engine: options.decisionEngine.name,
|
|
275
|
+
proposed: decision.choice,
|
|
276
|
+
probabilities: decision.probabilities,
|
|
277
|
+
proposedFields: {
|
|
278
|
+
worker: selected.worker.workerId,
|
|
279
|
+
provider: selected.model.provider,
|
|
280
|
+
model: selected.model.model,
|
|
281
|
+
reasoning: selected.model.reasoning ?? '',
|
|
282
|
+
},
|
|
283
|
+
fallback: false,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
evidence = {
|
|
288
|
+
mode: options.decisionMode ?? 'active',
|
|
289
|
+
engine: options.decisionEngine.name,
|
|
290
|
+
fallback: true,
|
|
291
|
+
error: error instanceof Error ? error.message : String(error),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const selectedBy = evidence.engine && !evidence.fallback ? 'execution-router:laya' : 'execution-router:fallback';
|
|
169
296
|
const routing = {
|
|
170
297
|
provider: selected.model.provider,
|
|
171
298
|
model: selected.model.model,
|
|
@@ -173,7 +300,7 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
173
300
|
reasoning: options.routingConstraints?.reasoning ?? selected.model.reasoning,
|
|
174
301
|
decidedBy: selectedBy,
|
|
175
302
|
decidedAt: new Date().toISOString(),
|
|
176
|
-
decisionEvidence:
|
|
303
|
+
decisionEvidence: evidence,
|
|
177
304
|
};
|
|
178
305
|
const audit = makeExecutionDecisionAudit({
|
|
179
306
|
task: options.auditTask,
|
|
@@ -200,7 +327,7 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
200
327
|
await options.auditStore.append(audit);
|
|
201
328
|
return {
|
|
202
329
|
candidate: { ...selected.worker, routing },
|
|
203
|
-
selection: workerSelection(selected.worker, selectedBy,
|
|
330
|
+
selection: workerSelection(selected.worker, selectedBy, evidence),
|
|
204
331
|
model: selected.model,
|
|
205
332
|
audit,
|
|
206
333
|
};
|
package/dist/relayd.js
CHANGED
|
@@ -397,6 +397,7 @@ export async function runDaemon(argv) {
|
|
|
397
397
|
reasoning: result.candidate.routing.reasoning,
|
|
398
398
|
},
|
|
399
399
|
probabilities: result.candidate.routing.decisionEvidence?.probabilities ?? {},
|
|
400
|
+
proposedFields: result.candidate.routing.decisionEvidence?.proposedFields ?? {},
|
|
400
401
|
fallback: result.candidate.routing.decisionEvidence?.fallback ?? false,
|
|
401
402
|
current: {
|
|
402
403
|
chatgptRunning: runtime.workers.chatgpt,
|
package/dist/router-evals.js
CHANGED
|
@@ -12,12 +12,12 @@ export const ROUTER_EVAL_CASES = [
|
|
|
12
12
|
{ id: 'workspace-exhausted', storySize: 'S', task: 'Review a focused issue while Browser Workspace has no free slot.', chatgptAvailable: false, chatgptSlots: 0, expectedWorker: 'codex' },
|
|
13
13
|
{ id: 'large-workspace-available', storySize: 'L', task: 'Implement a large feature even though ChatGPT workspace capacity is healthy.', chatgptAvailable: true, expectedWorker: 'codex' },
|
|
14
14
|
];
|
|
15
|
-
const CODEX_ROUTE = 'codex
|
|
16
|
-
const CHATGPT_ROUTE = 'chatgpt/browser-worker
|
|
15
|
+
const CODEX_ROUTE = 'codex/openai/default';
|
|
16
|
+
const CHATGPT_ROUTE = 'chatgpt/browser-worker/chatgpt/default';
|
|
17
17
|
function workerForRoute(route) {
|
|
18
18
|
if (route.startsWith('chatgpt/'))
|
|
19
19
|
return 'chatgpt';
|
|
20
|
-
if (route.startsWith('codex
|
|
20
|
+
if (route.startsWith('codex/'))
|
|
21
21
|
return 'codex';
|
|
22
22
|
return 'unknown';
|
|
23
23
|
}
|
|
@@ -28,29 +28,31 @@ export async function runRouterEvals(engine, rulesPath) {
|
|
|
28
28
|
const taskShape = executionTaskShape({ storySize: testCase.storySize });
|
|
29
29
|
const available = testCase.chatgptAvailable;
|
|
30
30
|
const choices = available ? [CODEX_ROUTE, CHATGPT_ROUTE] : [CODEX_ROUTE];
|
|
31
|
-
const
|
|
31
|
+
const workerCandidates = [
|
|
32
32
|
{
|
|
33
|
-
id:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
id: 'codex',
|
|
34
|
+
adapter: 'codex',
|
|
35
|
+
providers: {
|
|
36
|
+
openai: {
|
|
37
|
+
default: { model: 'gpt-5.6-sol', reasoning: 'medium' },
|
|
38
|
+
code_plan: { five_hour: null, weekly: null },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
40
41
|
},
|
|
41
42
|
...(available ? [{
|
|
42
|
-
id:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
id: 'chatgpt/browser-worker',
|
|
44
|
+
adapter: 'chatgpt',
|
|
45
|
+
providers: {
|
|
46
|
+
chatgpt: {
|
|
47
|
+
default: { model: 'account-defaults', reasoning: null },
|
|
48
|
+
code_plan: { five_hour: null, weekly: null },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
49
51
|
}] : []),
|
|
50
52
|
];
|
|
51
|
-
const facts = {
|
|
53
|
+
const facts = JSON.parse(JSON.stringify({
|
|
52
54
|
context: {
|
|
53
|
-
job: { id: 'router-eval', title: 'Router eval', objective: 'Evaluate worker and
|
|
55
|
+
job: { id: 'router-eval', title: 'Router eval', objective: 'Evaluate worker and provider route selection.' },
|
|
54
56
|
task: { id: testCase.id, kind: 'work', storySize: testCase.storySize, input: testCase.task },
|
|
55
57
|
task_shape: {
|
|
56
58
|
story_size: taskShape.storySize,
|
|
@@ -62,13 +64,16 @@ export async function runRouterEvals(engine, rulesPath) {
|
|
|
62
64
|
},
|
|
63
65
|
user_input_rules: rules.text,
|
|
64
66
|
runtime: { chatgpt_running: testCase.chatgptRunning ?? 0, preferred_chatgpt_concurrency: 10 },
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
worker_candidates: workerCandidates,
|
|
68
|
+
routing_instruction: 'For each selected provider, use its default model and reasoning unless user_input_rules explicitly provides another model/reasoning option.',
|
|
69
|
+
}));
|
|
67
70
|
const decision = await engine.decide({ facts }, {
|
|
68
71
|
type: 'choice',
|
|
69
|
-
question: 'Choose one
|
|
72
|
+
question: 'Choose one legal route. Each provider default is available. Additional model/reasoning routes exist only when explicitly provided by user_input_rules. Use context, code-plan quota, runtime load, and user_input_rules.',
|
|
70
73
|
choices,
|
|
71
74
|
});
|
|
75
|
+
if (decision.type !== 'choice')
|
|
76
|
+
throw new Error('Router eval expected a single-choice decision');
|
|
72
77
|
const selectedWorker = workerForRoute(decision.choice);
|
|
73
78
|
results.push({
|
|
74
79
|
...testCase,
|