agents-relay 1.0.73 → 1.0.74

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.
@@ -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'))
package/dist/dashboard.js CHANGED
@@ -158,7 +158,7 @@ export const dashboardHtml = `<!doctype html>
158
158
  </header>
159
159
  <main>
160
160
  <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"><button id="quick-jobs" class="quick-filter" type="button" aria-pressed="true">JOBS</button><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>
161
+ <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
162
  <div class="panel-body" aria-live="polite"><ul id="job-list" class="job-list"><li class="skeleton" aria-hidden="true"></li></ul></div>
163
163
  </section>
164
164
  <div class="workspace" id="jobs-workspace">
@@ -207,7 +207,6 @@ const jobsWorkspace=document.querySelector('#jobs-workspace');
207
207
  const jobList=document.querySelector('#job-list');
208
208
  const jobsRefresh=document.querySelector('#jobs-refresh');
209
209
  const repoFilter=document.querySelector('#repo-filter');
210
- const quickJobs=document.querySelector('#quick-jobs');
211
210
  const quickStatus=document.querySelector('#quick-status');
212
211
  const quickAutonomous=document.querySelector('#quick-autonomous');
213
212
  const quickDraft=document.querySelector('#quick-draft');
@@ -263,7 +262,6 @@ function syncFilters(){
263
262
  selectedRuntimeStatus=statuses.includes(previousStatus)?previousStatus:'';
264
263
  if(quickMode==='status'&&!selectedRuntimeStatus)quickMode='jobs';
265
264
  quickStatus.value=quickMode==='status'?selectedRuntimeStatus:'';
266
- quickJobs.setAttribute('aria-pressed',String(quickMode==='jobs'));
267
265
  quickAutonomous.setAttribute('aria-pressed',String(quickMode==='autonomous'));
268
266
  quickDraft.setAttribute('aria-pressed',String(quickMode==='draft'));
269
267
  }
@@ -307,7 +305,6 @@ jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encode
307
305
  jobsRefresh.addEventListener('click',()=>void refresh(true));
308
306
  eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
309
307
  repoFilter.addEventListener('change',()=>{selectedRepo=repoFilter.value;sessionStorage.setItem('agents-relay:repo-filter',selectedRepo);renderJobList();});
310
- quickJobs.addEventListener('click',()=>setQuickMode('jobs'));
311
308
  quickAutonomous.addEventListener('click',()=>setQuickMode('autonomous'));
312
309
  quickDraft.addEventListener('click',()=>setQuickMode('draft'));
313
310
  quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
@@ -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
@@ -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}. Objective: ${job.objective ?? job.description ?? job.title}. First publish task.started using the caller-provided execution source identity, then verify the deterministic pre-review gates have passed and inspect the completed work. Emit review.started, then exactly one of review.approved, review.changes_required, or review.blocked with data.review.headSha set to ${headSha}. For review.changes_required, include every actionable finding in data.review.issues as an array of concise issue strings; Relay will create one durable fix task containing all findings. Do not modify code. When auto-merge is enabled, Relay will merge this exact reviewed head after review.approved.`, 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 };
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/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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.73",
3
+ "version": "1.0.74",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {