agents-relay 1.0.150 → 1.0.152
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/README.md +1 -1
- package/dist/cli-help.js +1 -1
- package/dist/dashboard-html.js +5 -1
- package/dist/dashboard.js +7 -2
- package/dist/execution-routing.js +166 -0
- package/dist/reconciler-review.js +23 -25
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +2 -2
- package/skills/agents-relay/contracts/task-job-contract.md +5 -5
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ Planner-created tasks use stable IDs and the existing parent/subtask tree, so
|
|
|
81
81
|
retries and daemon restarts do not create duplicate work.
|
|
82
82
|
Task producers must follow `skills/agents-relay/contracts/task-job-contract.md`; that shared skill contract defines story size, lifecycle events, review, and auto-merge semantics.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Execution routing is code-driven: Relay builds complete executable worker+model routes from authoritative runtime/model/resource facts, removes only impossible routes, then calls the generic typed `decide(request, contract)` boundary once. Free-text routing guidance and task/resource/capability signals are soft decision facts; decision failure falls back deterministically to a usable Codex default route. See [Execution Router and typed decisions](docs/worker-model-routers.md).
|
|
85
85
|
All task producers use the same story-size contract: `S`≈5 minutes, `M`≈15 minutes, and `L`≈45 minutes of normal-reasoning execution, with `M` as the default. `high` reasoning doubles that overall window. ChatGPT Browser Worker start-ack timeout is derived as one third of the effective overall timeout, clamped between 1 and 5 minutes. Orchestrators and planners should choose `storySize`, not raw timeout milliseconds; `--timeout` remains only as an explicit compatibility/debug override.
|
|
86
86
|
|
|
87
87
|
Autonomous jobs can also be scheduled without a Relay database. The job marker
|
package/dist/cli-help.js
CHANGED
|
@@ -99,7 +99,7 @@ Actions:
|
|
|
99
99
|
'task create': `Create one durable Task under an existing Job.
|
|
100
100
|
|
|
101
101
|
Required: normal Job storage options plus --input-file PATH|- (preferred) or --input MARKDOWN, and --output task-pr|file. --input and --input-file are mutually exclusive.
|
|
102
|
-
Common: --task-id ID, --priority P0|P1|P2|P3, --story-size S|M|L, --agent NAME, --agent-graph-file PATH|-, --deps a,b, --parent ID, --draft. Normal model-backed Tasks omit adapter/provider/model so Relay runs
|
|
102
|
+
Common: --task-id ID, --priority P0|P1|P2|P3, --story-size S|M|L, --agent NAME, --agent-graph-file PATH|-, --deps a,b, --parent ID, --draft. Normal model-backed Tasks omit adapter/provider/model so Relay runs one Execution Router decision over complete worker+model routes. Use --adapter orchestrator only for synchronous self-work; explicit adapter/provider/model remain compatibility/debug constraints.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
103
103
|
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
104
104
|
|
|
105
105
|
Example:
|
package/dist/dashboard-html.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
const packageVersion = createRequire(import.meta.url)('../package.json').version;
|
|
3
|
+
export const dashboardVersion = packageVersion;
|
|
3
4
|
export const dashboardHtml = `<!doctype html>
|
|
4
5
|
<html lang="en" data-dashboard-contract="safe-dom,named-relay-events,snapshot-events,bounded-event-history">
|
|
5
6
|
<head>
|
|
@@ -163,7 +164,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
163
164
|
</head>
|
|
164
165
|
<body>
|
|
165
166
|
<header>
|
|
166
|
-
<div class="brand"><h1>Agents Relay Console</h1><p class="tagline">Durable PR state is authoritative; events are observability.</p><span class="version">v${
|
|
167
|
+
<div class="brand"><h1>Agents Relay Console</h1><p class="tagline">Durable PR state is authoritative; events are observability.</p><span class="version">v${dashboardVersion}</span></div>
|
|
167
168
|
<div id="runtime-summary" class="runtime-summary" aria-label="Runtime summary"></div>
|
|
168
169
|
<div id="rate-limit-warning" class="warning-banner" role="status" hidden></div>
|
|
169
170
|
<nav role="tablist" aria-label="Console views">
|
|
@@ -240,6 +241,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
240
241
|
// function usageBar(label, window)
|
|
241
242
|
|
|
242
243
|
const MAX_EVENTS=60;
|
|
244
|
+
const DASHBOARD_VERSION=${JSON.stringify(dashboardVersion)};
|
|
243
245
|
const jobsTab=document.querySelector('#jobs-tab');
|
|
244
246
|
const usageTab=document.querySelector('#usage-tab');
|
|
245
247
|
const rulesTab=document.querySelector('#rules-tab');
|
|
@@ -352,6 +354,7 @@ function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)
|
|
|
352
354
|
function jobGroup(job){if(isTerminalJob(job))return 3;if(job.executionMode==='autonomous')return 1;if(job.draft||job.state==='DRAFT')return 2;return 0;}
|
|
353
355
|
function compareJobs(a,b){const groupDelta=jobGroup(a)-jobGroup(b);if(groupDelta)return groupDelta;const mergePendingDelta=Number(isCompleteUnmerged(b))-Number(isCompleteUnmerged(a));return mergePendingDelta||String(b.updatedAt).localeCompare(String(a.updatedAt));}
|
|
354
356
|
async function loadJSON(url){const response=await fetch(url);if(!response.ok)throw new Error(response.status+' '+response.statusText);return response.json();}
|
|
357
|
+
async function refreshVersion(){try{const live=await loadJSON('/api/version');if(live?.version&&live.version!==DASHBOARD_VERSION)location.reload();}catch{}}
|
|
355
358
|
function renderRateLimitStatus(status){
|
|
356
359
|
if(!status?.limited){rateLimitWarning.hidden=true;rateLimitWarning.textContent='';return;}
|
|
357
360
|
const until=status.cooldownUntil?new Date(status.cooldownUntil):null;
|
|
@@ -426,6 +429,7 @@ stream.addEventListener('relay',message=>{const payload=parseMessage(message);if
|
|
|
426
429
|
if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/model-router-rules')showView('rules');else if(location.hash==='#/worker-router-rules')showView('worker-rules');else showView('jobs');
|
|
427
430
|
setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/model-router-rules'&&location.hash!=='#/worker-router-rules')void refresh();},300000);
|
|
428
431
|
setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
|
|
432
|
+
setInterval(()=>void refreshVersion(),30000);
|
|
429
433
|
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';}}
|
|
430
434
|
</script>
|
|
431
435
|
</body>
|
package/dist/dashboard.js
CHANGED
|
@@ -53,8 +53,8 @@ async function eventHistory(jobId, taskId, limit) {
|
|
|
53
53
|
}
|
|
54
54
|
return output;
|
|
55
55
|
}
|
|
56
|
-
export { dashboardHtml } from './dashboard-html.js';
|
|
57
|
-
import { dashboardHtml } from './dashboard-html.js';
|
|
56
|
+
export { dashboardHtml, dashboardVersion } from './dashboard-html.js';
|
|
57
|
+
import { dashboardHtml, dashboardVersion } from './dashboard-html.js';
|
|
58
58
|
export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobId, usageRegistry, overviewProvider, webhook, statusProvider, githubUsageProvider, exactJobProvider, actions) {
|
|
59
59
|
const legacyActions = typeof exactJobProvider === 'object' && exactJobProvider !== null ? exactJobProvider : undefined;
|
|
60
60
|
const legacyExactJobProvider = !exactJobProvider && githubUsageProvider ? githubUsageProvider : undefined;
|
|
@@ -154,6 +154,11 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
154
154
|
res.end(dashboardHtml);
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
|
+
if (req.url === '/api/version') {
|
|
158
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
159
|
+
res.end(JSON.stringify({ version: dashboardVersion }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
157
162
|
if (req.url === '/webhooks/github' && req.method === 'POST') {
|
|
158
163
|
const chunks = [];
|
|
159
164
|
for await (const chunk of req)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { chooseWithDecision } from './decision.js';
|
|
2
|
+
import { makeDecisionAudit } from './decision-audit.js';
|
|
3
|
+
import { discoverModelCandidates } from './model-routing.js';
|
|
4
|
+
import { loadModelRouterRules, loadRoutingPolicy, loadWorkerRouterRules, policyAllows } from './routing-policy.js';
|
|
5
|
+
import { DEFAULT_CHATGPT_PREFERRED_CONCURRENCY, eligibleWorkerCandidates } from './worker-routing.js';
|
|
6
|
+
function sameModelRoute(candidate, route) {
|
|
7
|
+
return (!route.provider || candidate.provider === route.provider)
|
|
8
|
+
&& (!route.model || candidate.model === route.model)
|
|
9
|
+
&& (route.profile === undefined || candidate.profile === route.profile);
|
|
10
|
+
}
|
|
11
|
+
function modelUsable(candidate, constraints, excludedRoutes, policy) {
|
|
12
|
+
if (candidate.resourceHealthy === false)
|
|
13
|
+
return false;
|
|
14
|
+
if ((excludedRoutes ?? []).some(route => sameModelRoute(candidate, route)))
|
|
15
|
+
return false;
|
|
16
|
+
if (!policyAllows(candidate, policy.policy))
|
|
17
|
+
return false;
|
|
18
|
+
if (constraints?.provider && candidate.provider !== constraints.provider)
|
|
19
|
+
return false;
|
|
20
|
+
if (constraints?.model && candidate.model !== constraints.model)
|
|
21
|
+
return false;
|
|
22
|
+
if (constraints?.profile && candidate.profile !== constraints.profile)
|
|
23
|
+
return false;
|
|
24
|
+
if (constraints?.reasoning && candidate.reasoning && candidate.reasoning !== constraints.reasoning)
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function routeId(worker, model) {
|
|
29
|
+
return `${worker.workerId}::${model.id}`;
|
|
30
|
+
}
|
|
31
|
+
function fallbackRoute(routes) {
|
|
32
|
+
const codex = routes.filter(route => route.worker.adapter === 'codex');
|
|
33
|
+
return codex.find(route => route.model.id.startsWith('default:')) ?? codex[0] ?? routes[0];
|
|
34
|
+
}
|
|
35
|
+
function workerSelection(worker, selectedBy, evidence) {
|
|
36
|
+
return {
|
|
37
|
+
workerId: worker.workerId,
|
|
38
|
+
adapter: worker.adapter,
|
|
39
|
+
capabilities: [...worker.capabilities],
|
|
40
|
+
selectedBy,
|
|
41
|
+
selectedAt: new Date().toISOString(),
|
|
42
|
+
resourceHealthy: worker.resourceHealthy,
|
|
43
|
+
resourceProvider: worker.resourceProvider,
|
|
44
|
+
resourceUsedPercent: worker.resourceUsedPercent,
|
|
45
|
+
resourceRemainingPercent: worker.resourceRemainingPercent,
|
|
46
|
+
resourceResetAt: worker.resourceResetAt,
|
|
47
|
+
resourceResetInMinutes: worker.resourceResetInMinutes,
|
|
48
|
+
resourceWindows: worker.resourceWindows,
|
|
49
|
+
browserWorkspaceCapacity: worker.browserWorkspaceCapacity,
|
|
50
|
+
decisionEvidence: evidence,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function discoverExecutionRoutes(workers, requirements, options = {}) {
|
|
54
|
+
const legalWorkers = eligibleWorkerCandidates(workers, requirements);
|
|
55
|
+
const registered = options.registered ?? [];
|
|
56
|
+
const policy = await loadRoutingPolicy(options.policyPath, options.modelRulesPath);
|
|
57
|
+
const routes = [];
|
|
58
|
+
for (const worker of legalWorkers) {
|
|
59
|
+
const models = await discoverModelCandidates(worker, registered, options.usage, undefined, options.modelRulesPath);
|
|
60
|
+
for (const model of models) {
|
|
61
|
+
if (!modelUsable(model, options.routingConstraints, options.excludedRoutes, policy))
|
|
62
|
+
continue;
|
|
63
|
+
routes.push({ id: routeId(worker, model), worker, model });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { routes, policy };
|
|
67
|
+
}
|
|
68
|
+
export async function routeExecution(workers, requirements, options = {}) {
|
|
69
|
+
const { routes, policy } = await discoverExecutionRoutes(workers, requirements, options);
|
|
70
|
+
if (routes.length === 0)
|
|
71
|
+
throw new Error('No executable worker+model route satisfies authoritative constraints');
|
|
72
|
+
const fallback = fallbackRoute(routes);
|
|
73
|
+
const [workerRules, modelRules] = await Promise.all([
|
|
74
|
+
loadWorkerRouterRules(options.workerRulesPath),
|
|
75
|
+
loadModelRouterRules(options.modelRulesPath),
|
|
76
|
+
]);
|
|
77
|
+
const environmentRules = [workerRules.text, modelRules.text].filter(Boolean).join('\n');
|
|
78
|
+
const preferredChatgptConcurrency = options.preferredChatgptConcurrency ?? DEFAULT_CHATGPT_PREFERRED_CONCURRENCY;
|
|
79
|
+
const chatgptRunning = options.chatgptRunning ?? 0;
|
|
80
|
+
const facts = JSON.parse(JSON.stringify({
|
|
81
|
+
task: options.taskFacts ?? {},
|
|
82
|
+
worker_requirements: requirements,
|
|
83
|
+
explicit_routing_constraints: options.routingConstraints ?? {},
|
|
84
|
+
environment_rules: environmentRules,
|
|
85
|
+
structured_policy: policy.snapshot,
|
|
86
|
+
usage_snapshot: options.usage ?? null,
|
|
87
|
+
runtime_load: {
|
|
88
|
+
chatgpt_running: chatgptRunning,
|
|
89
|
+
preferred_chatgpt_concurrency: preferredChatgptConcurrency,
|
|
90
|
+
},
|
|
91
|
+
routing_principles: [
|
|
92
|
+
'Choose one complete executable route: worker plus provider/model/profile/reasoning.',
|
|
93
|
+
'Only authoritative impossibility constraints are hard gates; all other task fit, capability, trust, region, labels, quota, cost, latency, load, reviewer diversity, and quality signals are soft facts.',
|
|
94
|
+
'User free-text environment rules are soft routing guidance and should be followed when they express a clear preference.',
|
|
95
|
+
'Codex is only the deterministic fallback when the decision engine fails; Codex is not a normal preference when decide() succeeds.',
|
|
96
|
+
],
|
|
97
|
+
candidates: routes.map(route => ({
|
|
98
|
+
id: route.id,
|
|
99
|
+
worker: {
|
|
100
|
+
id: route.worker.workerId,
|
|
101
|
+
adapter: route.worker.adapter,
|
|
102
|
+
runtime: route.worker.runtime ?? null,
|
|
103
|
+
availability: route.worker.availability ?? null,
|
|
104
|
+
capabilities: [...route.worker.capabilities],
|
|
105
|
+
trustDomains: route.worker.trustDomains ?? [],
|
|
106
|
+
region: route.worker.region ?? null,
|
|
107
|
+
labels: route.worker.labels ?? {},
|
|
108
|
+
browserWorkspaceCapacity: route.worker.browserWorkspaceCapacity ?? null,
|
|
109
|
+
},
|
|
110
|
+
model: {
|
|
111
|
+
provider: route.model.provider,
|
|
112
|
+
model: route.model.model,
|
|
113
|
+
profile: route.model.profile ?? null,
|
|
114
|
+
reasoning: route.model.reasoning ?? null,
|
|
115
|
+
resourceHealthy: route.model.resourceHealthy ?? null,
|
|
116
|
+
resourceUsedPercent: route.model.resourceUsedPercent ?? null,
|
|
117
|
+
resourceRemainingPercent: route.model.resourceRemainingPercent ?? null,
|
|
118
|
+
resourceResetAt: route.model.resourceResetAt ?? null,
|
|
119
|
+
resourceResetInMinutes: route.model.resourceResetInMinutes ?? null,
|
|
120
|
+
resourceWindows: route.model.resourceWindows ?? null,
|
|
121
|
+
},
|
|
122
|
+
})),
|
|
123
|
+
}));
|
|
124
|
+
const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', { facts }, {
|
|
125
|
+
type: 'choice',
|
|
126
|
+
question: 'Choose the best complete execution route for this task. The listed candidates are already executable. Treat all non-impossibility signals as soft facts, follow free-text user guidance, and choose worker+model together in this one decision.',
|
|
127
|
+
choices: routes.map(route => route.id),
|
|
128
|
+
}, () => fallback.id);
|
|
129
|
+
const selected = routes.find(route => route.id === choice.choice) ?? fallback;
|
|
130
|
+
const selectedBy = choice.evidence.engine && !choice.evidence.fallback ? 'execution-router:laya' : 'execution-router:fallback';
|
|
131
|
+
const routing = {
|
|
132
|
+
provider: selected.model.provider,
|
|
133
|
+
model: selected.model.model,
|
|
134
|
+
profile: selected.model.profile,
|
|
135
|
+
reasoning: options.routingConstraints?.reasoning ?? selected.model.reasoning,
|
|
136
|
+
decidedBy: selectedBy,
|
|
137
|
+
decidedAt: new Date().toISOString(),
|
|
138
|
+
decisionEvidence: choice.evidence,
|
|
139
|
+
};
|
|
140
|
+
const audit = makeDecisionAudit({
|
|
141
|
+
task: options.auditTask,
|
|
142
|
+
worker: { id: selected.worker.workerId, adapter: selected.worker.adapter, capabilities: [...selected.worker.capabilities], route_id: selected.id },
|
|
143
|
+
contract: {
|
|
144
|
+
question: 'execution-route-selection',
|
|
145
|
+
task: options.taskFacts ?? {},
|
|
146
|
+
workerRequirements: requirements,
|
|
147
|
+
constraints: options.routingConstraints ?? {},
|
|
148
|
+
excludedRoutes: options.excludedRoutes ?? [],
|
|
149
|
+
candidateRoutes: routes.map(route => ({ id: route.id, workerId: route.worker.workerId, adapter: route.worker.adapter })),
|
|
150
|
+
},
|
|
151
|
+
policy,
|
|
152
|
+
candidates: routes.map(route => ({ ...route.model, id: route.id })),
|
|
153
|
+
eligibleCandidates: routes.map(route => ({ ...route.model, id: route.id })),
|
|
154
|
+
selected: { ...selected.model, id: selected.id },
|
|
155
|
+
routing,
|
|
156
|
+
});
|
|
157
|
+
routing.decisionId = audit.decision_id;
|
|
158
|
+
if (options.auditStore)
|
|
159
|
+
await options.auditStore.append(audit);
|
|
160
|
+
return {
|
|
161
|
+
candidate: { ...selected.worker, routing },
|
|
162
|
+
selection: workerSelection(selected.worker, selectedBy, choice.evidence),
|
|
163
|
+
model: selected.model,
|
|
164
|
+
audit,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -2,8 +2,8 @@ import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
|
2
2
|
import { completionGateSatisfied, schedule, transitionTaskEvent, transitionJobEvent, tryTransitionTaskEvent } from './scheduler.js';
|
|
3
3
|
import { implementationComplete, implementationTasks } from './runtime-status.js';
|
|
4
4
|
import { eventFor } from './events.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { workerCandidates } from './worker-routing.js';
|
|
6
|
+
import { routeExecution } from './execution-routing.js';
|
|
7
7
|
import { ReconcilerRuntimeBase } from './reconciler-runtime.js';
|
|
8
8
|
export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
9
9
|
async currentHeadSha(job) { return await this.store.headSha?.() ?? job.headSha ?? job.review?.headSha ?? null; }
|
|
@@ -84,23 +84,18 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
const runtimeSummary = this.options.runtimeSummaryProvider?.();
|
|
87
|
-
const
|
|
87
|
+
const routed = await routeExecution(workers, configured?.requirements ?? { capabilities: ['model'] }, {
|
|
88
88
|
decisionEngine: this.options.decisionEngine,
|
|
89
89
|
decisionMode: this.options.decisionMode,
|
|
90
90
|
taskFacts,
|
|
91
91
|
usage,
|
|
92
|
+
registered,
|
|
92
93
|
chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
|
|
93
|
-
|
|
94
|
-
const models = await discoverModelCandidates(routedWorker.candidate, registered, usage);
|
|
95
|
-
const routedModel = await routeModel(routedWorker.candidate, models, {
|
|
96
|
-
decisionEngine: this.options.decisionEngine,
|
|
97
|
-
decisionMode: this.options.decisionMode,
|
|
98
|
-
constraints: configured?.routing,
|
|
99
|
-
taskFacts,
|
|
94
|
+
routingConstraints: configured?.routing,
|
|
100
95
|
});
|
|
101
96
|
return {
|
|
102
|
-
candidate:
|
|
103
|
-
selection:
|
|
97
|
+
candidate: routed.candidate,
|
|
98
|
+
selection: routed.selection,
|
|
104
99
|
reason: 'worker-router',
|
|
105
100
|
};
|
|
106
101
|
}
|
|
@@ -120,28 +115,31 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
120
115
|
attachments: task.attachments ?? [],
|
|
121
116
|
output: task.output ?? null,
|
|
122
117
|
};
|
|
123
|
-
const routedWorker = await routeWorker(candidates, requirements, {
|
|
124
|
-
decisionEngine: this.options.decisionEngine,
|
|
125
|
-
decisionMode: this.options.decisionMode,
|
|
126
|
-
taskFacts,
|
|
127
|
-
usage,
|
|
128
|
-
chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
|
|
129
|
-
});
|
|
130
|
-
const models = await discoverModelCandidates(routedWorker.candidate, registered, usage);
|
|
131
118
|
const routingConstraints = task.routing ? { provider: task.routing.provider, model: task.routing.model, profile: task.routing.profile, reasoning: task.routing.reasoning } : undefined;
|
|
132
119
|
const parent = task.agentName === 'troubleshooter' && task.parentTaskId ? job.tasks.find(item => item.id === task.parentTaskId) : undefined;
|
|
133
120
|
const parentRouteFailedBeforeStart = Boolean(parent?.routing && /exited before publishing task\.started|worker_exit_before_start|launch failed/i.test(parent.error ?? parent.failureReason ?? ''));
|
|
134
121
|
const excludedRoutes = parentRouteFailedBeforeStart && parent?.routing
|
|
135
122
|
? [{ provider: parent.routing.provider, model: parent.routing.model, profile: parent.routing.profile }]
|
|
136
123
|
: [];
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
124
|
+
const routed = await routeExecution(candidates, requirements, {
|
|
125
|
+
decisionEngine: this.options.decisionEngine,
|
|
126
|
+
decisionMode: this.options.decisionMode,
|
|
127
|
+
taskFacts: { ...taskFacts, recovery_parent_failed_route: excludedRoutes[0] ?? null },
|
|
128
|
+
usage,
|
|
129
|
+
registered,
|
|
130
|
+
chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
|
|
131
|
+
routingConstraints,
|
|
132
|
+
excludedRoutes,
|
|
133
|
+
auditStore: this.options.decisionAuditStore,
|
|
134
|
+
auditTask: task,
|
|
135
|
+
});
|
|
136
|
+
task.workerSelection = routed.selection;
|
|
137
|
+
task.adapter = routed.candidate.adapter;
|
|
138
|
+
task.routing = routed.candidate.routing;
|
|
141
139
|
task.timeoutMs = task.timeoutMs || taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
|
|
142
140
|
task.updatedAt = new Date().toISOString();
|
|
143
141
|
await this.store.saveTask(task);
|
|
144
|
-
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'worker.selected', 'queued', `Worker ${
|
|
142
|
+
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'worker.selected', 'queued', `Worker ${routed.selection.workerId} selected for task ${task.id}`, 'user', { workerSelection: routed.selection, workerRequirements: requirements }, { agent: 'relay', component: 'agents-relay' }));
|
|
145
143
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'model.selected', 'queued', `Selected ${task.routing.model} · thinking ${task.routing.reasoning ?? 'unknown'}`, 'user', { routing: task.routing }, { agent: 'relay', component: 'agents-relay' }));
|
|
146
144
|
}
|
|
147
145
|
}
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@ Every model-backed task producer normally declares:
|
|
|
28
28
|
- `task_pr`: the managed task/PR is the authoritative result; or
|
|
29
29
|
- `file`: an exact file path is the authoritative result.
|
|
30
30
|
|
|
31
|
-
Relay then runs
|
|
31
|
+
Relay then runs one Execution Router decision over complete worker+model routes and persists the selected worker/adapter and model routing before launch. Explicit adapter/provider/model values are compatibility/debug constraints, not normal producer requirements.
|
|
32
32
|
|
|
33
33
|
## Lifecycle contract
|
|
34
34
|
|
|
@@ -105,7 +105,7 @@ Each successful planner invocation is one real `plan-N` planning round. Relay pe
|
|
|
105
105
|
|
|
106
106
|
A zero-task autonomous Job remains idle. After a direct orchestrator-created initial batch completes, Relay may start the first real planner round. Fixed Jobs do not automatically create planner rounds.
|
|
107
107
|
|
|
108
|
-
Planner-created child Tasks remain descriptive agent work and must include
|
|
108
|
+
Planner-created child Tasks remain descriptive agent work and must include the output contract and `storySize`; worker requirements are optional soft routing facts unless an explicit runtime constraint is known. Execution Router chooses the complete runtime route afterward.
|
|
109
109
|
|
|
110
110
|
## Durable truth
|
|
111
111
|
|
|
@@ -56,12 +56,12 @@ Every new model-backed executable task follows this order:
|
|
|
56
56
|
|
|
57
57
|
1. The planner describes the executable Task precisely. It does not invent a capability matrix for Codex versus ChatGPT. workerRequirements is optional compatibility metadata; an explicit runtime may be used only when the caller already knows the Task must use that runtime.
|
|
58
58
|
2. worker-router gathers factual Task context, discovered worker availability, the current shared usage snapshot, built-in routing principles, and optional local free-text rules. Only authoritative legality facts such as an explicitly offline worker or an explicit caller-owned runtime constraint are hard gates. Descriptive capability labels, region/trust labels, and provider quota are decision facts rather than ordinary worker hard gates.
|
|
59
|
-
3.
|
|
60
|
-
4.
|
|
61
|
-
5. decide()
|
|
62
|
-
6. Relay persists the selected worker and model routing on the durable task before launch.
|
|
59
|
+
3. Execution Router builds complete executable routes that pair a discovered worker with one configured/authorized model profile. Only authoritative impossibility facts are hard gates: explicit runtime/routing constraints, offline/unavailable execution surfaces, zero allocatable Browser Workspace capacity, unconfigured/unauthorized models, or provider/profile state known to make execution impossible.
|
|
60
|
+
4. Capabilities, task kind, trust/region/labels, quota pressure, cost, latency, runtime load, reviewer diversity, and free-text environment guidance are soft decision facts unless the caller has explicitly promoted one to a hard constraint. The full workerRequirements object must be passed into the decision facts.
|
|
61
|
+
5. When more than one executable route exists, Execution Router calls the generic typed decide(request, contract) primitive exactly once to choose worker+provider/model/profile/reasoning together. If the decision engine is unavailable, invalid, or fails, Relay deterministically falls back to a usable Codex route, preferring Codex's configured default profile. Codex fallback is not a normal preference when decide() succeeds.
|
|
62
|
+
6. decide() owns no discovery facts or legality policy. Its optional Laya implementation is a transient CLI subprocess per decision; src/usage.ts remains the shared resource-fact source. Relay persists the selected worker and model routing on the durable task before launch.
|
|
63
63
|
|
|
64
|
-
Planner output must not hard-code codex/chatgpt, provider preference, inferred worker capabilities, or usage probes.
|
|
64
|
+
Planner output must not hard-code codex/chatgpt, provider preference, inferred worker capabilities, or usage probes. Execution Router owns runtime route selection; legacy task records that already contain adapter/routing/worker-requirement metadata remain readable.
|
|
65
65
|
|
|
66
66
|
## Story size
|
|
67
67
|
|