agents-relay 1.0.30 → 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 +19 -4
- package/dist/adapters.js +17 -29
- package/dist/cli.js +91 -24
- package/dist/dashboard.js +1 -1
- package/dist/execution-context.js +258 -0
- package/dist/planner.js +17 -3
- package/dist/reconciler.js +70 -17
- package/dist/relayd.js +7 -5
- package/dist/store.js +1 -1
- package/dist/workspace.js +11 -0
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +17 -1
- package/skills/chatgpt-browser-worker/SKILL.md +0 -67
- package/skills/chatgpt-browser-worker/agents/browser-worker.agent.md +0 -47
- package/skills/chatgpt-browser-worker/scripts/_submit_readiness.py +0 -30
- package/skills/chatgpt-browser-worker/scripts/_temporary_bh.py +0 -298
- package/skills/chatgpt-browser-worker/scripts/temporary_bh.py +0 -36
package/dist/planner.js
CHANGED
|
@@ -3,6 +3,18 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { taskOverallTimeoutMs } from './types.js';
|
|
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
|
+
}
|
|
6
18
|
function record(value) {
|
|
7
19
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
20
|
throw new Error('Planner result must be an object');
|
|
@@ -111,7 +123,7 @@ export function parsePlannerOutput(output) {
|
|
|
111
123
|
return parsePlannerResult(JSON.parse(candidate));
|
|
112
124
|
}
|
|
113
125
|
export function plannerInput(context) {
|
|
114
|
-
return JSON.stringify({ objective: context.objective, previous_planner_task_id: context.previousPlannerTaskId, tasks: context.tasks.map(task => ({ id: task.id, kind: task.kind ?? 'work', parent_task_id: task.parentTaskId, state: task.state, input: task.input, result: task.result, planner_result: task.plannerResult, error: task.error })) });
|
|
126
|
+
return JSON.stringify({ objective: context.objective, previous_planner_task_id: context.previousPlannerTaskId, execution_context: context.executionContext, tasks: context.tasks.map(task => ({ id: task.id, kind: task.kind ?? 'work', parent_task_id: task.parentTaskId, state: task.state, input: task.input, result: task.result, planner_result: task.plannerResult, error: task.error })) });
|
|
115
127
|
}
|
|
116
128
|
export class CommandObjectivePlanner {
|
|
117
129
|
command;
|
|
@@ -162,9 +174,11 @@ export class AgentObjectivePlanner {
|
|
|
162
174
|
this.agentFile = agentFile;
|
|
163
175
|
}
|
|
164
176
|
async plan(context, signal) {
|
|
165
|
-
const
|
|
177
|
+
const resolvedPlanner = context.executionContext?.agents.find(agent => agent.name === 'planner')?.path;
|
|
178
|
+
const instructions = await readFile(resolvedPlanner ?? this.agentFile, 'utf8');
|
|
166
179
|
const contract = await readFile(DEFAULT_TASK_JOB_CONTRACT, 'utf8');
|
|
167
|
-
const
|
|
180
|
+
const resolvedContext = context.executionContext ? executionContextPrompt(context.executionContext) + '\n\n' : '';
|
|
181
|
+
const prompt = instructions.trim() + '\n\n## Shared Task / Job Contract\n\n' + contract.trim() + '\n\n' + resolvedContext + '## Durable planner input\n\n' + plannerInput(context);
|
|
168
182
|
return await new Promise((resolve, reject) => {
|
|
169
183
|
const child = spawn(this.command, ['exec', '--json', '--', prompt], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
170
184
|
let stdoutBuffer = '';
|
package/dist/reconciler.js
CHANGED
|
@@ -4,9 +4,10 @@ import { schedule, transitionTask } from './scheduler.js';
|
|
|
4
4
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
5
5
|
import { eventFor } from './events.js';
|
|
6
6
|
import { parsePlannerResult, plannerInput } from './planner.js';
|
|
7
|
+
import { executionContextPrompt, resolveExecutionContext } from './execution-context.js';
|
|
7
8
|
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
8
9
|
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); }
|
|
9
|
-
export function managedWorkerInput(job, task) {
|
|
10
|
+
export function managedWorkerInput(job, task, context) {
|
|
10
11
|
if (!['codex', 'chatgpt'].includes(task.adapter) || !job.repository || job.prNumber <= 0)
|
|
11
12
|
return task.input;
|
|
12
13
|
if (!task.output)
|
|
@@ -20,9 +21,9 @@ PR: ${prUrl}
|
|
|
20
21
|
Job: ${job.id}
|
|
21
22
|
Task: ${task.id}
|
|
22
23
|
Parent task: ${task.parentTaskId ?? 'none'}
|
|
23
|
-
Project: ${task.projectName ?? 'unspecified'}
|
|
24
|
+
Project: ${task.projectName ?? context?.project.name ?? 'unspecified'}
|
|
24
25
|
|
|
25
|
-
${task.input}
|
|
26
|
+
${context ? executionContextPrompt(context) + '\n\n' : ''}${task.input}${task.parentTaskId ? '\n\n[Nested delegation contract]\nAny sub-work must be created as a durable Agents Relay child task with parentTaskId=' + task.id + '; do not directly spawn arbitrary child agents.' : ''}
|
|
26
27
|
|
|
27
28
|
[Output contract]
|
|
28
29
|
${output}
|
|
@@ -125,12 +126,17 @@ export class Reconciler {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
await this.recoverLostExecutions(job, now);
|
|
129
|
+
await this.recoverPlannerChildren(job);
|
|
128
130
|
for (const task of job.tasks)
|
|
129
131
|
if (task.state === 'CANCELLED') {
|
|
130
132
|
this.live.get(task.id)?.execution.cancel();
|
|
131
133
|
this.plannerLive.get(task.id)?.controller.abort();
|
|
132
134
|
}
|
|
133
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
|
+
}
|
|
134
140
|
if (prState === 'OPEN' && scheduled.executionMode === 'autonomous') {
|
|
135
141
|
scheduled = schedule(await this.maybeCreatePlanner(scheduled));
|
|
136
142
|
}
|
|
@@ -284,16 +290,24 @@ export class Reconciler {
|
|
|
284
290
|
async maybeCreatePlanner(job) {
|
|
285
291
|
if (job.executionMode !== 'autonomous')
|
|
286
292
|
return job;
|
|
293
|
+
if (job.initialPlanning?.status === 'pending')
|
|
294
|
+
return job;
|
|
287
295
|
const planners = job.tasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
288
296
|
if (planners.some(task => ['QUEUED', 'READY', 'RUNNING', 'WAITING'].includes(task.state)))
|
|
289
297
|
return job;
|
|
290
298
|
if (job.tasks.some(task => task.state === 'FAILED' || task.state === 'BLOCKED'))
|
|
291
299
|
return job;
|
|
300
|
+
if (job.tasks.length > 0 && !planners.some(task => task.state === 'SUCCEEDED'))
|
|
301
|
+
return job;
|
|
292
302
|
if (job.tasks.some(task => task.kind !== 'planner' && !['SUCCEEDED', 'CANCELLED'].includes(task.state)))
|
|
293
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;
|
|
294
306
|
const latest = planners.at(-1);
|
|
295
307
|
if (latest?.plannerResult?.objective_status === 'satisfied')
|
|
296
308
|
return job;
|
|
309
|
+
if (latest && job.review && job.review.outcome !== 'approved')
|
|
310
|
+
return job;
|
|
297
311
|
const used = new Set(job.tasks.map(task => task.id));
|
|
298
312
|
let number = planners.length + 1;
|
|
299
313
|
let id = `planner-${number}`;
|
|
@@ -302,11 +316,34 @@ export class Reconciler {
|
|
|
302
316
|
id = `planner-${number}`;
|
|
303
317
|
}
|
|
304
318
|
const now = new Date().toISOString();
|
|
305
|
-
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 };
|
|
306
320
|
await this.store.appendTask(task);
|
|
307
321
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.created', 'queued', `Planner task ${task.id} created`, 'orchestrator'));
|
|
308
322
|
return this.store.load(job.id);
|
|
309
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
|
+
}
|
|
339
|
+
async resolvedContext(job, task, role) {
|
|
340
|
+
const projectPath = this.options.projectPathForJob ? await this.options.projectPathForJob(job) : task.routing?.cwd ?? process.cwd();
|
|
341
|
+
return resolveExecutionContext(projectPath, role, {
|
|
342
|
+
...(this.options.executionContextRoots ?? {}),
|
|
343
|
+
registeredAgents: await this.store.listAgents(),
|
|
344
|
+
runtimes: this.options.adapters.map(adapter => ({ name: adapter.name, id: adapter.id, capabilities: [...adapter.capabilities] })),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
310
347
|
async launch(job, task) {
|
|
311
348
|
const adapter = this.options.adapters.find(x => x.name === task.adapter);
|
|
312
349
|
if (!adapter) {
|
|
@@ -322,8 +359,11 @@ export class Reconciler {
|
|
|
322
359
|
return;
|
|
323
360
|
}
|
|
324
361
|
let workerInput;
|
|
362
|
+
let executionContext;
|
|
325
363
|
try {
|
|
326
|
-
|
|
364
|
+
const role = task.agentName === 'reviewer' ? 'reviewer' : task.parentTaskId ? 'nested-worker' : 'worker';
|
|
365
|
+
executionContext = await this.resolvedContext(job, task, role);
|
|
366
|
+
workerInput = managedWorkerInput(job, task, executionContext);
|
|
327
367
|
}
|
|
328
368
|
catch (error) {
|
|
329
369
|
transitionTask(task, 'BLOCKED');
|
|
@@ -342,7 +382,7 @@ export class Reconciler {
|
|
|
342
382
|
await this.store.saveTask(task);
|
|
343
383
|
const controller = new AbortController();
|
|
344
384
|
let execution;
|
|
345
|
-
const workerTask = { ...task, input: workerInput };
|
|
385
|
+
const workerTask = { ...task, input: workerInput, routing: task.routing ? { ...task.routing, cwd: task.routing.cwd ?? executionContext.project.path } : task.routing };
|
|
346
386
|
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, Date.now(), this.options.startAckTimeoutMs ?? chatGptStartAckTimeoutMs(task.timeoutMs)) : null;
|
|
347
387
|
const closePolicy = task.chatgptTabClosePolicy ?? this.options.chatgptTabClosePolicy ?? adapter.tabClosePolicy ?? 'after-start';
|
|
348
388
|
const terminalWait = task.adapter === 'chatgpt' && closePolicy === 'after-terminal'
|
|
@@ -351,6 +391,7 @@ export class Reconciler {
|
|
|
351
391
|
const launchContext = {
|
|
352
392
|
...(startAckSetup ? { waitForWorkerStart: startAckSetup.ack } : {}),
|
|
353
393
|
...(terminalWait ? { waitForTerminal: terminalWait.promise } : {}),
|
|
394
|
+
executionContext,
|
|
354
395
|
};
|
|
355
396
|
try {
|
|
356
397
|
execution = adapter.launch(workerTask, controller.signal, launchContext);
|
|
@@ -435,7 +476,8 @@ export class Reconciler {
|
|
|
435
476
|
this.plannerLive.set(task.id, { executionId: executionId, controller, timer });
|
|
436
477
|
let promise;
|
|
437
478
|
try {
|
|
438
|
-
|
|
479
|
+
const executionContext = await this.resolvedContext(job, task, 'planner');
|
|
480
|
+
promise = planner.plan({ job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: task.parentTaskId, executionContext }, controller.signal);
|
|
439
481
|
}
|
|
440
482
|
catch (error) {
|
|
441
483
|
await this.finishPlanner(job.id, task.id, executionId, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -467,14 +509,15 @@ export class Reconciler {
|
|
|
467
509
|
}
|
|
468
510
|
try {
|
|
469
511
|
const result = parsePlannerResult(raw);
|
|
470
|
-
|
|
471
|
-
|
|
512
|
+
const children = result.next_tasks.map(spec => this.plannerChild(job, task, spec));
|
|
513
|
+
for (const child of children) {
|
|
472
514
|
const existing = job.tasks.find(item => item.id === child.id);
|
|
473
515
|
if (!existing)
|
|
474
|
-
|
|
516
|
+
continue;
|
|
475
517
|
else if (!samePlannerChildDefinition(existing, child))
|
|
476
518
|
throw new Error(`Planner task ${child.id} already exists with a different definition`);
|
|
477
519
|
}
|
|
520
|
+
// Persist the completed decision before exposing executable children.
|
|
478
521
|
task.plannerResult = result;
|
|
479
522
|
task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks } };
|
|
480
523
|
task.error = null;
|
|
@@ -482,22 +525,32 @@ export class Reconciler {
|
|
|
482
525
|
task.leaseExpiresAt = null;
|
|
483
526
|
transitionTask(task, 'SUCCEEDED');
|
|
484
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);
|
|
485
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 }));
|
|
486
532
|
}
|
|
487
533
|
catch (caught) {
|
|
488
534
|
const message = caught instanceof Error ? caught.message : String(caught);
|
|
489
|
-
task.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
}
|
|
495
548
|
}
|
|
496
549
|
this.detach(this.reconcile(jobId), jobId, task.id, 'planner follow-up reconciliation');
|
|
497
550
|
}
|
|
498
551
|
plannerChild(job, planner, spec) {
|
|
499
552
|
const now = new Date().toISOString();
|
|
500
|
-
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 };
|
|
501
554
|
}
|
|
502
555
|
clearExecution(taskId, executionId) {
|
|
503
556
|
const live = this.live.get(taskId);
|
package/dist/relayd.js
CHANGED
|
@@ -13,7 +13,7 @@ import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPull
|
|
|
13
13
|
import { githubAuthContext } from './github-auth.js';
|
|
14
14
|
import { isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
15
15
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
|
-
import { defaultWorkspaceRoot, discoverWorkspaceRepositories } from './workspace.js';
|
|
16
|
+
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath } from './workspace.js';
|
|
17
17
|
export const daemonHelp = `Agents Relay daemon runs the dashboard and worker pool.
|
|
18
18
|
|
|
19
19
|
Usage:
|
|
@@ -64,10 +64,12 @@ export async function runDaemon(argv) {
|
|
|
64
64
|
const trusted = auth.trustedAuthors;
|
|
65
65
|
const concurrency = Number(value(argv, '--concurrency', '4'));
|
|
66
66
|
const bus = value(argv, '--events') === 'nats' ? new NatsEventBus(value(argv, '--nats-url', 'nats://127.0.0.1:4222'), value(argv, '--subject-prefix', 'neo.events.job')) : undefined;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const workspaceRoot = normalized.workspaceRoot ?? defaultWorkspaceRoot();
|
|
68
|
+
const discoveredRepositories = normalized.repository ? [] : await discoverWorkspaceRepositories(workspaceRoot);
|
|
69
|
+
const repositories = normalized.repository ? [normalized.repository] : discoveredRepositories.map(item => item.repository);
|
|
70
|
+
const repositoryPaths = new Map(discoveredRepositories.map(item => [item.repository, item.path]));
|
|
71
|
+
const explicitProjectPath = value(argv, '--project-path');
|
|
72
|
+
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), eventBus: bus, reviewer: runtimeReviewer(), projectPathForJob: job => repositoryPaths.get(job.repository) ?? resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot) });
|
|
71
73
|
if (repositories.length === 0)
|
|
72
74
|
throw new Error(`No Git repositories with GitHub origins found under ${normalized.workspaceRoot ?? defaultWorkspaceRoot()}`);
|
|
73
75
|
const pool = new RepositoryWorkerPool(client, repositories, trusted, concurrency, makeReconciler);
|
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/dist/workspace.js
CHANGED
|
@@ -64,6 +64,17 @@ export async function discoverWorkspaceRepositories(root) {
|
|
|
64
64
|
}
|
|
65
65
|
return [...unique.values()].sort((a, b) => a.repository.localeCompare(b.repository));
|
|
66
66
|
}
|
|
67
|
+
export async function resolveWorkspaceRepositoryPath(repository, root = defaultWorkspaceRoot()) {
|
|
68
|
+
const match = (await discoverWorkspaceRepositories(root)).find(item => item.repository === repository);
|
|
69
|
+
if (!match)
|
|
70
|
+
throw new Error(`Repository ${repository} not found under workspace ${resolve(root)}; pass --project-path explicitly`);
|
|
71
|
+
return match.path;
|
|
72
|
+
}
|
|
73
|
+
export async function resolveManagedProjectPath(repository, explicitProjectPath = '', root = defaultWorkspaceRoot(), cwd = process.cwd()) {
|
|
74
|
+
if (explicitProjectPath)
|
|
75
|
+
return resolve(explicitProjectPath);
|
|
76
|
+
return repository ? resolveWorkspaceRepositoryPath(repository, root) : resolve(cwd);
|
|
77
|
+
}
|
|
67
78
|
function repositoryPathRank(path, root) {
|
|
68
79
|
const relativePath = relative(root, path);
|
|
69
80
|
if (!relativePath.includes('/'))
|
package/package.json
CHANGED
|
@@ -82,14 +82,30 @@ For Codex and ChatGPT workers, Agents Relay adds only durable execution identity
|
|
|
82
82
|
|
|
83
83
|
The job/task descriptions remain the durable source of intent. Adapters do not invent task meaning.
|
|
84
84
|
|
|
85
|
+
## Shared execution context
|
|
86
|
+
|
|
87
|
+
Before planner, reviewer, normal worker, or nested-worker execution, resolve one execution context from the bound project path. The shared resolver owns project instructions, skills, visible agents, and runtime capabilities with project-local > Neo shared > global `~/.agents` precedence. Concrete paths are execution-time data and must not be persisted into durable task records.
|
|
88
|
+
|
|
89
|
+
Leaf workers consume the resolved instruction/skill/agent paths, not the full top-level XChat orchestrator contract. Nested workers may inspect visible agents, but any delegation must be represented as an Agents Relay durable child task preserving `parentTaskId`, output contract, routing, leases/retries, events, and review gates.
|
|
90
|
+
|
|
85
91
|
## ChatGPT worker
|
|
86
92
|
|
|
87
|
-
The ChatGPT adapter uses the
|
|
93
|
+
The ChatGPT adapter uses the `chatgpt-browser-worker` path from the shared execution context; Relay does not bundle or independently rediscover that skill. It opens a fresh Temporary Chat tab, submits the complete task with the account defaults, verifies acceptance, applies the configured `after-start` (default), `never`, or `after-terminal` owned-tab close policy, and returns a submission receipt.
|
|
88
94
|
|
|
89
95
|
ChatGPT thread identity is diagnostic only. It is not persisted as a resumable task handle, and Relay does not poll, resume, retrieve a result from, or delete a ChatGPT conversation as part of task lifecycle.
|
|
90
96
|
|
|
91
97
|
## Autonomous planner
|
|
92
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
|
+
|
|
93
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.
|
|
94
110
|
|
|
95
111
|
## Durable truth
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: chatgpt-browser-worker
|
|
3
|
-
description: Submit an isolated one-shot ChatGPT browser worker task through browser-harness with a required durable output destination.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# ChatGPT browser worker
|
|
7
|
-
|
|
8
|
-
Use this skill to hand one self-contained task to ChatGPT through the authenticated
|
|
9
|
-
browser session. The caller does not manage ChatGPT threads, model selection,
|
|
10
|
-
thinking level, polling, or result retrieval.
|
|
11
|
-
|
|
12
|
-
## Output contract
|
|
13
|
-
|
|
14
|
-
Every task handed to this worker MUST declare exactly one durable output mode
|
|
15
|
-
before the worker is launched.
|
|
16
|
-
|
|
17
|
-
### 1. Task / PR output
|
|
18
|
-
|
|
19
|
-
Use this for repository or managed-task work.
|
|
20
|
-
|
|
21
|
-
The task prompt must identify the exact managed task/PR that owns the result.
|
|
22
|
-
The worker performs the requested work directly against that task/PR and records
|
|
23
|
-
its final outcome there.
|
|
24
|
-
|
|
25
|
-
Output:
|
|
26
|
-
task/PR
|
|
27
|
-
<exact managed task/PR identity and required final action>
|
|
28
|
-
|
|
29
|
-
### 2. File output
|
|
30
|
-
|
|
31
|
-
Use this for research, analysis, reports, or other artifact-producing work.
|
|
32
|
-
|
|
33
|
-
The task prompt must name the exact file path. That file is the authoritative
|
|
34
|
-
result.
|
|
35
|
-
|
|
36
|
-
Output:
|
|
37
|
-
file
|
|
38
|
-
<exact file path>
|
|
39
|
-
|
|
40
|
-
A Browser ChatGPT worker task without one of these output declarations is
|
|
41
|
-
invalid and must not be launched. The orchestrator chooses and writes the output
|
|
42
|
-
contract when it creates the worker task; the worker must not invent or change
|
|
43
|
-
the destination.
|
|
44
|
-
|
|
45
|
-
Progress and terminal success/failure are always reported through the normal
|
|
46
|
-
Neo event contract. Events are execution observability, not a third output mode.
|
|
47
|
-
|
|
48
|
-
## Runtime behavior
|
|
49
|
-
|
|
50
|
-
The worker is one-shot. It opens an isolated worker-owned ChatGPT tab, submits
|
|
51
|
-
the complete task, verifies that ChatGPT accepted the submission, and applies
|
|
52
|
-
the configured tab-close policy. `after-start` waits for the exact
|
|
53
|
-
post-submission worker `task.started` acknowledgement, `never` leaves the owned
|
|
54
|
-
tab open for debugging, and `after-terminal` waits for a new exact-task
|
|
55
|
-
`task.completed`, `task.failed`, `task.blocked`, or `task.cancelled` event.
|
|
56
|
-
ChatGPT continues the task independently and publishes the result through the
|
|
57
|
-
declared output contract. If the start acknowledgement does not arrive within
|
|
58
|
-
60 seconds, close only the owned tab and fail with `worker_start_timeout`.
|
|
59
|
-
|
|
60
|
-
The worker must never interact through a pre-existing user ChatGPT tab.
|
|
61
|
-
Browser details, transient conversation identity, submission verification,
|
|
62
|
-
model/default-thinking behavior, and tab cleanup are implementation concerns of
|
|
63
|
-
the worker and are not part of the caller-facing contract.
|
|
64
|
-
|
|
65
|
-
The runtime entry point is the browser-worker agent. Callers launch that agent
|
|
66
|
-
with the complete task prompt and declared output; they do not call helper
|
|
67
|
-
scripts directly.
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: browser-worker
|
|
3
|
-
description: Submit one isolated ChatGPT browser task and return after the task is accepted.
|
|
4
|
-
type: worker
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Browser Worker Agent
|
|
8
|
-
|
|
9
|
-
You are the runtime owner for the `chatgpt-browser-worker` skill. Load and follow
|
|
10
|
-
that skill and the `browser-harness` skill before execution.
|
|
11
|
-
|
|
12
|
-
The outer orchestrator gives you one complete task. That task MUST already
|
|
13
|
-
contain exactly one output declaration defined by the skill:
|
|
14
|
-
|
|
15
|
-
- `task/PR` with the exact managed task/PR identity and required final action; or
|
|
16
|
-
- `file` with the exact authoritative output path.
|
|
17
|
-
|
|
18
|
-
If the output declaration is missing or ambiguous, do not invent one.
|
|
19
|
-
|
|
20
|
-
## Runtime rules
|
|
21
|
-
|
|
22
|
-
- Use `browser-harness` for every ChatGPT browser interaction.
|
|
23
|
-
- Execute only a one-shot submission. Do not expose or operate a
|
|
24
|
-
create/resume/status/result/continue lifecycle for callers.
|
|
25
|
-
- Always create a fresh worker-owned tab. Never type, upload, click New chat,
|
|
26
|
-
select a Project, or submit through a pre-existing user ChatGPT tab.
|
|
27
|
-
- Use Temporary Chat and the account's existing default model/thinking
|
|
28
|
-
settings. Do not change model or thinking settings.
|
|
29
|
-
- Upload requested files, submit the complete task prompt, and verify that the
|
|
30
|
-
submission became a new user turn.
|
|
31
|
-
- Once submission is verified, follow the configured owned-tab close policy:
|
|
32
|
-
`after-start` closes after a new worker-owned `task.started` event, `never`
|
|
33
|
-
leaves the owned tab open for test/debug use, and `after-terminal` closes only
|
|
34
|
-
after a new exact-task `task.completed`, `task.failed`, `task.blocked`, or
|
|
35
|
-
`task.cancelled` event. Never close a user tab. Do not wait for the assistant
|
|
36
|
-
response or reopen/poll the conversation. If the start acknowledgement does
|
|
37
|
-
not arrive within 60 seconds, close only the owned tab and report a timeout.
|
|
38
|
-
- Treat any observed conversation/thread identity only as diagnostic evidence,
|
|
39
|
-
never as a resumable handle.
|
|
40
|
-
- Progress and terminal success/failure for the actual delegated task are
|
|
41
|
-
reported through the normal Neo event contract by the executing task. The
|
|
42
|
-
final durable result goes to the declared task/PR or file output.
|
|
43
|
-
- Stop on authentication, MFA, consent, or ambiguous browser state rather than
|
|
44
|
-
bypassing it.
|
|
45
|
-
|
|
46
|
-
Helper scripts under `scripts/` are implementation details. Callers must not
|
|
47
|
-
invoke them directly.
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""Bounded polling helpers for ChatGPT submit readiness."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import time
|
|
5
|
-
from collections.abc import Callable, Mapping
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def wait_until_stable(
|
|
9
|
-
read_state: Callable[[], Mapping[str, object]],
|
|
10
|
-
ready: Callable[[Mapping[str, object]], bool],
|
|
11
|
-
*,
|
|
12
|
-
timeout: float,
|
|
13
|
-
interval: float = 0.25,
|
|
14
|
-
stable_samples: int = 2,
|
|
15
|
-
phase: str,
|
|
16
|
-
) -> Mapping[str, object]:
|
|
17
|
-
"""Return only after the observable state is ready for stable_samples polls."""
|
|
18
|
-
deadline = time.monotonic() + timeout
|
|
19
|
-
stable = 0
|
|
20
|
-
last_state: Mapping[str, object] = {}
|
|
21
|
-
while time.monotonic() < deadline:
|
|
22
|
-
last_state = read_state()
|
|
23
|
-
if ready(last_state):
|
|
24
|
-
stable += 1
|
|
25
|
-
if stable >= stable_samples:
|
|
26
|
-
return last_state
|
|
27
|
-
else:
|
|
28
|
-
stable = 0
|
|
29
|
-
time.sleep(interval)
|
|
30
|
-
raise RuntimeError(f"Temporary Chat {phase} was not observed ready")
|