agents-relay 1.0.73 → 1.0.75
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 +74 -0
- package/dist/cli.js +34 -2
- package/dist/dashboard.js +27 -5
- package/dist/execution-context.js +25 -0
- package/dist/planner.js +2 -2
- package/dist/reconciler.js +10 -7
- package/dist/relayd.js +10 -1
- package/dist/store.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { agentContractPrompt } from './execution-context.js';
|
|
2
|
+
const ID = '[A-Za-z_][A-Za-z0-9_.-]*';
|
|
3
|
+
const EDGE = new RegExp(`^(${ID})(?:\\[agent:(${ID})\\])?\\s*-->\\s*(${ID})(?:\\[agent:(${ID})\\])?$`);
|
|
4
|
+
const NODE = new RegExp(`^(${ID})\\[agent:(${ID})\\]$`);
|
|
5
|
+
export function parseAgentGraph(source) {
|
|
6
|
+
const lines = source.split(/\r?\n/);
|
|
7
|
+
let header = false;
|
|
8
|
+
const agents = new Map();
|
|
9
|
+
const edges = [];
|
|
10
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
11
|
+
const line = lines[index].trim();
|
|
12
|
+
if (!line || line.startsWith('%%'))
|
|
13
|
+
continue;
|
|
14
|
+
if (!header) {
|
|
15
|
+
if (!/^flowchart\s+(?:TD|TB|LR|RL)$/.test(line))
|
|
16
|
+
throw new Error(`Agent Graph must start with Mermaid flowchart TD/TB/LR/RL (line ${index + 1})`);
|
|
17
|
+
header = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const edge = line.match(EDGE);
|
|
21
|
+
if (edge) {
|
|
22
|
+
const [, from, fromAgent, to, toAgent] = edge;
|
|
23
|
+
agents.set(from, fromAgent || agents.get(from) || from);
|
|
24
|
+
agents.set(to, toAgent || agents.get(to) || to);
|
|
25
|
+
edges.push({ from, to });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const node = line.match(NODE);
|
|
29
|
+
if (node) {
|
|
30
|
+
agents.set(node[1], node[2]);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unsupported Agent Graph syntax at line ${index + 1}: ${line}`);
|
|
34
|
+
}
|
|
35
|
+
if (!header)
|
|
36
|
+
throw new Error('Agent Graph is empty');
|
|
37
|
+
if (agents.size === 0)
|
|
38
|
+
throw new Error('Agent Graph requires at least one agent node');
|
|
39
|
+
const incoming = new Map([...agents.keys()].map(id => [id, 0]));
|
|
40
|
+
const outgoing = new Map([...agents.keys()].map(id => [id, []]));
|
|
41
|
+
for (const edge of edges) {
|
|
42
|
+
if (edge.from === edge.to)
|
|
43
|
+
throw new Error(`Agent Graph self-cycle at ${edge.from}`);
|
|
44
|
+
incoming.set(edge.to, (incoming.get(edge.to) ?? 0) + 1);
|
|
45
|
+
outgoing.get(edge.from)?.push(edge.to);
|
|
46
|
+
}
|
|
47
|
+
const remaining = new Map(incoming);
|
|
48
|
+
const waves = [];
|
|
49
|
+
let ready = [...remaining.entries()].filter(([, degree]) => degree === 0).map(([id]) => id).sort();
|
|
50
|
+
let visited = 0;
|
|
51
|
+
while (ready.length) {
|
|
52
|
+
waves.push(ready);
|
|
53
|
+
visited += ready.length;
|
|
54
|
+
const next = new Set();
|
|
55
|
+
for (const id of ready) {
|
|
56
|
+
for (const target of outgoing.get(id) ?? []) {
|
|
57
|
+
const degree = (remaining.get(target) ?? 0) - 1;
|
|
58
|
+
remaining.set(target, degree);
|
|
59
|
+
if (degree === 0)
|
|
60
|
+
next.add(target);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
ready = [...next].sort();
|
|
64
|
+
}
|
|
65
|
+
if (visited !== agents.size)
|
|
66
|
+
throw new Error('Agent Graph contains a cycle');
|
|
67
|
+
return { source, nodes: [...agents].map(([id, agent]) => ({ id, agent })), edges, waves };
|
|
68
|
+
}
|
|
69
|
+
export async function agentGraphPrompt(context, source) {
|
|
70
|
+
const graph = parseAgentGraph(source);
|
|
71
|
+
const contracts = await Promise.all(graph.nodes.map(async (node) => ({ node, prompt: await agentContractPrompt(context, node.agent) })));
|
|
72
|
+
const incoming = new Map(graph.nodes.map(node => [node.id, graph.edges.filter(edge => edge.to === node.id).map(edge => edge.from)]));
|
|
73
|
+
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')}`;
|
|
74
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -110,7 +110,7 @@ Actions:
|
|
|
110
110
|
'task create': `Create one durable Task under an existing Job.
|
|
111
111
|
|
|
112
112
|
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. Codex/ChatGPT tasks also require --model MODEL.
|
|
113
|
-
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
113
|
+
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --agent NAME, --agent-graph-file PATH|-, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
114
114
|
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
115
115
|
|
|
116
116
|
Example:
|
|
@@ -118,7 +118,7 @@ Example:
|
|
|
118
118
|
'task update': `Update mutable durable Task fields in place.
|
|
119
119
|
|
|
120
120
|
Required: --task-id ID and the normal Job storage options.
|
|
121
|
-
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
121
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --agent-graph-file PATH|-, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
122
122
|
Use --clear-routing to remove routing metadata.
|
|
123
123
|
|
|
124
124
|
Example:
|
|
@@ -352,6 +352,7 @@ async function workerTaskFromArgs(job, args, now) {
|
|
|
352
352
|
const outputKind = arg(args, '--output');
|
|
353
353
|
const outputPath = arg(args, '--output-path');
|
|
354
354
|
const input = await textOption(args, '--input', '--input-file');
|
|
355
|
+
const agentGraph = hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file') ? await textOption(args, '--agent-graph', '--agent-graph-file') : undefined;
|
|
355
356
|
if (!input.trim())
|
|
356
357
|
throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
|
|
357
358
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
|
|
@@ -374,6 +375,7 @@ async function workerTaskFromArgs(job, args, now) {
|
|
|
374
375
|
storySize: size,
|
|
375
376
|
projectName: arg(args, '--project') || undefined,
|
|
376
377
|
agentName: arg(args, '--agent') || undefined,
|
|
378
|
+
agentGraph,
|
|
377
379
|
parentTaskId: arg(args, '--parent') || null,
|
|
378
380
|
dependencies,
|
|
379
381
|
capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
|
|
@@ -474,6 +476,8 @@ async function runTaskCommand(action, args) {
|
|
|
474
476
|
task.projectName = arg(args, '--project') || undefined;
|
|
475
477
|
if (hasArg(args, '--agent'))
|
|
476
478
|
task.agentName = arg(args, '--agent') || undefined;
|
|
479
|
+
if (hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file'))
|
|
480
|
+
task.agentGraph = await textOption(args, '--agent-graph', '--agent-graph-file');
|
|
477
481
|
if (hasArg(args, '--parent'))
|
|
478
482
|
task.parentTaskId = arg(args, '--parent') || null;
|
|
479
483
|
if (hasArg(args, '--deps'))
|
|
@@ -669,6 +673,32 @@ async function runJobCommand(action, args) {
|
|
|
669
673
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
670
674
|
export function runtimeReviewer() { return { requirements: { capabilities: ['model'] } }; }
|
|
671
675
|
export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegistry()) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, usageSnapshotProvider: () => usageRegistry.snapshot(), reviewer: runtimeReviewer(), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: job => resolveManagedProjectPath(job.repository, arg(args, '--project-path'), arg(args, '--workspace-root') || defaultWorkspaceRoot()) }); }
|
|
676
|
+
export function dashboardTaskAction(store, reconciler, action) {
|
|
677
|
+
return async (jobId, taskId) => {
|
|
678
|
+
const job = await store.load(jobId);
|
|
679
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
680
|
+
if (!task)
|
|
681
|
+
throw new Error(`Task ${taskId} not found`);
|
|
682
|
+
if (action === 'retry') {
|
|
683
|
+
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
684
|
+
throw new Error(`Task ${task.id} is not restartable (${task.state})`);
|
|
685
|
+
task.maxAttempts = extendRetryBudget(task);
|
|
686
|
+
await transitionTaskEvent(task, 'retry');
|
|
687
|
+
task.error = null;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
if (!['DRAFT', 'QUEUED', 'BLOCKED', 'FAILED'].includes(task.state))
|
|
691
|
+
throw new Error(`Task ${task.id} is not cancellable (${task.state})`);
|
|
692
|
+
await transitionTaskEvent(task, 'cancel');
|
|
693
|
+
task.error = 'Cancelled by user';
|
|
694
|
+
}
|
|
695
|
+
task.leaseOwner = null;
|
|
696
|
+
task.leaseExpiresAt = null;
|
|
697
|
+
task.executionId = null;
|
|
698
|
+
await store.saveTask(task);
|
|
699
|
+
return await reconciler.wake(job.id);
|
|
700
|
+
};
|
|
701
|
+
}
|
|
672
702
|
export function dashboardJobAction(store, reconciler, action) {
|
|
673
703
|
return async (jobId) => {
|
|
674
704
|
const job = await store.load(jobId);
|
|
@@ -720,6 +750,8 @@ export async function createService(args) {
|
|
|
720
750
|
start: dashboardJobAction(loaded.store, reconciler, 'start'),
|
|
721
751
|
reconcile: dashboardJobAction(loaded.store, reconciler, 'reconcile'),
|
|
722
752
|
cancel: dashboardJobAction(loaded.store, reconciler, 'cancel'),
|
|
753
|
+
taskRetry: dashboardTaskAction(loaded.store, reconciler, 'retry'),
|
|
754
|
+
taskCancel: dashboardTaskAction(loaded.store, reconciler, 'cancel'),
|
|
723
755
|
});
|
|
724
756
|
await once(dashboardServer, 'listening');
|
|
725
757
|
const stopWatcher = await reconciler.watch(loaded.job.id);
|
package/dist/dashboard.js
CHANGED
|
@@ -124,6 +124,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
124
124
|
.task-row { width:100%; display:flex; align-items:center; gap:.4rem; min-width:0; padding:.32rem .38rem; border:0; border-radius:.35rem; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
|
|
125
125
|
.task-row[aria-current="true"] { background:var(--accent2); }
|
|
126
126
|
.task-id { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font: .77rem ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
127
|
+
.task-actions { display:flex; justify-content:flex-end; gap:.25rem; margin-bottom:.5rem; }
|
|
127
128
|
.event-list { display:grid; gap:.45rem; }
|
|
128
129
|
.event-item { padding-bottom:.4rem; border-bottom:1px solid var(--border); }
|
|
129
130
|
.event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
|
|
@@ -158,7 +159,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
158
159
|
</header>
|
|
159
160
|
<main>
|
|
160
161
|
<section id="jobs-view" role="tabpanel" aria-labelledby="jobs-tab" class="panel jobs-panel">
|
|
161
|
-
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><
|
|
162
|
+
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><select id="quick-status" class="job-filter" aria-label="Filter jobs by derived status"><option value="">STATUS</option></select><button id="quick-autonomous" class="quick-filter" type="button" aria-pressed="false">AUTONOMOUS</button><button id="quick-draft" class="quick-filter" type="button" aria-pressed="false">DRAFT</button><select id="repo-filter" class="job-filter" aria-label="Filter jobs by repository"><option value="">All repos</option></select></div><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div></div>
|
|
162
163
|
<div class="panel-body" aria-live="polite"><ul id="job-list" class="job-list"><li class="skeleton" aria-hidden="true"></li></ul></div>
|
|
163
164
|
</section>
|
|
164
165
|
<div class="workspace" id="jobs-workspace">
|
|
@@ -207,7 +208,6 @@ const jobsWorkspace=document.querySelector('#jobs-workspace');
|
|
|
207
208
|
const jobList=document.querySelector('#job-list');
|
|
208
209
|
const jobsRefresh=document.querySelector('#jobs-refresh');
|
|
209
210
|
const repoFilter=document.querySelector('#repo-filter');
|
|
210
|
-
const quickJobs=document.querySelector('#quick-jobs');
|
|
211
211
|
const quickStatus=document.querySelector('#quick-status');
|
|
212
212
|
const quickAutonomous=document.querySelector('#quick-autonomous');
|
|
213
213
|
const quickDraft=document.querySelector('#quick-draft');
|
|
@@ -263,7 +263,6 @@ function syncFilters(){
|
|
|
263
263
|
selectedRuntimeStatus=statuses.includes(previousStatus)?previousStatus:'';
|
|
264
264
|
if(quickMode==='status'&&!selectedRuntimeStatus)quickMode='jobs';
|
|
265
265
|
quickStatus.value=quickMode==='status'?selectedRuntimeStatus:'';
|
|
266
|
-
quickJobs.setAttribute('aria-pressed',String(quickMode==='jobs'));
|
|
267
266
|
quickAutonomous.setAttribute('aria-pressed',String(quickMode==='autonomous'));
|
|
268
267
|
quickDraft.setAttribute('aria-pressed',String(quickMode==='draft'));
|
|
269
268
|
}
|
|
@@ -276,7 +275,10 @@ function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();
|
|
|
276
275
|
function taskNode(task,children,depth){const li=element('li');const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{selectedTaskId=task.id;eventTaskId=task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const id=element('span','task-id',text(task.id));id.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(id,element('span','badge '+value(task.state),text(task.state)));li.append(row);const nested=children.get(task.id)||[];if(nested.length){const ul=element('ul','tree');for(const child of nested)ul.append(taskNode(child,children,depth+1));li.append(ul);}return li;}
|
|
277
276
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const known=new Set(tasks.map(task=>task.id));const children=new Map();const roots=[];for(const task of tasks){if(!task.parentTaskId||!known.has(task.parentTaskId))roots.push(task);else{const siblings=children.get(task.parentTaskId)||[];siblings.push(task);children.set(task.parentTaskId,siblings);}}for(const task of roots)taskTree.append(taskNode(task,children,0));}
|
|
278
277
|
function attemptBudget(task){const attempt=Number(task?.attempt)||0;const max=Number(task?.maxAttempts)||0;if(max>=3&&max%3===0){const effectiveMax=Math.max(max,Math.ceil(attempt/3)*3);return attempt+' / 3×'+(effectiveMax/3);}return attempt+' / '+max;}
|
|
279
|
-
function
|
|
278
|
+
function taskActionEnabled(task,action){if(action==='retry')return ['FAILED','BLOCKED'].includes(task.state);if(action==='cancel')return ['DRAFT','QUEUED','BLOCKED','FAILED'].includes(task.state);return false;}
|
|
279
|
+
function taskActionButton(task,action,label,icon){const button=document.createElement('button');button.type='button';button.className='job-action';button.textContent=icon;button.title=label;button.setAttribute('aria-label',label);button.disabled=!taskActionEnabled(task,action);button.addEventListener('click',()=>void runTaskAction(task,action,button));return button;}
|
|
280
|
+
async function runTaskAction(task,action,button){const job=selected();if(!job)return;button.disabled=true;try{const response=await fetch('/api/task-action?id='+encodeURIComponent(job.id)+'&task='+encodeURIComponent(task.id)+'&action='+encodeURIComponent(action),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>candidate.id===job.id);if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){button.title=(action==='retry'?'Restart':'Cancel')+': '+error.message;}finally{renderAll();}}
|
|
281
|
+
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'),chatField(task));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);}
|
|
280
282
|
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();}
|
|
281
283
|
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();}
|
|
282
284
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();}
|
|
@@ -307,7 +309,6 @@ jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encode
|
|
|
307
309
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
308
310
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
309
311
|
repoFilter.addEventListener('change',()=>{selectedRepo=repoFilter.value;sessionStorage.setItem('agents-relay:repo-filter',selectedRepo);renderJobList();});
|
|
310
|
-
quickJobs.addEventListener('click',()=>setQuickMode('jobs'));
|
|
311
312
|
quickAutonomous.addEventListener('click',()=>setQuickMode('autonomous'));
|
|
312
313
|
quickDraft.addEventListener('click',()=>setQuickMode('draft'));
|
|
313
314
|
quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
|
|
@@ -488,6 +489,27 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
488
489
|
}
|
|
489
490
|
return;
|
|
490
491
|
}
|
|
492
|
+
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/task-action') {
|
|
493
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
494
|
+
const requestedJobId = url.searchParams.get('id') ?? '';
|
|
495
|
+
const requestedTaskId = url.searchParams.get('task') ?? '';
|
|
496
|
+
const action = url.searchParams.get('action') ?? '';
|
|
497
|
+
const handler = action === 'retry' ? actions?.taskRetry : action === 'cancel' ? actions?.taskCancel : undefined;
|
|
498
|
+
if (req.method !== 'POST' || !/^[A-Za-z0-9_-]+$/.test(requestedJobId) || !/^[A-Za-z0-9_-]+$/.test(requestedTaskId) || !handler) {
|
|
499
|
+
res.writeHead(404);
|
|
500
|
+
res.end('Task action unavailable');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
505
|
+
res.end(JSON.stringify(await handler(requestedJobId, requestedTaskId)));
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
res.writeHead(409);
|
|
509
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
491
513
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job') {
|
|
492
514
|
try {
|
|
493
515
|
const requestedJobId = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('id') ?? undefined;
|
|
@@ -224,6 +224,31 @@ export async function resolveExecutionContext(projectPath, role, options = {}) {
|
|
|
224
224
|
delegation: { mode: 'relay-child-task', parentTaskIdRequired: role === 'nested-worker' },
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
|
+
export async function resolveAgentContract(context, agentName) {
|
|
228
|
+
const agent = context.agents.find(candidate => candidate.name === agentName);
|
|
229
|
+
if (!agent)
|
|
230
|
+
throw new Error(`Agent ${agentName} is not visible in the resolved execution context`);
|
|
231
|
+
if (!agent.path)
|
|
232
|
+
throw new Error(`Agent ${agentName} has no readable Markdown contract`);
|
|
233
|
+
const definition = await readFile(agent.path, 'utf8');
|
|
234
|
+
if (!definition.trim())
|
|
235
|
+
throw new Error(`Agent ${agentName} Markdown contract is empty`);
|
|
236
|
+
return { agent, definition };
|
|
237
|
+
}
|
|
238
|
+
export async function agentContractPrompt(context, agentName) {
|
|
239
|
+
const { agent, definition } = await resolveAgentContract(context, agentName);
|
|
240
|
+
return `[Selected task agent contract]
|
|
241
|
+
Agent: ${agent.name}
|
|
242
|
+
Source: ${agent.path ?? agent.source}
|
|
243
|
+
|
|
244
|
+
The following Markdown is the authoritative behavioral contract for this Task agent.
|
|
245
|
+
Follow it in addition to the shared Job/Task and events-bus contracts. Worker/model routing
|
|
246
|
+
does not change the selected agent identity.
|
|
247
|
+
|
|
248
|
+
--- BEGIN AGENT CONTRACT ---
|
|
249
|
+
${definition.trim()}
|
|
250
|
+
--- END AGENT CONTRACT ---`;
|
|
251
|
+
}
|
|
227
252
|
export function executionContextPrompt(context) {
|
|
228
253
|
const instructions = context.instructions.map(item => `- ${item.path} [${item.source}]`).join('\n') || '- none';
|
|
229
254
|
const skills = context.skills.map(item => `- ${item.name}: ${item.path} [${item.source}]`).join('\n') || '- none';
|
package/dist/planner.js
CHANGED
|
@@ -9,7 +9,7 @@ export function planningSpec(task) {
|
|
|
9
9
|
throw new Error(`Initial task ${task.id} requires an output contract`);
|
|
10
10
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(task.adapter))
|
|
11
11
|
throw new Error(`Initial task ${task.id} requires a supported adapter`);
|
|
12
|
-
return { id: task.id, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs };
|
|
12
|
+
return { id: task.id, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs };
|
|
13
13
|
}
|
|
14
14
|
function record(value) {
|
|
15
15
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -126,7 +126,7 @@ export function parsePlannerResult(value) {
|
|
|
126
126
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
127
127
|
return {
|
|
128
128
|
id, input: task.input, priority, storySize,
|
|
129
|
-
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'),
|
|
129
|
+
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph: optionalString(task.agentGraph, 'agentGraph'),
|
|
130
130
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
|
|
131
131
|
adapter: adapter, output: output, routing: route,
|
|
132
132
|
continuation: task.continuation, maxAttempts, timeoutMs
|
package/dist/reconciler.js
CHANGED
|
@@ -5,16 +5,19 @@ import { implementationComplete } from './runtime-status.js';
|
|
|
5
5
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
6
6
|
import { eventFor } from './events.js';
|
|
7
7
|
import { parsePlannerResult, plannerInput } from './planner.js';
|
|
8
|
-
import { executionContextPrompt, resolveExecutionContext } from './execution-context.js';
|
|
8
|
+
import { agentContractPrompt, executionContextPrompt, resolveExecutionContext } from './execution-context.js';
|
|
9
9
|
import { selectReviewer } from './reviewer-routing.js';
|
|
10
|
+
import { agentGraphPrompt } from './agent-graph.js';
|
|
10
11
|
import { selectWorker, usageForProvider, workerCandidates } from './worker-routing.js';
|
|
11
12
|
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
12
13
|
export function compareTaskPriority(a, b, jobPriority = 'P2') { const ar = PRIORITY_RANK[a.priority ?? jobPriority]; const br = PRIORITY_RANK[b.priority ?? jobPriority]; return ar - br || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
|
|
13
14
|
export function executionEventSource(jobId, taskId, executionId) { return `job/${jobId}/task/${taskId}/execution/${executionId}`; }
|
|
14
15
|
function eventSourceId(event) { return typeof event.source?.id === 'string' ? event.source.id : undefined; }
|
|
15
|
-
export function managedWorkerInput(job, task, context) {
|
|
16
|
+
export async function managedWorkerInput(job, task, context) {
|
|
16
17
|
if (!['codex', 'chatgpt'].includes(task.adapter) || !job.repository || job.prNumber <= 0)
|
|
17
18
|
return task.input;
|
|
19
|
+
const selectedAgent = task.agentName && context ? await agentContractPrompt(context, task.agentName) : '';
|
|
20
|
+
const selectedGraph = task.agentGraph && context ? await agentGraphPrompt(context, task.agentGraph) : '';
|
|
18
21
|
if (!task.output)
|
|
19
22
|
throw new Error(`Task ${task.id} requires an explicit output contract`);
|
|
20
23
|
if (!task.executionId)
|
|
@@ -34,7 +37,7 @@ Project: ${task.projectName ?? context?.project.name ?? 'unspecified'}
|
|
|
34
37
|
|
|
35
38
|
Retrieve the durable job and task from GitHub before doing the work. Use whatever GitHub access this environment provides: the URLs, a GitHub integration/tool, or GitHub CLI/API. GitHub is the source of truth for task intent and declared output.
|
|
36
39
|
|
|
37
|
-
${context ? executionContextPrompt(context) + '\n\n' : ''}Follow the resolved project instructions and Agents Relay/events-bus worker contracts.
|
|
40
|
+
${context ? executionContextPrompt(context) + '\n\n' : ''}${selectedAgent ? selectedAgent + '\n\n' : ''}${selectedGraph ? selectedGraph + '\n\n' : ''}Follow the resolved project instructions and Agents Relay/events-bus worker contracts.
|
|
38
41
|
|
|
39
42
|
[Output contract]
|
|
40
43
|
${output}
|
|
@@ -243,7 +246,7 @@ export class Reconciler {
|
|
|
243
246
|
const now = new Date().toISOString();
|
|
244
247
|
const implementationTasks = job.tasks.filter(item => item.agentName !== 'reviewer' && !this.isSupersededTask(item));
|
|
245
248
|
const reviewerRouting = await this.selectReviewer(implementationTasks);
|
|
246
|
-
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: implementationTasks.map(item => item.id), capabilities: ['review'], workerSelection: reviewerRouting.selection, adapter: reviewerRouting.candidate.adapter, input: `Review PR #${job.prNumber} at head SHA ${headSha}.
|
|
249
|
+
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: implementationTasks.map(item => item.id), capabilities: ['review'], workerSelection: reviewerRouting.selection, adapter: reviewerRouting.candidate.adapter, input: `Review existing PR #${job.prNumber} at exactly head SHA ${headSha}. Job objective: ${job.objective ?? job.description ?? job.title}. Use the selected reviewer agent contract as the authoritative review protocol.`, output: { kind: 'task_pr' }, routing: reviewerRouting.candidate.routing, continuation: null, continuationDeliveredAt: null, reviewHeadSha: headSha, state: 'QUEUED', attempt: 0, maxAttempts: this.options.reviewer?.maxAttempts ?? 2, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.reviewer?.timeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now };
|
|
247
250
|
await this.store.appendTask(task);
|
|
248
251
|
job.review = { outcome: 'pending', headSha, taskId: id, evidence: null, updatedAt: now };
|
|
249
252
|
job.updatedAt = now;
|
|
@@ -505,7 +508,7 @@ export class Reconciler {
|
|
|
505
508
|
const controller = new AbortController();
|
|
506
509
|
let execution;
|
|
507
510
|
const sourceId = executionEventSource(job.id, task.id, durableExecutionId);
|
|
508
|
-
const workerInput = managedWorkerInput(job, task, executionContext);
|
|
511
|
+
const workerInput = await managedWorkerInput(job, task, executionContext);
|
|
509
512
|
const workerTask = { ...task, input: workerInput, routing: task.routing ? { ...task.routing, cwd: task.routing.cwd ?? executionContext.project.path } : task.routing };
|
|
510
513
|
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, sourceId, Date.now(), this.options.startAckTimeoutMs ?? chatGptStartAckTimeoutMs(task.timeoutMs)) : null;
|
|
511
514
|
const closePolicy = task.chatgptTabClosePolicy ?? this.options.chatgptTabClosePolicy ?? adapter.tabClosePolicy ?? 'after-start';
|
|
@@ -676,7 +679,7 @@ export class Reconciler {
|
|
|
676
679
|
}
|
|
677
680
|
plannerChild(job, planner, spec) {
|
|
678
681
|
const now = new Date().toISOString();
|
|
679
|
-
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: spec.workerRequirements, adapter: 'codex', input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: (spec.dependencies?.length ?? 0) > 0 ? 'DRAFT' : 'QUEUED', attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M'), createdAt: now, updatedAt: now, planningDecisionId: planner.id, planningSource: planner.planningSource ?? 'model-planner', planningRound: planner.planningRound ?? 1 };
|
|
682
|
+
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, agentGraph: spec.agentGraph, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: spec.workerRequirements, adapter: 'codex', input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: (spec.dependencies?.length ?? 0) > 0 ? 'DRAFT' : 'QUEUED', attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M'), createdAt: now, updatedAt: now, planningDecisionId: planner.id, planningSource: planner.planningSource ?? 'model-planner', planningRound: planner.planningRound ?? 1 };
|
|
680
683
|
}
|
|
681
684
|
clearExecution(taskId, executionId) {
|
|
682
685
|
const live = this.live.get(taskId);
|
|
@@ -956,5 +959,5 @@ export class Reconciler {
|
|
|
956
959
|
} }
|
|
957
960
|
}
|
|
958
961
|
function samePlannerChildDefinition(left, right) {
|
|
959
|
-
return JSON.stringify({ id: left.id, input: left.input, output: left.output, priority: left.priority, storySize: left.storySize ?? 'M', projectName: left.projectName, agentName: left.agentName, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, input: right.input, output: right.output, priority: right.priority, storySize: right.storySize ?? 'M', projectName: right.projectName, agentName: right.agentName, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
|
|
962
|
+
return JSON.stringify({ id: left.id, input: left.input, output: left.output, priority: left.priority, storySize: left.storySize ?? 'M', projectName: left.projectName, agentName: left.agentName, agentGraph: left.agentGraph, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, input: right.input, output: right.output, priority: right.priority, storySize: right.storySize ?? 'M', projectName: right.projectName, agentName: right.agentName, agentGraph: right.agentGraph, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
|
|
960
963
|
}
|
package/dist/relayd.js
CHANGED
|
@@ -11,7 +11,7 @@ import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
|
|
|
11
11
|
import { Reconciler } from './reconciler.js';
|
|
12
12
|
import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestGraphql } from './store.js';
|
|
13
13
|
import { githubAuthContext } from './github-auth.js';
|
|
14
|
-
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
14
|
+
import { dashboardJobAction, dashboardTaskAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
15
15
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
16
|
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath } from './workspace.js';
|
|
17
17
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
@@ -127,6 +127,13 @@ export async function runDaemon(argv) {
|
|
|
127
127
|
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
128
128
|
return await dashboardJobAction(store, makeReconciler(store, concurrency), action)(jobId);
|
|
129
129
|
};
|
|
130
|
+
const workspaceTaskAction = (action) => async (jobId, taskId) => {
|
|
131
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
132
|
+
if (!overview)
|
|
133
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
134
|
+
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
135
|
+
return await dashboardTaskAction(store, makeReconciler(store, concurrency), action)(jobId, taskId);
|
|
136
|
+
};
|
|
130
137
|
const secretFile = value(argv, '--github-webhook-secret-file');
|
|
131
138
|
const secret = value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
|
|
132
139
|
const webhook = secret ? {
|
|
@@ -170,6 +177,8 @@ export async function runDaemon(argv) {
|
|
|
170
177
|
start: workspaceJobAction('start'),
|
|
171
178
|
reconcile: workspaceJobAction('reconcile'),
|
|
172
179
|
cancel: workspaceJobAction('cancel'),
|
|
180
|
+
taskRetry: workspaceTaskAction('retry'),
|
|
181
|
+
taskCancel: workspaceTaskAction('cancel'),
|
|
173
182
|
});
|
|
174
183
|
await once(server, 'listening');
|
|
175
184
|
const reconcileSafely = async () => {
|
package/dist/store.js
CHANGED
|
@@ -85,7 +85,7 @@ function immutableTaskDefinition(task) {
|
|
|
85
85
|
reasoning: task.routing.reasoning, cwd: task.routing.cwd, projectId: task.routing.projectId
|
|
86
86
|
} : undefined;
|
|
87
87
|
return {
|
|
88
|
-
jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName,
|
|
88
|
+
jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph,
|
|
89
89
|
kind: task.kind ?? 'work', planningDecisionId: task.planningDecisionId, planningSource: task.planningSource, planningRound: task.planningRound, parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, workerSelection: task.workerSelection, adapter: task.adapter,
|
|
90
90
|
input: task.input, attachments: task.attachments, output: task.output, routing, chatgptTabClosePolicy: task.chatgptTabClosePolicy ?? 'after-start', continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
|
|
91
91
|
};
|