agents-relay 1.0.106 → 1.0.107
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 +29 -3
- package/dist/reconciler-review.js +2 -0
- package/dist/relayd.js +3 -2
- package/dist/worker-routing.js +12 -3
- package/package.json +1 -1
package/dist/dashboard-html.js
CHANGED
|
@@ -37,6 +37,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
37
37
|
.quick-filter[aria-pressed="true"] { border-color:var(--accent); background:var(--accent2); color:var(--accent); }
|
|
38
38
|
.refresh-button { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.28rem .55rem; font:inherit; font-size:.72rem; font-weight:700; cursor:pointer; }
|
|
39
39
|
.refresh-button:disabled { cursor:default; opacity:.55; }
|
|
40
|
+
.mobile-only { display:none; }
|
|
40
41
|
.panel-body { padding:.75rem; overflow:auto; max-height:calc(100vh - 11.5rem); }
|
|
41
42
|
#jobs-workspace > .panel:first-child .panel-body { overflow:visible; max-height:none; }
|
|
42
43
|
.jobs-panel { grid-row:1/span 2; }
|
|
@@ -109,7 +110,26 @@ export const dashboardHtml = `<!doctype html>
|
|
|
109
110
|
.skeleton { height:3.2rem; border-radius:.4rem; background:linear-gradient(90deg,#edf0f4,#fafbfc,#edf0f4); animation:pulse 1.2s infinite; }
|
|
110
111
|
@keyframes pulse { 50% { opacity:.55; } }
|
|
111
112
|
@media (max-width:1050px) { main { grid-template-columns:1fr; } .jobs-panel { grid-row:auto; } .panel-body { max-height:24rem; } #jobs-workspace > .panel:first-child .panel-body { max-height:none; } }
|
|
112
|
-
@media (max-width:650px) {
|
|
113
|
+
@media (max-width:650px) {
|
|
114
|
+
header { padding:.65rem .7rem; }
|
|
115
|
+
nav[role="tablist"] { overflow-x:auto; padding-bottom:.1rem; }
|
|
116
|
+
nav button { flex:0 0 auto; white-space:nowrap; }
|
|
117
|
+
main { padding:.5rem; gap:.65rem; }
|
|
118
|
+
body[data-mobile-jobs-view="list"] #jobs-workspace { display:none; }
|
|
119
|
+
body[data-mobile-jobs-view="detail"] #jobs-view { display:none; }
|
|
120
|
+
.mobile-only { display:inline-flex; }
|
|
121
|
+
#jobs-view .panel-head { display:grid; align-items:start; }
|
|
122
|
+
#jobs-view .panel-actions { width:100%; flex-wrap:wrap; }
|
|
123
|
+
#jobs-view .job-filters { width:100%; }
|
|
124
|
+
#jobs-view .job-search { width:100%; max-width:none; flex:1 1 100%; }
|
|
125
|
+
#jobs-view .panel-body { max-height:none; }
|
|
126
|
+
#jobs-workspace { gap:.65rem; }
|
|
127
|
+
#jobs-workspace .panel-body { max-height:none; }
|
|
128
|
+
.job-summary,.detail-groups { grid-template-columns:1fr; }
|
|
129
|
+
.panel-head { padding:.55rem .6rem; }
|
|
130
|
+
.panel-body { padding:.6rem; }
|
|
131
|
+
.tagline { display:none; }
|
|
132
|
+
}
|
|
113
133
|
@media (prefers-reduced-motion:reduce) { .skeleton { animation:none; } .usage-fill { transition:none; } }
|
|
114
134
|
</style>
|
|
115
135
|
</head>
|
|
@@ -132,7 +152,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
132
152
|
</section>
|
|
133
153
|
<div class="workspace" id="jobs-workspace">
|
|
134
154
|
<section class="panel" aria-labelledby="workspace-heading">
|
|
135
|
-
<div class="panel-head"><h2 id="workspace-heading"><span id="selected-job-title" class="subtle"></span></h2><div class="panel-actions"><button id="selected-job-refresh" class="refresh-button" type="button">Refresh</button><span id="connection" class="status-pill neutral" aria-live="polite" aria-label="Connecting"></span></div></div>
|
|
155
|
+
<div class="panel-head"><button id="mobile-jobs-back" class="refresh-button mobile-only" type="button">← Jobs</button><h2 id="workspace-heading"><span id="selected-job-title" class="subtle"></span></h2><div class="panel-actions"><button id="selected-job-refresh" class="refresh-button" type="button">Refresh</button><span id="connection" class="status-pill neutral" aria-live="polite" aria-label="Connecting"></span></div></div>
|
|
136
156
|
<div class="panel-body">
|
|
137
157
|
<div id="job-detail"><div class="skeleton"></div></div>
|
|
138
158
|
<h3 class="section-title">Task hierarchy</h3>
|
|
@@ -209,6 +229,8 @@ const quickStatus=document.querySelector('#quick-status');
|
|
|
209
229
|
const quickAutonomous=document.querySelector('#quick-autonomous');
|
|
210
230
|
const quickDraft=document.querySelector('#quick-draft');
|
|
211
231
|
const selectedJobRefresh=document.querySelector('#selected-job-refresh');
|
|
232
|
+
const mobileJobsBack=document.querySelector('#mobile-jobs-back');
|
|
233
|
+
const mobileJobs=window.matchMedia('(max-width:650px)');
|
|
212
234
|
const jobDetail=document.querySelector('#job-detail');
|
|
213
235
|
const taskTree=document.querySelector('#task-tree');
|
|
214
236
|
const taskDetailRoot=document.querySelector('#task-detail');
|
|
@@ -245,6 +267,7 @@ function element(name,className,child){const node=document.createElement(name);i
|
|
|
245
267
|
function field(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),element('p','detail-value',text(input)));return box;}
|
|
246
268
|
function markdownField(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),markdownValue(input));return box;}
|
|
247
269
|
function rememberSelection(){if(selectedJob)sessionStorage.setItem('agents-relay:selected-job',selectedJob);else sessionStorage.removeItem('agents-relay:selected-job');if(selectedTaskId)sessionStorage.setItem('agents-relay:selected-task',selectedTaskId);else sessionStorage.removeItem('agents-relay:selected-task');}
|
|
270
|
+
function syncMobileJobsView(){const detail=mobileJobs.matches&&Boolean(selectedJob)&&/^#\/jobs\/.+/.test(location.hash);document.body.dataset.mobileJobsView=detail?'detail':'list';}
|
|
248
271
|
function selected(){return jobs.find(job=>job.id===selectedJob)||jobs[0]||null;}
|
|
249
272
|
function counts(tasks){const out={};for(const task of Array.isArray(tasks)?tasks:[])out[task.state]=(out[task.state]||0)+1;return out;}
|
|
250
273
|
function compactCounts(input){const order=['RUNNING','BLOCKED','FAILED','QUEUED','DRAFT','SUCCEEDED','CANCELLED'];return order.filter(state=>input[state]).map(state=>input[state]+' '+state).join(', ')||'No tasks';}
|
|
@@ -294,7 +317,7 @@ async function runTaskAction(task,action,button){const job=selected();if(!job)re
|
|
|
294
317
|
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const actions=element('div','task-actions');actions.append(taskActionButton(task,'retry','Restart task','↻'),taskActionButton(task,'cancel','Cancel task','×'));const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Task',task.id),field('Kind',task.kind||'work'),field('State',task.state),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),taskCommentField(task),chatField(task));const graph=agentGraphField(task);if(graph)groups.append(graph);if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));taskDetailRoot.append(actions,groups);}
|
|
295
318
|
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}
|
|
296
319
|
function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),field('Lifecycle state',job.state),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Review',reviewStatus(job)),field('Review head SHA',job.review?.headSha||job.headSha||'—'),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}
|
|
297
|
-
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();}
|
|
320
|
+
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
|
298
321
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
299
322
|
function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
|
|
300
323
|
function jobGroup(job){if(isTerminalJob(job))return 3;if(job.executionMode==='autonomous')return 1;if(job.draft||job.state==='DRAFT')return 2;return 0;}
|
|
@@ -334,6 +357,9 @@ quickAutonomous.addEventListener('click',()=>toggleQuickMode('autonomous'));
|
|
|
334
357
|
quickDraft.addEventListener('click',()=>toggleQuickMode('draft'));
|
|
335
358
|
quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
|
|
336
359
|
selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
|
|
360
|
+
mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJobsView();window.scrollTo({top:0,behavior:'instant'});});
|
|
361
|
+
mobileJobs.addEventListener('change',syncMobileJobsView);
|
|
362
|
+
window.addEventListener('hashchange',syncMobileJobsView);
|
|
337
363
|
usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
|
|
338
364
|
rulesTab.addEventListener('click',()=>{location.hash='#/model-router-rules';showView('rules');});
|
|
339
365
|
workerRulesTab.addEventListener('click',()=>{location.hash='#/worker-router-rules';showView('worker-rules');});
|
|
@@ -108,6 +108,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
108
108
|
const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
|
|
109
109
|
const registered = await this.store.listAgents();
|
|
110
110
|
const candidates = workerCandidates(this.options.adapters, registered, usage);
|
|
111
|
+
const runtimeSummary = this.options.runtimeSummaryProvider?.();
|
|
111
112
|
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
113
|
const requirements = task.workerRequirements ?? { capabilities: [] };
|
|
113
114
|
const taskFacts = {
|
|
@@ -124,6 +125,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
124
125
|
decisionMode: this.options.decisionMode,
|
|
125
126
|
taskFacts,
|
|
126
127
|
usage,
|
|
128
|
+
chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
|
|
127
129
|
});
|
|
128
130
|
const models = await discoverModelCandidates(routedWorker.candidate, registered, usage);
|
|
129
131
|
const routingConstraints = task.routing ? { provider: task.routing.provider, model: task.routing.model, profile: task.routing.profile, reasoning: task.routing.reasoning } : undefined;
|
package/dist/relayd.js
CHANGED
|
@@ -17,6 +17,7 @@ import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProj
|
|
|
17
17
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
18
18
|
import { decisionMode, LayaCliDecisionEngine } from './decision.js';
|
|
19
19
|
import { JsonlDecisionAuditStore } from './decision-audit.js';
|
|
20
|
+
import { summarizeRuntime } from './runtime-summary.js';
|
|
20
21
|
export const daemonHelp = `Agents Relay daemon runs the dashboard and worker pool.
|
|
21
22
|
|
|
22
23
|
Usage:
|
|
@@ -74,13 +75,13 @@ export async function runDaemon(argv) {
|
|
|
74
75
|
const explicitProjectPath = value(argv, '--project-path');
|
|
75
76
|
const usageRegistry = codexAndZaiUsageRegistry();
|
|
76
77
|
const decisionAuditStore = new JsonlDecisionAuditStore(process.env.AGENTS_RELAY_DECISION_AUDIT_PATH ?? join(homedir(), '.agents-relay', 'decision-audit.jsonl'));
|
|
77
|
-
|
|
78
|
+
let cachedOverviews = [];
|
|
79
|
+
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: new LayaCliDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, reviewer: runtimeReviewer(), projectPathForJob: job => repositoryPaths.get(job.repository) ?? resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot) });
|
|
78
80
|
if (repositories.length === 0)
|
|
79
81
|
throw new Error(`No Git repositories with GitHub origins found under ${normalized.workspaceRoot ?? defaultWorkspaceRoot()}`);
|
|
80
82
|
const pool = new RepositoryWorkerPool(client, repositories, trusted, concurrency, makeReconciler);
|
|
81
83
|
const cacheDirectory = join(homedir(), '.agents-relay');
|
|
82
84
|
const cacheFile = join(cacheDirectory, 'dashboard-jobs.json');
|
|
83
|
-
let cachedOverviews = [];
|
|
84
85
|
try {
|
|
85
86
|
const parsed = JSON.parse(await readFile(cacheFile, 'utf8'));
|
|
86
87
|
if (Array.isArray(parsed))
|
package/dist/worker-routing.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { chooseWithDecision } from './decision.js';
|
|
2
2
|
import { loadWorkerRouterRules } from './routing-policy.js';
|
|
3
|
+
export const DEFAULT_CHATGPT_PREFERRED_CONCURRENCY = 10;
|
|
3
4
|
export const DEFAULT_WORKER_ROUTER_PRINCIPLES = [
|
|
4
5
|
'Worker Router chooses only the execution worker. Model Router chooses provider/model/profile after worker selection.',
|
|
5
6
|
'Prefer ChatGPT when both workers can execute the task because it does not consume Codex coding-plan tokens and normally has access to a stronger ChatGPT Sol model.',
|
|
6
7
|
'Prefer Codex when the task requires native/local execution or computer-use abilities that are unavailable through ChatGPT plus MacBridge.',
|
|
7
8
|
'Treat coding-plan/provider quota as decision context, not worker legality. Codex may still use another provider selected later by Model Router.',
|
|
8
9
|
'When Codex/OpenAI coding quota is scarce, prefer ChatGPT unless the Codex execution environment is necessary.',
|
|
10
|
+
'When current ChatGPT running load reaches the preferred ChatGPT concurrency, strongly prefer Codex unless ChatGPT is materially required or better suited for the task.',
|
|
9
11
|
].join('\n');
|
|
10
12
|
export function usageForProvider(snapshot, provider, now = Date.now()) {
|
|
11
13
|
const usage = snapshot?.providers.find(item => item.id === provider)?.usage;
|
|
@@ -84,9 +86,10 @@ export function satisfiesWorker(candidate, requirements) {
|
|
|
84
86
|
export function eligibleWorkerCandidates(candidates, requirements) {
|
|
85
87
|
return candidates.filter(candidate => satisfiesWorker(candidate, requirements));
|
|
86
88
|
}
|
|
87
|
-
function deterministicWorker(eligible) {
|
|
89
|
+
function deterministicWorker(eligible, chatgptRunning = 0, preferredChatgptConcurrency = DEFAULT_CHATGPT_PREFERRED_CONCURRENCY) {
|
|
90
|
+
const chatgptAtPreferenceLimit = chatgptRunning >= preferredChatgptConcurrency;
|
|
88
91
|
return [...eligible].map((candidate, index) => ({ candidate, index })).sort((left, right) => {
|
|
89
|
-
const preference = (item) => item.adapter === 'chatgpt' ? 0 : 1;
|
|
92
|
+
const preference = (item) => chatgptAtPreferenceLimit ? (item.adapter === 'codex' ? 0 : 1) : (item.adapter === 'chatgpt' ? 0 : 1);
|
|
90
93
|
return preference(left.candidate) - preference(right.candidate) || left.index - right.index;
|
|
91
94
|
})[0].candidate;
|
|
92
95
|
}
|
|
@@ -120,7 +123,9 @@ export async function routeWorker(candidates, requirements, options = {}) {
|
|
|
120
123
|
const eligible = eligibleWorkerCandidates(candidates, requirements);
|
|
121
124
|
if (eligible.length === 0)
|
|
122
125
|
throw new Error('No eligible worker satisfies the authoritative worker constraints');
|
|
123
|
-
const
|
|
126
|
+
const preferredChatgptConcurrency = options.preferredChatgptConcurrency ?? DEFAULT_CHATGPT_PREFERRED_CONCURRENCY;
|
|
127
|
+
const chatgptRunning = options.chatgptRunning ?? 0;
|
|
128
|
+
const deterministic = deterministicWorker(eligible, chatgptRunning, preferredChatgptConcurrency);
|
|
124
129
|
if (eligible.length === 1) {
|
|
125
130
|
const evidence = { mode: options.decisionMode ?? 'off', fallback: true };
|
|
126
131
|
return { candidate: deterministic, selection: selection(deterministic, 'worker-router:constraint', evidence) };
|
|
@@ -132,6 +137,10 @@ export async function routeWorker(candidates, requirements, options = {}) {
|
|
|
132
137
|
routing_principles: DEFAULT_WORKER_ROUTER_PRINCIPLES,
|
|
133
138
|
environment_rules: rules.text,
|
|
134
139
|
usage_snapshot: options.usage ?? null,
|
|
140
|
+
runtime_load: {
|
|
141
|
+
chatgpt_running: chatgptRunning,
|
|
142
|
+
preferred_chatgpt_concurrency: preferredChatgptConcurrency,
|
|
143
|
+
},
|
|
135
144
|
candidates: eligible.map(item => ({
|
|
136
145
|
id: item.workerId,
|
|
137
146
|
adapter: item.adapter,
|