agents-relay 2.0.23 → 2.0.25
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 -3
- package/dist/dashboard.js +10 -1
- package/dist/execution-routing.js +4 -1
- package/dist/reconciler-review.js +14 -6
- package/dist/relayd.js +26 -3
- package/package.json +1 -1
package/dist/dashboard-html.js
CHANGED
|
@@ -177,7 +177,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
177
177
|
}
|
|
178
178
|
@media (prefers-reduced-motion:reduce) { .skeleton { animation:none; } .usage-fill { transition:none; } }
|
|
179
179
|
|
|
180
|
-
.router-rules-workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start}.router-rules-pane{min-width:0}.audit-log{display:flex;flex-direction:column;gap:8px;max-height:
|
|
180
|
+
.router-rules-workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start}.router-rules-pane{min-width:0;max-height:560px;overflow:auto}.rules-editor .rules-textarea{min-height:420px}.audit-log{display:flex;flex-direction:column;gap:8px;max-height:500px;overflow:auto}.audit-entry{border:1px solid var(--border);border-radius:10px;padding:10px;background:var(--surface)}.audit-entry-head{display:flex;justify-content:space-between;gap:8px;align-items:center}.audit-entry pre{white-space:pre-wrap;word-break:break-word;margin:8px 0 0;font-size:11px}.router-test-panel{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}.router-test-results{margin-top:8px;display:flex;flex-direction:column;gap:6px;max-height:480px;overflow:auto;min-width:0}.router-test-results pre{white-space:pre;overflow:auto;max-width:100%;font-size:11px;line-height:1.45;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px}.router-test-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:7px 0;border-bottom:1px solid var(--border)}@media(max-width:900px){.router-rules-workspace{grid-template-columns:1fr}.router-rules-pane{max-height:none}.router-test-results{max-height:420px}}
|
|
181
181
|
</style>
|
|
182
182
|
</head>
|
|
183
183
|
<body>
|
|
@@ -235,7 +235,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
235
235
|
</div>
|
|
236
236
|
</div>
|
|
237
237
|
</section>
|
|
238
|
-
<section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden><div class="panel"><div class="panel-head"><h2>Router Rules</h2><span id="rules-meta" class="subtle"></span></div><div class="panel-body"><div class="router-rules-workspace"><div class="router-rules-pane rules-editor"><h3>Rules Editor</h3><p class="rules-help">Free-text soft policy loaded by the Execution Router for every worker+model routing decision. Impossible routes remain excluded by code; Codex remains the deterministic fallback if decision fails.</p><textarea id="rules-text" class="rules-textarea" spellcheck="true" aria-label="Execution router rules" placeholder="Example: Prefer ZAI glm-5.3-flash high while quota is available. Use ChatGPT for research when appropriate. Codex is fallback only."></textarea><div class="rules-footer"><span id="rules-status" class="subtle" aria-live="polite"></span><div><button id="rules-test" class="rules-save" type="button">Test Router</button> <button id="rules-save" class="rules-save" type="button">Save rules</button></div></div
|
|
238
|
+
<section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden><div class="panel"><div class="panel-head"><h2>Router Rules</h2><span id="rules-meta" class="subtle"></span></div><div class="panel-body"><div class="router-rules-workspace"><div class="router-rules-pane rules-editor"><h3>Rules Editor</h3><p class="rules-help">Free-text soft policy loaded by the Execution Router for every worker+model routing decision. Impossible routes remain excluded by code; Codex remains the deterministic fallback if decision fails.</p><textarea id="rules-text" class="rules-textarea" spellcheck="true" aria-label="Execution router rules" placeholder="Example: Prefer ZAI glm-5.3-flash high while quota is available. Use ChatGPT for research when appropriate. Codex is fallback only."></textarea><div class="rules-footer"><span id="rules-status" class="subtle" aria-live="polite"></span><div><button id="rules-test" class="rules-save" type="button">Test Router</button> <button id="rules-save" class="rules-save" type="button">Save rules</button></div></div></div><div class="router-rules-pane"><div class="panel-head"><h3>Audit Log</h3><button id="audit-refresh" type="button">Refresh</button></div><div id="audit-log" class="audit-log" aria-live="polite"><p class="empty">Loading latest routing decisions…</p></div></div></div><div class="router-test-panel"><h3>Test Router Result</h3><div id="router-test-results" class="router-test-results" aria-live="polite"><p class="empty">Select a Job and Task, then run Test Router.</p></div></div></div></div></section>
|
|
239
239
|
<section id="shortcuts-view" role="tabpanel" aria-labelledby="shortcuts-tab" hidden>
|
|
240
240
|
<div class="panel">
|
|
241
241
|
<div class="panel-head"><h2>ChatGPT Shortcuts</h2><span class="subtle">One per line · %1 = current repo/pr</span></div>
|
|
@@ -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('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 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(JSON.stringify(payload,null,2))));}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{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(JSON.stringify(payload,null,2))));}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
|
@@ -454,7 +454,16 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
454
454
|
return;
|
|
455
455
|
}
|
|
456
456
|
try {
|
|
457
|
-
|
|
457
|
+
let body = '';
|
|
458
|
+
for await (const chunk of req)
|
|
459
|
+
body += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
|
|
460
|
+
const parsed = JSON.parse(body || '{}');
|
|
461
|
+
if (!parsed.jobId || !parsed.taskId) {
|
|
462
|
+
res.writeHead(400);
|
|
463
|
+
res.end('Select a Job and Task first');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const result = await routerTestProvider({ jobId: parsed.jobId, taskId: parsed.taskId });
|
|
458
467
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
459
468
|
res.end(JSON.stringify(result));
|
|
460
469
|
}
|
|
@@ -4,7 +4,8 @@ import { discoverModelCandidates } from './model-routing.js';
|
|
|
4
4
|
import { loadExecutionRouterRules, loadRoutingPolicy, policyAllows } from './routing-policy.js';
|
|
5
5
|
import { DEFAULT_CHATGPT_PREFERRED_CONCURRENCY } from './worker-routing.js';
|
|
6
6
|
export function executionTaskShape(taskFacts = {}) {
|
|
7
|
-
const
|
|
7
|
+
const nestedTask = taskFacts.task && typeof taskFacts.task === 'object' ? taskFacts.task : undefined;
|
|
8
|
+
const raw = taskFacts.storySize ?? nestedTask?.storySize;
|
|
8
9
|
const storySize = raw === 'S' || raw === 'L' ? raw : 'M';
|
|
9
10
|
const sustainedWork = storySize === 'L';
|
|
10
11
|
return {
|
|
@@ -116,6 +117,8 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
116
117
|
const taskFacts = options.taskFacts ?? {};
|
|
117
118
|
const taskShape = executionTaskShape(taskFacts);
|
|
118
119
|
const facts = JSON.parse(JSON.stringify({
|
|
120
|
+
job: taskFacts.job ?? null,
|
|
121
|
+
task: taskFacts.task ?? taskFacts,
|
|
119
122
|
task_shape: {
|
|
120
123
|
story_size: taskShape.storySize,
|
|
121
124
|
sustained_work: taskShape.sustainedWork,
|
|
@@ -177,13 +177,21 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
177
177
|
return;
|
|
178
178
|
const requirements = task.workerRequirements ?? { capabilities: [] };
|
|
179
179
|
const taskFacts = {
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
job: {
|
|
181
|
+
id: freshJob.id, title: freshJob.title, objective: freshJob.objective ?? null,
|
|
182
|
+
description: freshJob.description ?? null, priority: freshJob.priority ?? null,
|
|
183
|
+
executionMode: freshJob.executionMode, repository: freshJob.repository,
|
|
184
|
+
prNumber: freshJob.prNumber, state: freshJob.state,
|
|
185
|
+
},
|
|
186
|
+
task: {
|
|
187
|
+
id: task.id, kind: task.kind ?? 'work', storySize: task.storySize ?? 'M',
|
|
188
|
+
priority: task.priority ?? null, agentName: task.agentName ?? null,
|
|
189
|
+
parentTaskId: task.parentTaskId, dependencies: task.dependencies,
|
|
190
|
+
capabilities: task.capabilities, input: task.input,
|
|
191
|
+
attachments: task.attachments ?? [], output: task.output ?? null,
|
|
192
|
+
state: task.state, attempt: task.attempt, maxAttempts: task.maxAttempts,
|
|
193
|
+
},
|
|
182
194
|
storySize: task.storySize ?? 'M',
|
|
183
|
-
agentName: task.agentName ?? null,
|
|
184
|
-
input: task.input,
|
|
185
|
-
attachments: task.attachments ?? [],
|
|
186
|
-
output: task.output ?? null,
|
|
187
195
|
};
|
|
188
196
|
const routingConstraints = task.routing ? { provider: task.routing.provider, model: task.routing.model, profile: task.routing.profile, reasoning: task.routing.reasoning } : undefined;
|
|
189
197
|
const parent = task.agentName === 'troubleshooter' && task.parentTaskId ? freshJob.tasks.find(item => item.id === task.parentTaskId) : undefined;
|
package/dist/relayd.js
CHANGED
|
@@ -348,17 +348,40 @@ export async function runDaemon(argv) {
|
|
|
348
348
|
taskCancel: workspaceTaskAction('cancel'),
|
|
349
349
|
taskComplete: workspaceTaskComplete,
|
|
350
350
|
taskSetState: workspaceTaskSetState,
|
|
351
|
-
}, async () => {
|
|
351
|
+
}, async ({ jobId, taskId }) => {
|
|
352
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
353
|
+
if (!overview)
|
|
354
|
+
throw new Error('Selected Job ' + jobId + ' is not available');
|
|
355
|
+
const job = overview.job;
|
|
356
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
357
|
+
if (!task)
|
|
358
|
+
throw new Error('Selected Task ' + taskId + ' is not available in Job ' + jobId);
|
|
352
359
|
const usage = await usageRegistry.snapshot();
|
|
353
360
|
const runtime = summarizeRuntime(cachedOverviews.map(item => item.job));
|
|
354
361
|
const workers = workerCandidates([
|
|
355
362
|
{ name: 'codex', id: 'codex', capabilities: ['model', 'codex'] },
|
|
356
363
|
{ name: 'chatgpt', id: 'chatgpt/browser-worker', capabilities: ['model', 'chatgpt', 'browser-harness'] },
|
|
357
364
|
], [], usage);
|
|
358
|
-
const result = await routeExecution(workers, { capabilities: [] }, {
|
|
365
|
+
const result = await routeExecution(workers, task.workerRequirements ?? { capabilities: task.capabilities ?? [] }, {
|
|
359
366
|
decisionEngine: new LayaCliDecisionEngine(),
|
|
360
367
|
decisionMode: 'active',
|
|
361
|
-
taskFacts: {
|
|
368
|
+
taskFacts: {
|
|
369
|
+
job: {
|
|
370
|
+
id: job.id, title: job.title, objective: job.objective ?? null,
|
|
371
|
+
description: job.description ?? null, priority: job.priority ?? null,
|
|
372
|
+
executionMode: job.executionMode, repository: job.repository,
|
|
373
|
+
prNumber: job.prNumber, state: job.state,
|
|
374
|
+
},
|
|
375
|
+
task: {
|
|
376
|
+
id: task.id, kind: task.kind ?? 'work', storySize: task.storySize ?? 'M',
|
|
377
|
+
priority: task.priority ?? null, agentName: task.agentName ?? null,
|
|
378
|
+
parentTaskId: task.parentTaskId, dependencies: task.dependencies,
|
|
379
|
+
capabilities: task.capabilities, input: task.input,
|
|
380
|
+
attachments: task.attachments ?? [], output: task.output ?? null,
|
|
381
|
+
state: task.state, attempt: task.attempt, maxAttempts: task.maxAttempts,
|
|
382
|
+
},
|
|
383
|
+
storySize: task.storySize ?? 'M',
|
|
384
|
+
},
|
|
362
385
|
usage,
|
|
363
386
|
chatgptRunning: runtime.workers.chatgpt,
|
|
364
387
|
});
|