agents-relay 1.0.103 → 1.0.105
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/agent-graph.js +14 -0
- package/dist/cli-help.js +3 -1
- package/dist/cli-support.js +10 -0
- package/dist/cli.js +11 -1
- package/dist/dashboard-html.js +38 -6
- package/dist/dashboard.js +42 -0
- package/dist/decision-audit.js +58 -0
- package/dist/execution-context.js +1 -0
- package/dist/model-redecision.js +41 -0
- package/dist/model-routing.js +41 -15
- package/dist/planner.js +8 -1
- package/dist/reconciler-review.js +17 -4
- package/dist/reconciler-runtime.js +117 -127
- package/dist/reconciler.js +12 -6
- package/dist/relayd.js +3 -1
- package/dist/reviewer-routing.js +3 -17
- package/dist/routing-policy.js +88 -0
- package/dist/runtime-status.js +2 -0
- package/dist/scheduler.js +10 -4
- package/dist/troubleshooter.js +1 -1
- package/dist/worker-routing.js +38 -31
- package/dist/workflow/contracts.js +4 -3
- package/dist/workflow/definitions.js +1 -0
- package/dist/workflow/durable-transition.js +45 -0
- package/dist/workflow/runtime.js +7 -0
- package/package.json +1 -1
- package/skills/agents-relay/agents/implementation.agent.md +24 -0
- package/skills/agents-relay/agents/planner.agent.md +5 -3
- package/skills/agents-relay/contracts/task-job-contract.md +8 -7
- package/workflow/task-lifecycle.mmd +9 -3
package/dist/agent-graph.js
CHANGED
|
@@ -66,8 +66,22 @@ export function parseAgentGraph(source) {
|
|
|
66
66
|
throw new Error('Agent Graph contains a cycle');
|
|
67
67
|
return { source, nodes: [...agents].map(([id, agent]) => ({ id, agent })), edges, waves };
|
|
68
68
|
}
|
|
69
|
+
export function validateAgentSelection(context, agentName, agentGraph) {
|
|
70
|
+
const visible = new Set(context.agents.map(agent => agent.name));
|
|
71
|
+
if (agentName && !visible.has(agentName)) {
|
|
72
|
+
throw new Error(`Agent ${agentName} is not visible in the resolved planning/execution context`);
|
|
73
|
+
}
|
|
74
|
+
if (!agentGraph)
|
|
75
|
+
return;
|
|
76
|
+
for (const node of parseAgentGraph(agentGraph).nodes) {
|
|
77
|
+
if (!visible.has(node.agent)) {
|
|
78
|
+
throw new Error(`Agent ${node.agent} referenced by agentGraph node ${node.id} is not visible in the resolved planning/execution context`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
69
82
|
export async function agentGraphPrompt(context, source) {
|
|
70
83
|
const graph = parseAgentGraph(source);
|
|
84
|
+
validateAgentSelection(context, undefined, source);
|
|
71
85
|
const contracts = await Promise.all(graph.nodes.map(async (node) => ({ node, prompt: await agentContractPrompt(context, node.agent) })));
|
|
72
86
|
const incoming = new Map(graph.nodes.map(node => [node.id, graph.edges.filter(edge => edge.to === node.id).map(edge => edge.from)]));
|
|
73
87
|
return `[Task Agent Graph]\n${source.trim()}\n\nExecution waves: ${graph.waves.map((wave, index) => `${index + 1}:${wave.join(',')}`).join(' | ')}\n\nExecute this graph inside the current durable Task. Nodes are reasoning roles, NOT durable child Tasks. Preserve the original Job/Task context for every node. Nodes in the same wave are independent: evaluate each from the same original context without using sibling prose. For each later node, provide the original context plus structured outputs only from its declared predecessors. Do not emit outer task.started or task terminal events per node; the current durable Task owns exactly one start and one terminal event.\n\n${contracts.map(({ node, prompt }) => `[Node ${node.id}; predecessors=${(incoming.get(node.id) ?? []).join(',') || 'none'}]\n${prompt}`).join('\n\n')}`;
|
package/dist/cli-help.js
CHANGED
|
@@ -156,10 +156,12 @@ Common: --task-id ID, --commit SHA, --project NAME.
|
|
|
156
156
|
|
|
157
157
|
Example:
|
|
158
158
|
npx agents-relay record --file .agents-relay.json --summary "Built release" --commit abc123`,
|
|
159
|
-
reconcile: `Reconcile a job once
|
|
159
|
+
reconcile: `Reconcile a job once by requesting one reconciliation pass.
|
|
160
160
|
|
|
161
161
|
Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
162
162
|
|
|
163
|
+
Managed GitHub jobs are reconciled by agents-relayd. The CLI publishes a job.wake request and does not launch workers; use --events nats.
|
|
164
|
+
|
|
163
165
|
Example:
|
|
164
166
|
npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1`,
|
|
165
167
|
tick: `Process autonomous jobs indexed into current/recent GitHub next-run buckets.
|
package/dist/cli-support.js
CHANGED
|
@@ -74,6 +74,16 @@ export async function publishWake(bus, job, taskId, message) { if (!bus)
|
|
|
74
74
|
catch (error) {
|
|
75
75
|
process.stderr.write(`warning: event wake unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
76
76
|
} }
|
|
77
|
+
export async function requestManagedReconciliation(bus, job) {
|
|
78
|
+
if (!bus)
|
|
79
|
+
throw new Error('Managed GitHub reconciliation requires --events nats so agents-relayd can execute the job');
|
|
80
|
+
try {
|
|
81
|
+
await bus.publish(eventFor(job.id, 'reconcile', null, 'job.wake', 'queued', `Reconcile requested for ${job.id}`, 'orchestrator', { repository: job.repository, prNumber: job.prNumber }));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw new Error(`Unable to deliver managed reconciliation wake to agents-relayd: ${error instanceof Error ? error.message : String(error)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
77
87
|
export function priority(args, fallback = 'P2') { const value = arg(args, '--priority', fallback); if (!['P0', 'P1', 'P2', 'P3'].includes(value))
|
|
78
88
|
throw new Error('--priority must be P0, P1, P2, or P3'); return value; }
|
|
79
89
|
export function storySize(args, fallback = 'M') { const value = arg(args, '--story-size', fallback).toUpperCase(); if (!['S', 'M', 'L'].includes(value))
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { defaultWorkspaceRoot, resolveManagedProjectPath } from './workspace.js'
|
|
|
22
22
|
import { runBlockerCommand } from './cli-blocker.js';
|
|
23
23
|
import { helpText, usage } from './cli-help.js';
|
|
24
24
|
export { helpText } from './cli-help.js';
|
|
25
|
-
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, } from './cli-support.js';
|
|
25
|
+
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, requestManagedReconciliation, } from './cli-support.js';
|
|
26
26
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
27
27
|
export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
|
|
28
28
|
if (!options.id)
|
|
@@ -646,6 +646,16 @@ async function main() {
|
|
|
646
646
|
console.log(JSON.stringify(await store.load(job.id), null, 2));
|
|
647
647
|
return;
|
|
648
648
|
}
|
|
649
|
+
if (command === 'reconcile' && !localFile) {
|
|
650
|
+
try {
|
|
651
|
+
await requestManagedReconciliation(bus, job);
|
|
652
|
+
console.log(JSON.stringify(await store.load(job.id), null, 2));
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
await bus?.close?.();
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
649
659
|
const reconciler = runtime(store, args, bus);
|
|
650
660
|
if (command === 'reconcile') {
|
|
651
661
|
await reconciler.reconcile(job.id);
|
package/dist/dashboard-html.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const packageVersion = createRequire(import.meta.url)('../package.json').version;
|
|
1
3
|
export const dashboardHtml = `<!doctype html>
|
|
2
4
|
<html lang="en" data-dashboard-contract="safe-dom,named-relay-events,snapshot-events,bounded-event-history">
|
|
3
5
|
<head>
|
|
@@ -11,6 +13,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
11
13
|
body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,sans-serif; }
|
|
12
14
|
header { position:sticky; top:0; z-index:3; background:var(--surface); border-bottom:1px solid var(--border); padding:.8rem 1rem; }
|
|
13
15
|
.brand { display:flex; align-items:baseline; gap:.65rem; min-width:0; }
|
|
16
|
+
.version { margin-left:auto; color:var(--muted); font-size:.72rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; white-space:nowrap; }
|
|
14
17
|
h1 { margin:0; font-size:1.08rem; letter-spacing:-.02em; }
|
|
15
18
|
.tagline,.subtle { color:var(--muted); font-size:.78rem; }
|
|
16
19
|
.warning-banner { margin:.65rem 0 0; padding:.5rem .65rem; border:1px solid #e6bf62; border-radius:.4rem; background:#fff4d6; color:#6f4b00; font-size:.78rem; font-weight:700; }
|
|
@@ -83,7 +86,13 @@ export const dashboardHtml = `<!doctype html>
|
|
|
83
86
|
.event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
|
|
84
87
|
.event-kind { color:var(--accent); font-weight:800; }
|
|
85
88
|
.event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
|
|
86
|
-
#usage-view { grid-column:1 / -1; }
|
|
89
|
+
#usage-view,#rules-view { grid-column:1 / -1; }
|
|
90
|
+
.rules-editor { display:grid; gap:.65rem; }
|
|
91
|
+
.rules-help { margin:0; color:var(--muted); font-size:.8rem; }
|
|
92
|
+
.rules-textarea { width:100%; min-height:55vh; resize:vertical; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); color:var(--text); padding:.8rem; font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
93
|
+
.rules-footer { display:flex; align-items:center; justify-content:space-between; gap:.75rem; flex-wrap:wrap; }
|
|
94
|
+
.rules-save { border:1px solid var(--accent); border-radius:.35rem; background:var(--accent); color:#fff; padding:.38rem .8rem; font:inherit; font-weight:750; cursor:pointer; }
|
|
95
|
+
.rules-save:disabled { opacity:.55; cursor:default; }
|
|
87
96
|
.usage-providers { display:grid; gap:.55rem; }
|
|
88
97
|
.usage-provider { display:grid; grid-template-columns:minmax(130px,180px) minmax(0,1fr); gap:1rem; align-items:center; padding:.7rem .8rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
|
|
89
98
|
.usage-provider h3 { margin:0; font-size:.86rem; }
|
|
@@ -103,11 +112,12 @@ export const dashboardHtml = `<!doctype html>
|
|
|
103
112
|
</head>
|
|
104
113
|
<body>
|
|
105
114
|
<header>
|
|
106
|
-
<div class="brand"><h1>Agents Relay Console</h1><p class="tagline">Durable PR state is authoritative; events are observability.</p></div>
|
|
115
|
+
<div class="brand"><h1>Agents Relay Console</h1><p class="tagline">Durable PR state is authoritative; events are observability.</p><span class="version">v${packageVersion}</span></div>
|
|
107
116
|
<div id="rate-limit-warning" class="warning-banner" role="status" hidden></div>
|
|
108
117
|
<nav role="tablist" aria-label="Console views">
|
|
109
118
|
<button id="jobs-tab" type="button" role="tab" aria-selected="true" aria-controls="jobs-view">Jobs</button>
|
|
110
119
|
<button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
|
|
120
|
+
<button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Model Router Rules</button>
|
|
111
121
|
</nav>
|
|
112
122
|
</header>
|
|
113
123
|
<main>
|
|
@@ -137,6 +147,18 @@ export const dashboardHtml = `<!doctype html>
|
|
|
137
147
|
<div class="panel-body" id="usage-root" aria-live="polite"><div class="skeleton"></div></div>
|
|
138
148
|
</div>
|
|
139
149
|
</section>
|
|
150
|
+
<section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden>
|
|
151
|
+
<div class="panel">
|
|
152
|
+
<div class="panel-head"><h2>Model Router Rules</h2><span id="rules-meta" class="subtle"></span></div>
|
|
153
|
+
<div class="panel-body">
|
|
154
|
+
<div class="rules-editor">
|
|
155
|
+
<p class="rules-help">Free-text guidance loaded by the model decider for every routing decision. Hard capability and health gates remain enforced by code.</p>
|
|
156
|
+
<textarea id="rules-text" class="rules-textarea" spellcheck="true" aria-label="Model router rules" placeholder="Example: Prefer ZAI when paid quota is abundant and task quality is sufficient. If a provider/runtime path just failed, reconsider using the failure evidence and fresh usage."></textarea>
|
|
157
|
+
<div class="rules-footer"><span id="rules-status" class="subtle" aria-live="polite"></span><button id="rules-save" class="rules-save" type="button">Save rules</button></div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</section>
|
|
140
162
|
</main>
|
|
141
163
|
<script>
|
|
142
164
|
'use strict';
|
|
@@ -155,8 +177,10 @@ export const dashboardHtml = `<!doctype html>
|
|
|
155
177
|
const MAX_EVENTS=60;
|
|
156
178
|
const jobsTab=document.querySelector('#jobs-tab');
|
|
157
179
|
const usageTab=document.querySelector('#usage-tab');
|
|
180
|
+
const rulesTab=document.querySelector('#rules-tab');
|
|
158
181
|
const jobsView=document.querySelector('#jobs-view');
|
|
159
182
|
const usageView=document.querySelector('#usage-view');
|
|
183
|
+
const rulesView=document.querySelector('#rules-view');
|
|
160
184
|
const jobsWorkspace=document.querySelector('#jobs-workspace');
|
|
161
185
|
const jobList=document.querySelector('#job-list');
|
|
162
186
|
const jobsRefresh=document.querySelector('#jobs-refresh');
|
|
@@ -176,6 +200,10 @@ const connection=document.querySelector('#connection');
|
|
|
176
200
|
const selectedJobTitle=document.querySelector('#selected-job-title');
|
|
177
201
|
const usageRoot=document.querySelector('#usage-root');
|
|
178
202
|
const usageMeta=document.querySelector('#usage-meta');
|
|
203
|
+
const rulesText=document.querySelector('#rules-text');
|
|
204
|
+
const rulesMeta=document.querySelector('#rules-meta');
|
|
205
|
+
const rulesStatus=document.querySelector('#rules-status');
|
|
206
|
+
const rulesSave=document.querySelector('#rules-save');
|
|
179
207
|
const rateLimitWarning=document.querySelector('#rate-limit-warning');
|
|
180
208
|
let jobs=[];
|
|
181
209
|
let selectedJob=sessionStorage.getItem('agents-relay:selected-job');
|
|
@@ -266,7 +294,9 @@ function usageProvider(provider){const card=element('section','usage-provider');
|
|
|
266
294
|
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;}
|
|
267
295
|
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;}
|
|
268
296
|
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:[];if(!providers.length&&!credits.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}const grid=element('div','usage-providers');for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
|
|
269
|
-
function
|
|
297
|
+
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;}}
|
|
298
|
+
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;}}
|
|
299
|
+
function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isRules=view==='rules';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;rulesView.hidden=!isRules;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));rulesTab.setAttribute('aria-selected',String(isRules));if(isJobs)void refresh();else if(isUsage)void drawUsage();else void drawRules();}
|
|
270
300
|
jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
|
|
271
301
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
272
302
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
@@ -277,15 +307,17 @@ quickDraft.addEventListener('click',()=>toggleQuickMode('draft'));
|
|
|
277
307
|
quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
|
|
278
308
|
selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
|
|
279
309
|
usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
|
|
280
|
-
|
|
310
|
+
rulesTab.addEventListener('click',()=>{location.hash='#/model-router-rules';showView('rules');});
|
|
311
|
+
rulesSave.addEventListener('click',()=>void saveRules());
|
|
312
|
+
window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const next=location.hash==='#/usage'?'rules':location.hash==='#/model-router-rules'?'jobs':'usage';location.hash=next==='usage'?'#/usage':next==='rules'?'#/model-router-rules':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
|
|
281
313
|
function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
|
|
282
314
|
const stream=new EventSource('/events');
|
|
283
315
|
stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
|
|
284
316
|
stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
|
|
285
317
|
stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);renderAll();}};
|
|
286
318
|
stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);}if(payload?.event)addEvent(payload.event);renderAll();});
|
|
287
|
-
if(location.hash==='#/usage')showView('usage');else showView('jobs');
|
|
288
|
-
setInterval(()=>{if(location.hash!=='#/usage')void refresh();},300000);
|
|
319
|
+
if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/model-router-rules')showView('rules');else showView('jobs');
|
|
320
|
+
setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/model-router-rules')void refresh();},300000);
|
|
289
321
|
setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
|
|
290
322
|
async function refreshSelected(){if(!selectedJob){await refresh();return;}selectedJobRefresh.disabled=true;selectedJobRefresh.textContent='Refreshing…';try{const updated=await loadJSON('/api/job?id='+encodeURIComponent(selectedJob));const index=jobs.findIndex(job=>job.id===selectedJob);if(index>=0)jobs[index]=updated;else jobs.push(updated);rememberSelection();renderAll();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';}finally{selectedJobRefresh.disabled=false;selectedJobRefresh.textContent='Refresh';}}
|
|
291
323
|
</script>
|
package/dist/dashboard.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { readUsageSnapshot } from './usage.js';
|
|
6
6
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
7
7
|
import { DeliveryDeduper, parseGitHubWebhook, verifyGitHubWebhook } from './github-webhook.js';
|
|
8
|
+
import { loadModelRouterRules, saveModelRouterRules } from './routing-policy.js';
|
|
8
9
|
const GITHUB_REPOSITORY_PATTERN = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
|
|
9
10
|
const SSE_HEARTBEAT_MS = 15000;
|
|
10
11
|
const EVENTS_HISTORY_FILE = process.env.EVENTS_BUS_DATA_DIR ? join(process.env.EVENTS_BUS_DATA_DIR, 'events.jsonl') : join(homedir(), 'Library', 'Application Support', 'EventsBus', 'events.jsonl');
|
|
@@ -267,6 +268,47 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
267
268
|
res.end(JSON.stringify(events));
|
|
268
269
|
return;
|
|
269
270
|
}
|
|
271
|
+
if (req.url === '/api/model-router-rules') {
|
|
272
|
+
if (req.method === 'GET') {
|
|
273
|
+
try {
|
|
274
|
+
const rules = await loadModelRouterRules();
|
|
275
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
276
|
+
res.end(JSON.stringify(rules));
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
res.writeHead(500);
|
|
280
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (req.method === 'PUT') {
|
|
285
|
+
const chunks = [];
|
|
286
|
+
let size = 0;
|
|
287
|
+
for await (const chunk of req) {
|
|
288
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
289
|
+
size += buffer.length;
|
|
290
|
+
if (size > 64 * 1024) {
|
|
291
|
+
res.writeHead(413);
|
|
292
|
+
res.end('Model router rules exceed 64 KiB');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
chunks.push(buffer);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const rules = await saveModelRouterRules(Buffer.concat(chunks).toString('utf8'));
|
|
299
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
300
|
+
res.end(JSON.stringify(rules));
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
res.writeHead(500);
|
|
304
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
res.writeHead(405, { allow: 'GET, PUT' });
|
|
309
|
+
res.end('Method not allowed');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
270
312
|
if (req.url === '/api/status') {
|
|
271
313
|
const status = statusProvider ? statusProvider() : { limited: false, cooldownUntil: null };
|
|
272
314
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
4
|
+
export const DECISION_AUDIT_SCHEMA_VERSION = 2;
|
|
5
|
+
export class JsonlDecisionAuditStore {
|
|
6
|
+
path;
|
|
7
|
+
constructor(path) {
|
|
8
|
+
this.path = path;
|
|
9
|
+
}
|
|
10
|
+
async append(record) { await mkdir(dirname(this.path), { recursive: true }); await appendFile(this.path, `${JSON.stringify(sanitize(record))}\n`, 'utf8'); }
|
|
11
|
+
async read() {
|
|
12
|
+
try {
|
|
13
|
+
return (await readFile(this.path, 'utf8')).split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sanitize(value, key = '') {
|
|
21
|
+
if (/(token|secret|password|credential|authorization|api.?key)/i.test(key))
|
|
22
|
+
return '[redacted]';
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return value.map(item => sanitize(item));
|
|
25
|
+
if (value && typeof value === 'object')
|
|
26
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, sanitize(v, k)]));
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
export function decisionAuditId() { return randomUUID(); }
|
|
30
|
+
export function auditInputHash(record) { return createHash('sha256').update(JSON.stringify(record)).digest('hex'); }
|
|
31
|
+
export function outcomeForDecision(decisionId, kind, summary, evidence) { return { record_type: 'outcome', schema_version: DECISION_AUDIT_SCHEMA_VERSION, outcome_id: randomUUID(), decision_id: decisionId, timestamp: new Date().toISOString(), kind, summary, evidence }; }
|
|
32
|
+
export function taskAuditFacts(task) {
|
|
33
|
+
if (!task)
|
|
34
|
+
return {};
|
|
35
|
+
return { job_id: task.jobId, task_id: task.id, attempt: task.attempt, execution_id: task.executionId, capabilities: [...task.capabilities], input_class: task.input.slice(0, 80).replace(/\s+/g, ' ') };
|
|
36
|
+
}
|
|
37
|
+
export function makeDecisionAudit(input) {
|
|
38
|
+
const candidate = (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 });
|
|
39
|
+
const eligibleIds = new Set(input.eligibleCandidates.map(item => item.id));
|
|
40
|
+
const gate_results = input.candidates.map(item => {
|
|
41
|
+
const reasons = [];
|
|
42
|
+
if (item.resourceHealthy === false)
|
|
43
|
+
reasons.push('resource_unhealthy');
|
|
44
|
+
const constraints = input.contract.constraints;
|
|
45
|
+
if (typeof constraints?.provider === 'string' && constraints.provider !== item.provider)
|
|
46
|
+
reasons.push('provider_constraint');
|
|
47
|
+
if (typeof constraints?.model === 'string' && constraints.model !== item.model)
|
|
48
|
+
reasons.push('model_constraint');
|
|
49
|
+
if (typeof constraints?.profile === 'string' && constraints.profile !== item.profile)
|
|
50
|
+
reasons.push('profile_constraint');
|
|
51
|
+
if (input.policy.policy.disabledProviders?.includes(item.provider))
|
|
52
|
+
reasons.push('policy_disabled_provider');
|
|
53
|
+
if (item.profile && input.policy.policy.disabledProfiles?.includes(item.profile))
|
|
54
|
+
reasons.push('policy_disabled_profile');
|
|
55
|
+
return { candidate_id: item.id, eligible: eligibleIds.has(item.id), rejection_reasons: reasons };
|
|
56
|
+
});
|
|
57
|
+
return { record_type: 'decision', schema_version: DECISION_AUDIT_SCHEMA_VERSION, decision_id: decisionAuditId(), prior_decision_id: input.priorDecisionId, timestamp: input.routing.decidedAt, correlation: { job_id: input.task?.jobId, task_id: input.task?.id, execution_id: input.task?.executionId, attempt: input.task?.attempt }, task: taskAuditFacts(input.task), worker: input.worker, contract: input.contract, policy: { path: input.policy.path, hash: input.policy.hash, snapshot: input.policy.snapshot, rules: input.policy.rules }, candidates: input.candidates.map(candidate), gate_results, eligible_candidates: input.eligibleCandidates.map(candidate), selected: candidate(input.selected), engine: input.routing.decidedBy, fallback: { used: input.routing.decisionEvidence?.fallback ?? false, reason: input.routing.decisionEvidence?.error }, decision_evidence: input.routing.decisionEvidence, prior_failure: input.priorFailure };
|
|
58
|
+
}
|
|
@@ -258,6 +258,7 @@ export function executionContextPrompt(context) {
|
|
|
258
258
|
const skills = context.skills.map(item => `- ${item.name}: ${item.path} [${item.source}]`).join('\n') || '- none';
|
|
259
259
|
const agents = context.agents.map(item => {
|
|
260
260
|
const details = [
|
|
261
|
+
item.description ? `purpose=${item.description}` : '',
|
|
261
262
|
item.capabilities.length ? `capabilities=${item.capabilities.join(',')}` : '',
|
|
262
263
|
item.runtime ? `runtime=${item.runtime}` : '',
|
|
263
264
|
item.trustDomains.length ? `trust=${item.trustDomains.join(',')}` : '',
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { outcomeForDecision } from './decision-audit.js';
|
|
2
|
+
import { discoverModelCandidates, routeModel } from './model-routing.js';
|
|
3
|
+
import { readUsageSnapshot } from './usage.js';
|
|
4
|
+
/** Classify from durable lifecycle evidence and logs; task.error is only a supporting hint. */
|
|
5
|
+
export function classifyFailureEvidence(input) {
|
|
6
|
+
const text = [...input.events.map(event => `${event.type} ${event.message ?? ''} ${JSON.stringify(event.data ?? {})}`), ...(input.logs ?? [])].join('\n').toLowerCase();
|
|
7
|
+
const model = /model|provider|laya|launch|auth|credential|configuration|incompatible|rate.?limit|quota|unavailable/.test(text);
|
|
8
|
+
const infra = /worker|tool|daemon|relay|network|socket|nats|checkout|filesystem/.test(text);
|
|
9
|
+
const semantic = /assert|test failed|implementation|compile|typescript|syntax|semantic/.test(text);
|
|
10
|
+
const timeout = /timeout|timed out|deadline/.test(text);
|
|
11
|
+
if (model && !infra)
|
|
12
|
+
return { classification: 'model_provider_runtime', summary: 'Durable events/logs indicate the selected model/provider/runtime path failed.', sources: ['task.events', 'worker.logs'] };
|
|
13
|
+
if (infra)
|
|
14
|
+
return { classification: 'worker_tool_infrastructure', summary: 'Durable events/logs indicate worker, tool, or infrastructure failure unrelated to model choice.', sources: ['task.events', 'worker.logs'] };
|
|
15
|
+
if (semantic)
|
|
16
|
+
return { classification: 'semantic_task', summary: 'Durable events/logs indicate meaningful task execution failed semantically.', sources: ['task.events', 'worker.logs'] };
|
|
17
|
+
if (timeout)
|
|
18
|
+
return { classification: 'timeout_unknown', summary: 'Timeout evidence is insufficient to attribute failure to model choice.', sources: ['task.events', 'worker.logs'] };
|
|
19
|
+
return { classification: 'timeout_unknown', summary: input.events.length === 0 && !input.logs?.length ? 'No durable event or log evidence was supplied.' : 'Failure attribution is unknown from the available evidence.', sources: ['task.events', 'worker.logs'] };
|
|
20
|
+
}
|
|
21
|
+
/** Re-route only. This action never retries or changes the Task lifecycle. */
|
|
22
|
+
export async function redecideFailedTask(options) {
|
|
23
|
+
if (options.evidence.classification !== 'model_provider_runtime')
|
|
24
|
+
throw new Error('Model re-decision requires model/provider/runtime evidence');
|
|
25
|
+
const usage = await readUsageSnapshot(options.usage);
|
|
26
|
+
const candidates = await discoverModelCandidates(options.worker, options.registered, usage);
|
|
27
|
+
const result = await routeModel(options.worker, candidates, {
|
|
28
|
+
decisionEngine: options.decisionEngine,
|
|
29
|
+
decisionMode: options.decisionMode,
|
|
30
|
+
policyPath: options.policyPath,
|
|
31
|
+
auditStore: options.auditStore,
|
|
32
|
+
auditTask: options.task,
|
|
33
|
+
taskFacts: { kind: options.task.kind ?? 'work', capabilities: options.task.capabilities, prior_routing: options.task.routing ?? null, fresh_usage_generated_at: usage.generatedAt },
|
|
34
|
+
priorDecisionId: options.task.routing?.decisionId,
|
|
35
|
+
priorFailure: options.evidence,
|
|
36
|
+
});
|
|
37
|
+
await options.updateRouting(options.task, result.routing);
|
|
38
|
+
if (options.auditStore && options.task.routing?.decisionId)
|
|
39
|
+
await options.auditStore.append(outcomeForDecision(options.task.routing.decisionId, 'provider_failure', options.evidence.summary, { classification: options.evidence.classification, fresh_usage_generated_at: usage.generatedAt }));
|
|
40
|
+
return { ...result, freshUsageGeneratedAt: usage.generatedAt };
|
|
41
|
+
}
|
package/dist/model-routing.js
CHANGED
|
@@ -3,6 +3,8 @@ import { basename, join } from 'node:path';
|
|
|
3
3
|
import { readdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { chooseWithDecision } from './decision.js';
|
|
5
5
|
import { usageForProvider } from './worker-routing.js';
|
|
6
|
+
import { makeDecisionAudit } from './decision-audit.js';
|
|
7
|
+
import { loadRoutingPolicy, policyAllows, policyRank } from './routing-policy.js';
|
|
6
8
|
function topLevelCodexConfig(text) {
|
|
7
9
|
const result = {};
|
|
8
10
|
let inSection = false;
|
|
@@ -73,10 +75,12 @@ export async function discoverModelCandidates(worker, registered, usage, codexHo
|
|
|
73
75
|
candidates.push(withResource({ id: 'default:openai/gpt-5.6-luna', provider: 'openai', model: 'gpt-5.6-luna' }, usage));
|
|
74
76
|
return candidates.filter((candidate, index, all) => all.findIndex(item => item.id === candidate.id) === index);
|
|
75
77
|
}
|
|
76
|
-
function eligibleModels(candidates, constraints) {
|
|
78
|
+
function eligibleModels(candidates, constraints, policy) {
|
|
77
79
|
return candidates.filter(candidate => {
|
|
78
80
|
if (candidate.resourceHealthy === false)
|
|
79
81
|
return false;
|
|
82
|
+
if (policy && !policyAllows(candidate, policy.policy))
|
|
83
|
+
return false;
|
|
80
84
|
if (constraints?.provider && candidate.provider !== constraints.provider)
|
|
81
85
|
return false;
|
|
82
86
|
if (constraints?.model && candidate.model !== constraints.model)
|
|
@@ -88,37 +92,59 @@ function eligibleModels(candidates, constraints) {
|
|
|
88
92
|
return true;
|
|
89
93
|
});
|
|
90
94
|
}
|
|
91
|
-
function deterministicModel(candidates) {
|
|
95
|
+
function deterministicModel(candidates, taskKind, policy) {
|
|
92
96
|
return [...candidates].map((candidate, index) => ({ candidate, index })).sort((left, right) => {
|
|
93
97
|
const pressure = (item) => item.resourceUsedPercent === undefined ? 1 : item.resourceUsedPercent >= 90 ? 2 : 0;
|
|
94
|
-
return pressure(left.candidate) - pressure(right.candidate) || left.index - right.index;
|
|
98
|
+
return pressure(left.candidate) - pressure(right.candidate) || (policy ? policyRank(right.candidate, taskKind, policy.policy) - policyRank(left.candidate, taskKind, policy.policy) : 0) || left.index - right.index;
|
|
95
99
|
})[0].candidate;
|
|
96
100
|
}
|
|
97
101
|
export async function routeModel(worker, candidates, options = {}) {
|
|
98
|
-
const
|
|
102
|
+
const policy = await loadRoutingPolicy(options.policyPath, options.rulesPath);
|
|
103
|
+
const eligible = eligibleModels(candidates, options.constraints, policy);
|
|
99
104
|
if (eligible.length === 0)
|
|
100
105
|
throw new Error(`No eligible model profile is usable by worker ${worker.workerId}`);
|
|
101
|
-
const
|
|
106
|
+
const taskKind = typeof options.taskFacts?.kind === 'string' ? options.taskFacts.kind : undefined;
|
|
107
|
+
const deterministic = deterministicModel(eligible, taskKind, policy);
|
|
102
108
|
const choice = eligible.length === 1
|
|
103
109
|
? { choice: deterministic.id, evidence: { mode: options.decisionMode ?? 'off', fallback: true } }
|
|
104
110
|
: await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', {
|
|
105
111
|
facts: JSON.parse(JSON.stringify({
|
|
106
112
|
task: options.taskFacts ?? {},
|
|
113
|
+
environment_policy: { structured_policy: policy.snapshot, rules: policy.rules.text },
|
|
114
|
+
prior_decision_id: options.priorDecisionId ?? null,
|
|
115
|
+
prior_failure: options.priorFailure ?? null,
|
|
107
116
|
worker: { id: worker.workerId, adapter: worker.adapter, capabilities: [...worker.capabilities] },
|
|
108
117
|
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
118
|
})),
|
|
110
|
-
}, { type: 'choice', question: 'Which eligible model profile is best suited to execute this task?
|
|
119
|
+
}, { type: 'choice', question: 'Which eligible model profile is best suited to execute this task? Apply hard gates first, then explicit task constraints, then current environment policy; consider every quota window and never violate task fit or continuity.', choices: eligible.map(item => item.id) }, () => deterministic.id);
|
|
111
120
|
const selected = eligible.find(item => item.id === choice.choice) ?? deterministic;
|
|
121
|
+
const routing = {
|
|
122
|
+
provider: selected.provider,
|
|
123
|
+
model: selected.model,
|
|
124
|
+
profile: selected.profile,
|
|
125
|
+
reasoning: options.constraints?.reasoning ?? selected.reasoning,
|
|
126
|
+
decidedBy: choice.evidence.engine && !choice.evidence.fallback ? 'model-router:laya' : 'model-router',
|
|
127
|
+
decidedAt: new Date().toISOString(),
|
|
128
|
+
decisionEvidence: choice.evidence,
|
|
129
|
+
};
|
|
130
|
+
const audit = makeDecisionAudit({
|
|
131
|
+
task: options.auditTask,
|
|
132
|
+
worker: { id: worker.workerId, adapter: worker.adapter, capabilities: [...worker.capabilities] },
|
|
133
|
+
contract: { question: 'model-profile-selection', task: options.taskFacts ?? {}, constraints: options.constraints ?? {} },
|
|
134
|
+
policy,
|
|
135
|
+
candidates,
|
|
136
|
+
eligibleCandidates: eligible,
|
|
137
|
+
selected,
|
|
138
|
+
routing,
|
|
139
|
+
priorDecisionId: options.priorDecisionId,
|
|
140
|
+
priorFailure: options.priorFailure,
|
|
141
|
+
});
|
|
142
|
+
routing.decisionId = audit.decision_id;
|
|
143
|
+
if (options.auditStore)
|
|
144
|
+
await options.auditStore.append(audit);
|
|
112
145
|
return {
|
|
113
146
|
candidate: selected,
|
|
114
|
-
routing
|
|
115
|
-
|
|
116
|
-
model: selected.model,
|
|
117
|
-
profile: selected.profile,
|
|
118
|
-
reasoning: options.constraints?.reasoning ?? selected.reasoning,
|
|
119
|
-
decidedBy: choice.evidence.engine && !choice.evidence.fallback ? 'model-router:laya' : 'model-router',
|
|
120
|
-
decidedAt: new Date().toISOString(),
|
|
121
|
-
decisionEvidence: choice.evidence,
|
|
122
|
-
},
|
|
147
|
+
routing,
|
|
148
|
+
audit,
|
|
123
149
|
};
|
|
124
150
|
}
|
package/dist/planner.js
CHANGED
|
@@ -140,7 +140,14 @@ export function parsePlannerOutput(output) {
|
|
|
140
140
|
return parsePlannerResult(JSON.parse(candidate));
|
|
141
141
|
}
|
|
142
142
|
export function plannerInput(context) {
|
|
143
|
-
|
|
143
|
+
const visibleAgents = context.executionContext?.agents.map(agent => ({
|
|
144
|
+
name: agent.name,
|
|
145
|
+
description: agent.description,
|
|
146
|
+
capabilities: agent.capabilities,
|
|
147
|
+
boundaries: agent.restrictions,
|
|
148
|
+
runtime: agent.runtime,
|
|
149
|
+
}));
|
|
150
|
+
return JSON.stringify({ objective: context.objective, previous_planner_task_id: context.previousPlannerTaskId, visible_agents: visibleAgents ?? [], execution_context: context.executionContext, tasks: context.tasks.map(task => ({ id: task.id, kind: task.kind ?? 'work', parent_task_id: task.parentTaskId, state: task.state, input: task.input, result: task.result, planner_result: task.plannerResult, error: task.error })) });
|
|
144
151
|
}
|
|
145
152
|
export class CommandObjectivePlanner {
|
|
146
153
|
command;
|
|
@@ -109,12 +109,25 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
109
109
|
const registered = await this.store.listAgents();
|
|
110
110
|
const candidates = workerCandidates(this.options.adapters, registered, usage);
|
|
111
111
|
for (const task of job.tasks.filter(item => item.kind !== 'planner' && item.agentName !== 'reviewer' && item.adapter !== 'orchestrator' && ['QUEUED', 'BLOCKED'].includes(item.state) && item.output && !item.workerSelection && (item.workerRequirements || item.planningSource === 'model-planner'))) {
|
|
112
|
-
const requirements = task.workerRequirements ?? { capabilities:
|
|
113
|
-
const taskFacts = {
|
|
114
|
-
|
|
112
|
+
const requirements = task.workerRequirements ?? { capabilities: [] };
|
|
113
|
+
const taskFacts = {
|
|
114
|
+
id: task.id,
|
|
115
|
+
kind: task.kind ?? 'work',
|
|
116
|
+
storySize: task.storySize ?? 'M',
|
|
117
|
+
agentName: task.agentName ?? null,
|
|
118
|
+
input: task.input,
|
|
119
|
+
attachments: task.attachments ?? [],
|
|
120
|
+
output: task.output ?? null,
|
|
121
|
+
};
|
|
122
|
+
const routedWorker = await routeWorker(candidates, requirements, {
|
|
123
|
+
decisionEngine: this.options.decisionEngine,
|
|
124
|
+
decisionMode: this.options.decisionMode,
|
|
125
|
+
taskFacts,
|
|
126
|
+
usage,
|
|
127
|
+
});
|
|
115
128
|
const models = await discoverModelCandidates(routedWorker.candidate, registered, usage);
|
|
116
129
|
const routingConstraints = task.routing ? { provider: task.routing.provider, model: task.routing.model, profile: task.routing.profile, reasoning: task.routing.reasoning } : undefined;
|
|
117
|
-
const routedModel = await routeModel(routedWorker.candidate, models, { decisionEngine: this.options.decisionEngine, decisionMode: this.options.decisionMode, taskFacts, constraints: routingConstraints });
|
|
130
|
+
const routedModel = await routeModel(routedWorker.candidate, models, { decisionEngine: this.options.decisionEngine, decisionMode: this.options.decisionMode, taskFacts, constraints: routingConstraints, auditStore: this.options.decisionAuditStore, auditTask: task });
|
|
118
131
|
task.workerSelection = routedWorker.selection;
|
|
119
132
|
task.adapter = routedWorker.candidate.adapter;
|
|
120
133
|
task.routing = routedModel.routing;
|