agents-relay 2.0.23 → 2.0.24

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.
@@ -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
- const result = await routerTestProvider();
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 raw = taskFacts.storySize;
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
- id: task.id,
181
- kind: task.kind ?? 'work',
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: { storySize: 'M', role: 'router-test' },
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.23",
3
+ "version": "2.0.24",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {