agents-relay 1.0.31 → 1.0.32
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 +10 -3
- package/dist/adapters.js +6 -1
- package/dist/cli.js +43 -21
- package/dist/dashboard.js +1 -1
- package/dist/planner.js +11 -0
- package/dist/reconciler.js +50 -11
- package/dist/store.js +1 -1
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +10 -0
package/README.md
CHANGED
|
@@ -31,9 +31,16 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
|
|
|
31
31
|
--capabilities model,chatgpt,browser-harness
|
|
32
32
|
~~~
|
|
33
33
|
|
|
34
|
-
Jobs default to `fixed`, preserving the original submit-and-run behavior.
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
Jobs default to `fixed`, preserving the original submit-and-run behavior. Every
|
|
35
|
+
executable task is now created by a completed durable planning decision. `job
|
|
36
|
+
create --start` records `planner-0` (supplied by the orchestrator) and the
|
|
37
|
+
initial task atomically from the caller's perspective; retrying the command is
|
|
38
|
+
idempotent. Use `--mode autonomous` with `init` or `job create` to make
|
|
39
|
+
reconciliation own planner progress. Autonomous creation with only an objective
|
|
40
|
+
may create a model planner first, but it cannot materialize work until that
|
|
41
|
+
planner succeeds. Fixed jobs never grow another planning round.
|
|
42
|
+
|
|
43
|
+
The durable status JSON and dashboard show the mode and planning provenance. An
|
|
37
44
|
autonomous runtime uses the bundled `skills/agents-relay/agents/planner.agent.md` by default; `--planner-command` is an optional override
|
|
38
45
|
snapshot as JSON on stdin and must return `{ "objective_status": "in_progress"|"satisfied", "assessment": "...", "next_tasks": [...] }`.
|
|
39
46
|
Planner-created tasks use stable IDs and the existing parent/subtask tree, so
|
package/dist/adapters.js
CHANGED
|
@@ -108,7 +108,12 @@ export class StartAckTimeout extends LaunchFailure {
|
|
|
108
108
|
}
|
|
109
109
|
async function loadBrowserWorkerRuntime(context, explicit) {
|
|
110
110
|
const resolvedSkill = context?.skills.find(skill => skill.name === 'chatgpt-browser-worker');
|
|
111
|
-
|
|
111
|
+
// Once a worker receives a resolved execution context, that context owns
|
|
112
|
+
// skill discovery. The explicit path remains the fallback for direct
|
|
113
|
+
// adapter use, where no shared context has been resolved yet.
|
|
114
|
+
const candidate = resolvedSkill
|
|
115
|
+
? join(dirname(resolvedSkill.path), 'agents', 'browser-worker.agent.md')
|
|
116
|
+
: explicit;
|
|
112
117
|
if (!candidate)
|
|
113
118
|
throw new Error('chatgpt-browser-worker is not visible in the resolved execution context');
|
|
114
119
|
try {
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { NatsEventBus, eventFor } from './events.js';
|
|
|
14
14
|
import { serveDashboard } from './dashboard.js';
|
|
15
15
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
16
|
import { discoverAgents } from './registry.js';
|
|
17
|
-
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
17
|
+
import { AgentObjectivePlanner, CommandObjectivePlanner, orchestratorPlanningDecision } from './planner.js';
|
|
18
18
|
import { extendRetryBudget } from './scheduler.js';
|
|
19
19
|
import { resolveExecutionContext } from './execution-context.js';
|
|
20
20
|
import { defaultWorkspaceRoot, resolveManagedProjectPath } from './workspace.js';
|
|
@@ -270,7 +270,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
270
270
|
if (!(error instanceof Error) || !error.message.includes('has no durable'))
|
|
271
271
|
throw error;
|
|
272
272
|
const now = new Date().toISOString();
|
|
273
|
-
job = { id: options.id, title: options.title || pr.title || 'Agents Relay job', objective: options.objective || pr.body || undefined, description: pr.body || undefined, priority: options.priority ?? 'P2', executionMode: options.executionMode ?? 'fixed', autoMerge: options.autoMerge ?? true, prNumber: pr.number, repository: repositoryName, state: 'OPEN', continuation: options.continuation ?? null, createdAt: now, updatedAt: now, tasks: [] };
|
|
273
|
+
job = { id: options.id, title: options.title || pr.title || 'Agents Relay job', objective: options.objective || pr.body || undefined, description: pr.body || undefined, priority: options.priority ?? 'P2', executionMode: options.executionMode ?? 'fixed', autoMerge: options.autoMerge ?? true, prNumber: pr.number, repository: repositoryName, state: 'OPEN', continuation: options.continuation ?? null, initialPlanning: options.initialTasks?.length ? { status: 'pending', decisionId: 'planner-0' } : undefined, createdAt: now, updatedAt: now, tasks: [] };
|
|
274
274
|
}
|
|
275
275
|
if (!job.description && pr.body)
|
|
276
276
|
job.description = pr.body;
|
|
@@ -286,9 +286,40 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
286
286
|
job.priority = 'P2';
|
|
287
287
|
if (!job.title && pr.title)
|
|
288
288
|
job.title = pr.title;
|
|
289
|
-
|
|
289
|
+
if (options.initialTasks?.length) {
|
|
290
|
+
const decisionId = job.initialPlanning?.decisionId ?? 'planner-0';
|
|
291
|
+
job.initialPlanning = { status: 'pending', decisionId };
|
|
292
|
+
job.updatedAt = new Date().toISOString();
|
|
293
|
+
await store.saveJob(job);
|
|
294
|
+
const decision = orchestratorPlanningDecision(job, options.initialTasks, decisionId, null, 0);
|
|
295
|
+
if (!(await store.load(options.id)).tasks.some(task => task.id === decision.id))
|
|
296
|
+
await store.appendTask(decision);
|
|
297
|
+
for (const original of options.initialTasks) {
|
|
298
|
+
const task = { ...original, parentTaskId: decision.id, planningDecisionId: decision.id, planningSource: 'orchestrator', planningRound: 0 };
|
|
299
|
+
if (!(await store.load(options.id)).tasks.some(item => item.id === task.id))
|
|
300
|
+
await store.appendTask(task);
|
|
301
|
+
}
|
|
302
|
+
job.initialPlanning = { status: 'complete', decisionId };
|
|
303
|
+
job.updatedAt = new Date().toISOString();
|
|
304
|
+
await store.saveJob(job);
|
|
305
|
+
}
|
|
306
|
+
else
|
|
307
|
+
await store.saveJob(job);
|
|
290
308
|
return { job: await store.load(options.id), pr };
|
|
291
309
|
}
|
|
310
|
+
async function appendPlannedTask(store, job, task) {
|
|
311
|
+
const existing = job.tasks.find(item => item.id === task.id);
|
|
312
|
+
if (existing)
|
|
313
|
+
return existing;
|
|
314
|
+
const decisionId = `plan-${task.id}`;
|
|
315
|
+
const decision = orchestratorPlanningDecision(job, [task], decisionId, task.parentTaskId, job.tasks.filter(item => item.kind === 'planner').length, 'orchestrator');
|
|
316
|
+
if (!job.tasks.some(item => item.id === decision.id))
|
|
317
|
+
await store.appendTask(decision);
|
|
318
|
+
const planned = { ...task, parentTaskId: task.parentTaskId ?? null, planningDecisionId: decision.id, planningSource: 'orchestrator', planningRound: decision.planningRound };
|
|
319
|
+
await store.appendTask(planned);
|
|
320
|
+
job.tasks.push(planned, decision);
|
|
321
|
+
return planned;
|
|
322
|
+
}
|
|
292
323
|
function workerTaskFromArgs(job, args, now, requireInput = false) {
|
|
293
324
|
const model = arg(args, '--model');
|
|
294
325
|
const adapterValue = arg(args, '--adapter');
|
|
@@ -438,12 +469,12 @@ async function runJobCommand(action, args) {
|
|
|
438
469
|
if (!id)
|
|
439
470
|
throw new Error('--id is required for managed GitHub jobs');
|
|
440
471
|
const start = hasArg(args, '--start');
|
|
472
|
+
const initialTask = start ? workerTaskFromArgs({ id, title: '', executionMode: 'fixed', prNumber: 0, repository: repo, state: 'OPEN', continuation: null, createdAt: '', updatedAt: '', tasks: [] }, args, new Date().toISOString(), true) : undefined;
|
|
441
473
|
if (start && action !== 'create')
|
|
442
474
|
throw new Error('--start is only supported by job create');
|
|
443
475
|
if (start) {
|
|
444
476
|
if (!hasArg(args, '--adapter'))
|
|
445
477
|
throw new Error('job create --start requires --adapter codex|chatgpt');
|
|
446
|
-
workerTaskFromArgs({ id, title: '', executionMode: 'fixed', prNumber: 0, repository: repo, state: 'OPEN', continuation: null, createdAt: '', updatedAt: '', tasks: [] }, args, new Date().toISOString(), true);
|
|
447
478
|
}
|
|
448
479
|
const auth = await githubAuthContext(args);
|
|
449
480
|
const client = auth.client;
|
|
@@ -465,18 +496,11 @@ async function runJobCommand(action, args) {
|
|
|
465
496
|
prNumber,
|
|
466
497
|
head: arg(args, '--head') || undefined,
|
|
467
498
|
base: arg(args, '--base', 'main'),
|
|
468
|
-
body: arg(args, '--body') || undefined
|
|
499
|
+
body: arg(args, '--body') || undefined,
|
|
500
|
+
initialTasks: initialTask ? [initialTask] : undefined
|
|
469
501
|
});
|
|
470
|
-
if (start)
|
|
471
|
-
|
|
472
|
-
const existing = result.job.tasks.find(item => item.id === task.id);
|
|
473
|
-
const store = new GitHubStore(client, repo, result.pr.number, trustedAuthors);
|
|
474
|
-
await store.appendTask(task);
|
|
475
|
-
if (!existing) {
|
|
476
|
-
await publishWake(eventBus(args), result.job, task.id, `Task ${task.id} submitted`);
|
|
477
|
-
result.job.tasks.push(task);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
502
|
+
if (start)
|
|
503
|
+
await publishWake(eventBus(args), result.job, initialTask?.id ?? 'initial', `Initial planning decision and task ${initialTask?.id ?? 'initial'} recorded`);
|
|
480
504
|
console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state, task: start ? arg(args, '--task-id', 'initial') : undefined }, null, 2));
|
|
481
505
|
}
|
|
482
506
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
@@ -621,8 +645,7 @@ async function main() {
|
|
|
621
645
|
throw new Error('record requires --summary');
|
|
622
646
|
const commit = arg(args, '--commit');
|
|
623
647
|
const task = { jobId: job.id, id, projectName: arg(args, '--project') || undefined, agentName: arg(args, '--agent', 'orchestrator'), parentTaskId: arg(args, '--parent') || null, dependencies: arg(args, '--deps').split(',').filter(Boolean), capabilities: [], adapter: 'orchestrator', input: arg(args, '--input', summary), continuation: null, continuationDeliveredAt: null, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: commit || null, threadId: null, result: { summary, data: commit ? { commit } : undefined }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now };
|
|
624
|
-
await store
|
|
625
|
-
job.tasks.push(task);
|
|
648
|
+
await appendPlannedTask(store, job, task);
|
|
626
649
|
await saveLocal(localFile, job);
|
|
627
650
|
console.log(`Recorded ${id}`);
|
|
628
651
|
return;
|
|
@@ -631,16 +654,15 @@ async function main() {
|
|
|
631
654
|
const now = new Date().toISOString();
|
|
632
655
|
const task = workerTaskFromArgs(job, args, now);
|
|
633
656
|
task.id = arg(args, '--task-id', randomUUID());
|
|
634
|
-
await store
|
|
635
|
-
job.tasks.push(task);
|
|
657
|
+
const planned = await appendPlannedTask(store, job, task);
|
|
636
658
|
if (job.state !== 'RUNNING') {
|
|
637
659
|
job.state = 'RUNNING';
|
|
638
660
|
job.updatedAt = now;
|
|
639
661
|
await store.saveJob(job);
|
|
640
662
|
}
|
|
641
663
|
await saveLocal(localFile, job);
|
|
642
|
-
await publishWake(bus, job,
|
|
643
|
-
console.log(`Submitted ${
|
|
664
|
+
await publishWake(bus, job, planned.id, `Planning decision recorded and task ${planned.id} submitted`);
|
|
665
|
+
console.log(`Submitted ${planned.id}`);
|
|
644
666
|
return;
|
|
645
667
|
}
|
|
646
668
|
if (command === 'status') {
|
package/dist/dashboard.js
CHANGED
|
@@ -240,7 +240,7 @@ function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();
|
|
|
240
240
|
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;}
|
|
241
241
|
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));}
|
|
242
242
|
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;}
|
|
243
|
-
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}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('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(groups);}
|
|
243
|
+
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}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(groups);}
|
|
244
244
|
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();}
|
|
245
245
|
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('GitHub state',job.githubState||'UNKNOWN'),field('Durable state',job.state),field('Execution mode',job.executionMode||'fixed'),field('Status',awaitingInitialTask(job)?'Awaiting initial task':job.state),field('Review',reviewStatus(job)),field('Review head SHA',job.review?.headSha||job.headSha||'—'),field('Objective',job.objective||job.description||job.title),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();}
|
|
246
246
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();}
|
package/dist/planner.js
CHANGED
|
@@ -4,6 +4,17 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { taskOverallTimeoutMs } from './types.js';
|
|
6
6
|
import { executionContextPrompt } from './execution-context.js';
|
|
7
|
+
export function planningSpec(task) {
|
|
8
|
+
if (!task.output && task.adapter !== 'orchestrator')
|
|
9
|
+
throw new Error(`Initial task ${task.id} requires an output contract`);
|
|
10
|
+
if (!['codex', 'chatgpt', 'orchestrator'].includes(task.adapter))
|
|
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, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs };
|
|
13
|
+
}
|
|
14
|
+
export function orchestratorPlanningDecision(job, tasks, id, parentTaskId, round, source = 'orchestrator') {
|
|
15
|
+
const now = new Date().toISOString();
|
|
16
|
+
return { jobId: job.id, id, kind: 'planner', priority: job.priority, storySize: 'S', agentName: source === 'orchestrator' ? 'orchestrator' : 'planner', parentTaskId, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: `Planning decision supplied by ${source} for ${tasks.length} task(s)`, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: { summary: source === 'orchestrator' ? 'Initial task set supplied by orchestrator' : 'Planning decision completed' }, plannerResult: { objective_status: 'in_progress', assessment: source === 'orchestrator' ? 'Initial task set supplied by orchestrator' : 'Planning decision completed', next_tasks: tasks.map(planningSpec) }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now, planningDecisionId: id, planningSource: source, planningRound: round };
|
|
17
|
+
}
|
|
7
18
|
function record(value) {
|
|
8
19
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
20
|
throw new Error('Planner result must be an object');
|
package/dist/reconciler.js
CHANGED
|
@@ -126,12 +126,17 @@ export class Reconciler {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
await this.recoverLostExecutions(job, now);
|
|
129
|
+
await this.recoverPlannerChildren(job);
|
|
129
130
|
for (const task of job.tasks)
|
|
130
131
|
if (task.state === 'CANCELLED') {
|
|
131
132
|
this.live.get(task.id)?.execution.cancel();
|
|
132
133
|
this.plannerLive.get(task.id)?.controller.abort();
|
|
133
134
|
}
|
|
134
135
|
let scheduled = schedule(await this.store.load(jobId));
|
|
136
|
+
if (prState === 'OPEN') {
|
|
137
|
+
await this.ensureFinalReview(scheduled, prState, headSha);
|
|
138
|
+
scheduled = schedule(await this.store.load(jobId));
|
|
139
|
+
}
|
|
135
140
|
if (prState === 'OPEN' && scheduled.executionMode === 'autonomous') {
|
|
136
141
|
scheduled = schedule(await this.maybeCreatePlanner(scheduled));
|
|
137
142
|
}
|
|
@@ -285,16 +290,24 @@ export class Reconciler {
|
|
|
285
290
|
async maybeCreatePlanner(job) {
|
|
286
291
|
if (job.executionMode !== 'autonomous')
|
|
287
292
|
return job;
|
|
293
|
+
if (job.initialPlanning?.status === 'pending')
|
|
294
|
+
return job;
|
|
288
295
|
const planners = job.tasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
289
296
|
if (planners.some(task => ['QUEUED', 'READY', 'RUNNING', 'WAITING'].includes(task.state)))
|
|
290
297
|
return job;
|
|
291
298
|
if (job.tasks.some(task => task.state === 'FAILED' || task.state === 'BLOCKED'))
|
|
292
299
|
return job;
|
|
300
|
+
if (job.tasks.length > 0 && !planners.some(task => task.state === 'SUCCEEDED'))
|
|
301
|
+
return job;
|
|
293
302
|
if (job.tasks.some(task => task.kind !== 'planner' && !['SUCCEEDED', 'CANCELLED'].includes(task.state)))
|
|
294
303
|
return job;
|
|
304
|
+
if (planners.length > 0 && planners.at(-1)?.plannerResult?.objective_status === 'in_progress' && !job.tasks.some(task => task.planningDecisionId === planners.at(-1)?.id))
|
|
305
|
+
return job;
|
|
295
306
|
const latest = planners.at(-1);
|
|
296
307
|
if (latest?.plannerResult?.objective_status === 'satisfied')
|
|
297
308
|
return job;
|
|
309
|
+
if (latest && job.review && job.review.outcome !== 'approved')
|
|
310
|
+
return job;
|
|
298
311
|
const used = new Set(job.tasks.map(task => task.id));
|
|
299
312
|
let number = planners.length + 1;
|
|
300
313
|
let id = `planner-${number}`;
|
|
@@ -303,11 +316,26 @@ export class Reconciler {
|
|
|
303
316
|
id = `planner-${number}`;
|
|
304
317
|
}
|
|
305
318
|
const now = new Date().toISOString();
|
|
306
|
-
const task = { jobId: job.id, id, kind: 'planner', storySize: 'M', agentName: 'planner', parentTaskId: latest?.id ?? null, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: plannerInput({ job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: latest?.id ?? null }), continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now };
|
|
319
|
+
const task = { jobId: job.id, id, kind: 'planner', storySize: 'M', agentName: 'planner', parentTaskId: latest?.id ?? null, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: plannerInput({ job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: latest?.id ?? null }), continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now, planningDecisionId: id, planningSource: 'model-planner', planningRound: planners.length + 1 };
|
|
307
320
|
await this.store.appendTask(task);
|
|
308
321
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.created', 'queued', `Planner task ${task.id} created`, 'orchestrator'));
|
|
309
322
|
return this.store.load(job.id);
|
|
310
323
|
}
|
|
324
|
+
async recoverPlannerChildren(job) {
|
|
325
|
+
for (const planner of job.tasks.filter(task => task.kind === 'planner' && task.state === 'SUCCEEDED' && task.plannerResult)) {
|
|
326
|
+
const children = planner.plannerResult?.next_tasks.map(spec => this.plannerChild(job, planner, spec)) ?? [];
|
|
327
|
+
for (const child of children) {
|
|
328
|
+
const existing = job.tasks.find(item => item.id === child.id);
|
|
329
|
+
if (existing) {
|
|
330
|
+
if (!samePlannerChildDefinition(existing, child))
|
|
331
|
+
throw new Error(`Planner task ${child.id} already exists with a different definition`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
await this.store.appendTask(child);
|
|
335
|
+
job.tasks.push(child);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
311
339
|
async resolvedContext(job, task, role) {
|
|
312
340
|
const projectPath = this.options.projectPathForJob ? await this.options.projectPathForJob(job) : task.routing?.cwd ?? process.cwd();
|
|
313
341
|
return resolveExecutionContext(projectPath, role, {
|
|
@@ -481,14 +509,15 @@ export class Reconciler {
|
|
|
481
509
|
}
|
|
482
510
|
try {
|
|
483
511
|
const result = parsePlannerResult(raw);
|
|
484
|
-
|
|
485
|
-
|
|
512
|
+
const children = result.next_tasks.map(spec => this.plannerChild(job, task, spec));
|
|
513
|
+
for (const child of children) {
|
|
486
514
|
const existing = job.tasks.find(item => item.id === child.id);
|
|
487
515
|
if (!existing)
|
|
488
|
-
|
|
516
|
+
continue;
|
|
489
517
|
else if (!samePlannerChildDefinition(existing, child))
|
|
490
518
|
throw new Error(`Planner task ${child.id} already exists with a different definition`);
|
|
491
519
|
}
|
|
520
|
+
// Persist the completed decision before exposing executable children.
|
|
492
521
|
task.plannerResult = result;
|
|
493
522
|
task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks } };
|
|
494
523
|
task.error = null;
|
|
@@ -496,22 +525,32 @@ export class Reconciler {
|
|
|
496
525
|
task.leaseExpiresAt = null;
|
|
497
526
|
transitionTask(task, 'SUCCEEDED');
|
|
498
527
|
await this.store.saveTask(task);
|
|
528
|
+
for (const child of children)
|
|
529
|
+
if (!job.tasks.some(item => item.id === child.id))
|
|
530
|
+
await this.store.appendTask(child);
|
|
499
531
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.completed', 'succeeded', `Planner ${task.id} completed: ${result.objective_status}`, 'orchestrator', { objective_status: result.objective_status }));
|
|
500
532
|
}
|
|
501
533
|
catch (caught) {
|
|
502
534
|
const message = caught instanceof Error ? caught.message : String(caught);
|
|
503
|
-
task.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
535
|
+
if (task.state === 'SUCCEEDED') {
|
|
536
|
+
task.error = `Child materialization failed: ${message}`;
|
|
537
|
+
await this.store.saveTask(task);
|
|
538
|
+
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.children_failed', 'failed', `Planner ${task.id} child materialization failed: ${message}`, 'user'));
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
task.error = message;
|
|
542
|
+
task.leaseOwner = null;
|
|
543
|
+
task.leaseExpiresAt = null;
|
|
544
|
+
transitionTask(task, task.attempt < task.maxAttempts ? 'READY' : 'FAILED');
|
|
545
|
+
await this.store.saveTask(task);
|
|
546
|
+
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.failed', 'failed', `Planner ${task.id}: ${message}`, 'user'));
|
|
547
|
+
}
|
|
509
548
|
}
|
|
510
549
|
this.detach(this.reconcile(jobId), jobId, task.id, 'planner follow-up reconciliation');
|
|
511
550
|
}
|
|
512
551
|
plannerChild(job, planner, spec) {
|
|
513
552
|
const now = new Date().toISOString();
|
|
514
|
-
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 ?? [], adapter: spec.adapter, input: spec.input, output: spec.output, routing: spec.routing, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: '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', spec.routing?.reasoning), createdAt: now, updatedAt: now };
|
|
553
|
+
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 ?? [], adapter: spec.adapter, input: spec.input, output: spec.output, routing: spec.routing, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: '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', spec.routing?.reasoning), createdAt: now, updatedAt: now, planningDecisionId: planner.id, planningSource: planner.planningSource ?? 'model-planner', planningRound: planner.planningRound ?? 1 };
|
|
515
554
|
}
|
|
516
555
|
clearExecution(taskId, executionId) {
|
|
517
556
|
const live = this.live.get(taskId);
|
package/dist/store.js
CHANGED
|
@@ -82,7 +82,7 @@ function immutableTaskDefinition(task) {
|
|
|
82
82
|
} : undefined;
|
|
83
83
|
return {
|
|
84
84
|
jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName,
|
|
85
|
-
kind: task.kind ?? 'work', parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, adapter: task.adapter,
|
|
85
|
+
kind: task.kind ?? 'work', planningDecisionId: task.planningDecisionId, planningSource: task.planningSource, planningRound: task.planningRound, parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, adapter: task.adapter,
|
|
86
86
|
input: task.input, output: task.output, routing, chatgptTabClosePolicy: task.chatgptTabClosePolicy ?? 'after-start', continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
|
|
87
87
|
};
|
|
88
88
|
}
|
package/package.json
CHANGED
|
@@ -96,6 +96,16 @@ ChatGPT thread identity is diagnostic only. It is not persisted as a resumable t
|
|
|
96
96
|
|
|
97
97
|
## Autonomous planner
|
|
98
98
|
|
|
99
|
+
No executable task may be materialized without a completed durable planning
|
|
100
|
+
decision. Job creation with caller-supplied initial work records an
|
|
101
|
+
orchestrator-owned `planner-0` decision before its initial tasks; autonomous
|
|
102
|
+
mode must not start a model planner in that case. Objective-only autonomous
|
|
103
|
+
jobs may start a real model planner, and planner children are created only
|
|
104
|
+
after that planner succeeds. Fixed jobs do not automatically create another
|
|
105
|
+
planning round. Each task exposes `planningDecisionId`, `planningSource`, and
|
|
106
|
+
`planningRound` for dashboard lineage. Nested worker delegation must create its
|
|
107
|
+
durable planning decision and children through Relay as well.
|
|
108
|
+
|
|
99
109
|
Planner-created child tasks must also be descriptive agent work. They must select `codex` or `chatgpt` and include an explicit output contract. A planner must never generate shell-command worker tasks.
|
|
100
110
|
|
|
101
111
|
## Durable truth
|