agents-relay 2.0.53 → 2.0.54
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 +4 -0
- package/dist/cli-help.js +4 -1
- package/dist/cli-support.js +15 -11
- package/dist/cli.js +11 -4
- package/dist/dashboard.js +2 -0
- package/dist/reconciler-review.js +53 -64
- package/dist/reconciler.js +13 -11
- package/dist/relayd.js +18 -2
- package/dist/task-actions.js +23 -3
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +9 -0
- package/skills/agents-relay/contracts/task-job-contract.md +8 -0
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ Agents Relay is a generic TypeScript runtime for durable asynchronous agent jobs
|
|
|
4
4
|
|
|
5
5
|
V2 separates **operational execution state** from that collaboration record. A local RelayStore may use SQLite/WAL for runtime snapshots, event idempotency and a durable GitHub projection outbox. ProjectedDurableStore bootstraps from the existing GitHub representation, commits Job/Plan/Task runtime changes locally, and lets GitHubProjectionWriter project them asynchronously. GitHub therefore remains the human-readable durable Job/Plan/Task/Review surface without being required for every high-frequency runtime transition.
|
|
6
6
|
|
|
7
|
+
The canonical V2 architecture contract is
|
|
8
|
+
[DESIGN-PRINCIPLES.md](DESIGN-PRINCIPLES.md). README sections below describe
|
|
9
|
+
the runtime; they do not replace that contract.
|
|
10
|
+
|
|
7
11
|
## V2 architecture
|
|
8
12
|
|
|
9
13
|
```mermaid
|
package/dist/cli-help.js
CHANGED
|
@@ -123,9 +123,12 @@ Required: --task-id ID and the normal Job storage options.`,
|
|
|
123
123
|
'task list': `List durable Tasks for one Job.
|
|
124
124
|
|
|
125
125
|
Required: the normal Job storage options.`,
|
|
126
|
+
'task start': `Mark one orchestrator-owned self Task RUNNING when the creating orchestrator actually begins the work.
|
|
127
|
+
|
|
128
|
+
Required: --task-id ID and the normal Job storage options. The Task must use adapter=orchestrator and be QUEUED. Start must occur before task complete.`,
|
|
126
129
|
'task complete': `Complete one orchestrator-owned Task after the creating orchestrator has finished the work.
|
|
127
130
|
|
|
128
|
-
Required: --task-id ID, --summary TEXT, and the normal Job storage options. The Task must use adapter=orchestrator and be
|
|
131
|
+
Required: --task-id ID, --summary TEXT, and the normal Job storage options. The Task must use adapter=orchestrator and already be RUNNING. This command is for the same orchestrator turn that created the self-task; it is not a deferred handoff mechanism.`,
|
|
129
132
|
'task retry': `Retry one durable Task.
|
|
130
133
|
|
|
131
134
|
Required: --task-id ID and the normal Job storage options.`,
|
package/dist/cli-support.js
CHANGED
|
@@ -162,20 +162,24 @@ export async function publishTaskUnblocked(bus, job, taskId, reason) { if (!bus)
|
|
|
162
162
|
catch (error) {
|
|
163
163
|
process.stderr.write(`warning: unblock event unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
164
164
|
} }
|
|
165
|
+
export async function publishOrchestratorTaskStarted(bus, job, task) {
|
|
166
|
+
if (!bus)
|
|
167
|
+
return;
|
|
168
|
+
try {
|
|
169
|
+
await bus.publish(eventFor(job.id, task.id, task.parentTaskId, 'task.started', 'running', `Task ${task.id} started by orchestrator`, 'orchestrator', { adapter: 'orchestrator' }, { agent: 'relay', component: 'agents-relay' }));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
process.stderr.write(`warning: orchestrator task start event unavailable for ${job.id}/${task.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
165
175
|
export async function publishOrchestratorTaskCompletion(bus, job, task, summary) {
|
|
166
176
|
if (!bus)
|
|
167
177
|
return;
|
|
168
|
-
|
|
169
|
-
eventFor(job.id, task.id, task.parentTaskId, 'task.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
await bus.publish(event);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
process.stderr.write(`warning: orchestrator task event unavailable for ${job.id}/${task.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
178
|
-
}
|
|
178
|
+
try {
|
|
179
|
+
await bus.publish(eventFor(job.id, task.id, task.parentTaskId, 'task.completed', 'succeeded', `Task ${task.id} completed by orchestrator`, 'orchestrator', { adapter: 'orchestrator', summary }, { agent: 'relay', component: 'agents-relay' }));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
process.stderr.write(`warning: orchestrator task completion event unavailable for ${job.id}/${task.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
export async function publishDependentUnblocked(bus, targets, reason) { if (!bus)
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { decisionMode, LayaCliDecisionEngine } from './decision.js';
|
|
|
18
18
|
import { discoverAgents } from './registry.js';
|
|
19
19
|
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
20
20
|
import { transitionJobEvent } from './scheduler.js';
|
|
21
|
-
import { applyOrchestratorCompleteAction, applyTaskHoldAction, dispatchTaskAction } from './task-actions.js';
|
|
21
|
+
import { applyOrchestratorStartAction, applyOrchestratorCompleteAction, applyTaskHoldAction, dispatchTaskAction } from './task-actions.js';
|
|
22
22
|
import { applyJobActivateAction, applyJobCancelAction } from './job-actions.js';
|
|
23
23
|
import { resolveExecutionContext } from './execution-context.js';
|
|
24
24
|
import { defaultWorkspaceRoot, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
@@ -30,7 +30,7 @@ import { parseAgentGraph } from './agent-graph.js';
|
|
|
30
30
|
import { runJobUnblockCommand, runTaskUnblockCommand } from './cli-unblock.js';
|
|
31
31
|
import { createJobEntity, createTaskEntity } from './workflow/entity-creation.js';
|
|
32
32
|
export { helpText } from './cli-help.js';
|
|
33
|
-
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, publishDependentUnblocked, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayTaskCommand, withEventBus
|
|
33
|
+
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, publishDependentUnblocked, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayTaskCommand, withEventBus } from './cli-support.js';
|
|
34
34
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
35
35
|
export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
|
|
36
36
|
if (!options.id)
|
|
@@ -157,10 +157,10 @@ async function runManagedTaskCommand(action, args) {
|
|
|
157
157
|
console.log(JSON.stringify(task, null, 2));
|
|
158
158
|
}
|
|
159
159
|
async function runTaskCommand(action, args) {
|
|
160
|
-
if (!['create', 'update', 'show', 'list', 'complete', 'retry', 'unblock', 'cancel'].includes(action))
|
|
160
|
+
if (!['create', 'update', 'show', 'list', 'start', 'complete', 'retry', 'unblock', 'cancel'].includes(action))
|
|
161
161
|
throw new Error(usage);
|
|
162
162
|
const managed = Boolean(arg(args, '--repo') && arg(args, '--pr') && !arg(args, '--file'));
|
|
163
|
-
if (managed && ['create', 'complete', 'retry', 'cancel'].includes(action)) {
|
|
163
|
+
if (managed && ['create', 'start', 'complete', 'retry', 'cancel'].includes(action)) {
|
|
164
164
|
await runManagedTaskCommand(action, args);
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
@@ -201,6 +201,13 @@ async function runTaskCommand(action, args) {
|
|
|
201
201
|
await runTaskUnblockCommand(args, store, job, task, localFile);
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
|
+
if (action === 'start') {
|
|
205
|
+
await applyOrchestratorStartAction(task);
|
|
206
|
+
await store.saveTask(task);
|
|
207
|
+
await saveLocal(localFile, await store.load(job.id));
|
|
208
|
+
console.log(JSON.stringify(task, null, 2));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
204
211
|
if (action === 'complete') {
|
|
205
212
|
const summary = arg(args, '--summary');
|
|
206
213
|
if (!summary)
|
package/dist/dashboard.js
CHANGED
|
@@ -280,6 +280,8 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
280
280
|
let result;
|
|
281
281
|
if (body.action === 'create' && body.task && resolvedActions?.taskCreate)
|
|
282
282
|
result = await resolvedActions.taskCreate(requestedJobId, body.task);
|
|
283
|
+
else if (body.action === 'start' && isTaskId(requestedTaskId) && resolvedActions?.taskStart)
|
|
284
|
+
result = await resolvedActions.taskStart(requestedJobId, requestedTaskId);
|
|
283
285
|
else if (body.action === 'retry' && isTaskId(requestedTaskId) && resolvedActions?.taskRetry)
|
|
284
286
|
result = await resolvedActions.taskRetry(requestedJobId, requestedTaskId);
|
|
285
287
|
else if (body.action === 'cancel' && isTaskId(requestedTaskId) && resolvedActions?.taskCancel)
|
|
@@ -4,10 +4,9 @@ import { implementationComplete, implementationTasks } from './runtime-status.js
|
|
|
4
4
|
import { eventFor } from './events.js';
|
|
5
5
|
import { workerCandidates } from './worker-routing.js';
|
|
6
6
|
import { routeExecution } from './execution-routing.js';
|
|
7
|
-
import { ReconcilerRuntimeBase
|
|
7
|
+
import { ReconcilerRuntimeBase } from './reconciler-runtime.js';
|
|
8
8
|
import { dispatchReviewLifecycle, reviewOutcome } from './workflow/review-v2.js';
|
|
9
9
|
import { createTaskEntity } from './workflow/entity-creation.js';
|
|
10
|
-
const taskRouteClaims = new Set();
|
|
11
10
|
export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
12
11
|
async currentHeadSha(job) { return await this.store.headSha?.() ?? job.headSha ?? job.review?.headSha ?? null; }
|
|
13
12
|
reviewFacts(job, prState, headSha) {
|
|
@@ -159,72 +158,62 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
159
158
|
reason: 'worker-router',
|
|
160
159
|
};
|
|
161
160
|
}
|
|
162
|
-
async
|
|
161
|
+
async routeTaskForExecution(job, task) {
|
|
162
|
+
if (task.routing)
|
|
163
|
+
return { task, routed: false };
|
|
164
|
+
if (!task.output || task.adapter === 'orchestrator')
|
|
165
|
+
return { task, routed: false };
|
|
163
166
|
const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
|
|
164
167
|
const registered = await this.store.listAgents();
|
|
165
168
|
const candidates = workerCandidates(this.options.adapters, registered, usage);
|
|
166
169
|
const runtimeSummary = this.options.runtimeSummaryProvider?.();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
task.workerSelection = routed.selection;
|
|
215
|
-
task.adapter = routed.candidate.adapter;
|
|
216
|
-
task.routing = routed.candidate.routing;
|
|
217
|
-
task.timeoutMs = task.timeoutMs || taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
|
|
218
|
-
task.updatedAt = new Date().toISOString();
|
|
219
|
-
await this.store.saveTask(task);
|
|
220
|
-
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'worker.selected', 'queued', `Worker ${routed.selection.workerId} selected for task ${task.id}`, 'user', { workerSelection: routed.selection, workerRequirements: requirements }, { agent: 'relay', component: 'agents-relay' }));
|
|
221
|
-
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'model.selected', 'queued', `Selected ${task.routing.model} · thinking ${task.routing.reasoning ?? 'unknown'}`, 'user', { routing: task.routing }, { agent: 'relay', component: 'agents-relay' }));
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
finally {
|
|
225
|
-
taskRouteClaims.delete(claimKey);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
170
|
+
const requirements = task.workerRequirements ?? { capabilities: [] };
|
|
171
|
+
const taskFacts = {
|
|
172
|
+
job: {
|
|
173
|
+
id: job.id, title: job.title, objective: job.objective ?? null,
|
|
174
|
+
description: job.description ?? null, priority: job.priority ?? null,
|
|
175
|
+
executionMode: job.executionMode, repository: job.repository,
|
|
176
|
+
prNumber: job.prNumber, state: job.state,
|
|
177
|
+
},
|
|
178
|
+
task: {
|
|
179
|
+
id: task.id, kind: task.kind ?? 'work', storySize: task.storySize ?? 'M',
|
|
180
|
+
priority: task.priority ?? null, agentName: task.agentName ?? null,
|
|
181
|
+
parentTaskId: task.parentTaskId, dependencies: task.dependencies,
|
|
182
|
+
capabilities: task.capabilities, input: task.input,
|
|
183
|
+
attachments: task.attachments ?? [], output: task.output ?? null,
|
|
184
|
+
state: task.state, attempt: task.attempt, maxAttempts: task.maxAttempts,
|
|
185
|
+
},
|
|
186
|
+
storySize: task.storySize ?? 'M',
|
|
187
|
+
};
|
|
188
|
+
const routingConstraints = undefined;
|
|
189
|
+
const parent = task.agentName === 'troubleshooter' && task.parentTaskId
|
|
190
|
+
? job.tasks.find(item => item.id === task.parentTaskId)
|
|
191
|
+
: undefined;
|
|
192
|
+
const parentRouteFailedBeforeStart = Boolean(parent?.routing && /exited before publishing task\.started|worker_exit_before_start|launch failed/i.test(parent.error ?? parent.failureReason ?? ''));
|
|
193
|
+
const excludedRoutes = parentRouteFailedBeforeStart && parent?.routing
|
|
194
|
+
? [{ provider: parent.routing.provider, model: parent.routing.model, profile: parent.routing.profile }]
|
|
195
|
+
: [];
|
|
196
|
+
const routed = await routeExecution(candidates, requirements, {
|
|
197
|
+
decisionEngine: this.options.decisionEngine,
|
|
198
|
+
decisionMode: this.options.decisionMode,
|
|
199
|
+
taskFacts: { ...taskFacts, recovery_parent_failed_route: excludedRoutes[0] ?? null },
|
|
200
|
+
usage,
|
|
201
|
+
registered,
|
|
202
|
+
chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
|
|
203
|
+
routingConstraints,
|
|
204
|
+
excludedRoutes,
|
|
205
|
+
auditStore: this.options.decisionAuditStore,
|
|
206
|
+
auditTask: task,
|
|
207
|
+
});
|
|
208
|
+
task.workerSelection = routed.selection;
|
|
209
|
+
task.adapter = routed.candidate.adapter;
|
|
210
|
+
task.routing = routed.candidate.routing;
|
|
211
|
+
task.timeoutMs = task.timeoutMs || taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
|
|
212
|
+
task.updatedAt = new Date().toISOString();
|
|
213
|
+
await this.store.saveTask(task);
|
|
214
|
+
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'worker.selected', 'queued', 'Worker ' + routed.selection.workerId + ' selected for task ' + task.id, 'user', { workerSelection: routed.selection, workerRequirements: requirements }, { agent: 'relay', component: 'agents-relay' }));
|
|
215
|
+
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'model.selected', 'queued', 'Selected ' + task.routing.model + ' · thinking ' + (task.routing.reasoning ?? 'unknown'), 'user', { routing: task.routing }, { agent: 'relay', component: 'agents-relay' }));
|
|
216
|
+
return { task, routed: true };
|
|
228
217
|
}
|
|
229
218
|
async applyReviewEvent(event) {
|
|
230
219
|
const job = await this.store.load(event.job_id);
|
package/dist/reconciler.js
CHANGED
|
@@ -100,7 +100,6 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
100
100
|
}, context => `effect.mergePullRequest:${context.jobId}:${String(context.data?.headSha ?? 'missing')}`));
|
|
101
101
|
}
|
|
102
102
|
async dispatchExecutableTasks(jobId) {
|
|
103
|
-
await this.routeExecutableTasks(await this.store.load(jobId));
|
|
104
103
|
const scheduled = schedule(await this.store.load(jobId));
|
|
105
104
|
for (const task of scheduled.tasks.filter(item => item.state === 'SUCCEEDED' && (item.continuation || scheduled.continuation) && !item.continuationDeliveredAt)) {
|
|
106
105
|
this.detach(this.deliverContinuation(scheduled, task), scheduled.id, task.id, 'continuation');
|
|
@@ -111,7 +110,6 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
111
110
|
&& task.executionId === null
|
|
112
111
|
&& task.kind !== 'planner'
|
|
113
112
|
&& task.adapter !== 'orchestrator'
|
|
114
|
-
&& (!['codex', 'chatgpt'].includes(task.adapter) || Boolean(task.routing))
|
|
115
113
|
&& dependencyReady(task, new Map(scheduled.tasks.map(item => [item.id, item]))))
|
|
116
114
|
.sort((a, b) => compareTaskPriority(a, b, scheduled.priority ?? 'P2'))
|
|
117
115
|
.slice(0, capacity);
|
|
@@ -368,13 +366,6 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
368
366
|
return withObservabilityCorrelation({ repository: job.repository, prNumber: job.prNumber, jobId: job.id, taskId: task.id }, () => this.launchCorrelated(job, task));
|
|
369
367
|
}
|
|
370
368
|
async launchCorrelated(job, task) {
|
|
371
|
-
const adapter = this.options.adapters.find(x => x.name === task.adapter);
|
|
372
|
-
if (!adapter) {
|
|
373
|
-
await transitionTaskEvent(task, 'fail');
|
|
374
|
-
task.error = `No adapter ${task.adapter}`;
|
|
375
|
-
await this.store.saveTask(task);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
369
|
if (['codex', 'chatgpt'].includes(task.adapter) && !this.options.eventBus) {
|
|
379
370
|
await transitionTaskEvent(task, 'block');
|
|
380
371
|
task.error = 'Model-backed tasks require an event bus for authoritative lifecycle state';
|
|
@@ -383,11 +374,17 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
383
374
|
}
|
|
384
375
|
const claimedTask = await withExecutionBoundary(job.id, task.id, async () => {
|
|
385
376
|
const freshJob = await this.store.load(job.id);
|
|
386
|
-
|
|
377
|
+
let freshTask = freshJob.tasks.find(item => item.id === task.id);
|
|
387
378
|
if (!freshTask || freshTask.state !== 'QUEUED' || !dependencyReady(freshTask, new Map(freshJob.tasks.map(item => [item.id, item]))))
|
|
388
379
|
return;
|
|
389
380
|
if (freshTask.executionId !== null)
|
|
390
381
|
return;
|
|
382
|
+
if (!freshTask.routing && freshTask.adapter !== 'orchestrator') {
|
|
383
|
+
freshTask = (await this.routeTaskForExecution(freshJob, freshTask)).task;
|
|
384
|
+
}
|
|
385
|
+
if (['codex', 'chatgpt'].includes(freshTask.adapter) && !freshTask.routing) {
|
|
386
|
+
throw new Error('Task ' + freshTask.id + ' requires routing metadata before execution claim');
|
|
387
|
+
}
|
|
391
388
|
const leaseMs = Math.max(this.options.leaseMs ?? 300000, freshTask.timeoutMs);
|
|
392
389
|
const startedAt = (this.options.now ?? new Date()).getTime();
|
|
393
390
|
freshTask.attempt += 1;
|
|
@@ -405,6 +402,11 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
405
402
|
if (!claimedTask)
|
|
406
403
|
return;
|
|
407
404
|
task = claimedTask;
|
|
405
|
+
const adapter = this.options.adapters.find(x => x.name === task.adapter);
|
|
406
|
+
if (!adapter) {
|
|
407
|
+
await this.failLaunch(job.id, task.id, task.executionId, 'No adapter ' + task.adapter);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
408
410
|
let executionContext;
|
|
409
411
|
let managedHead = null;
|
|
410
412
|
try {
|
|
@@ -459,7 +461,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
459
461
|
void (execution.launched ?? Promise.resolve()).then(() => this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.process.launched', 'running', `Task ${task.id} process launched`, 'user', { adapter: adapter.id, model: task.routing?.model }, { agent: 'relay', component: 'agents-relay' }))).catch(error => this.failLaunch(job.id, task.id, durableExecutionId, error instanceof Error ? error.message : String(error), 'launch'));
|
|
460
462
|
if (startAckSetup)
|
|
461
463
|
void startAckSetup.ack.finally(() => { void startAckSetup.stop(); }).catch(() => { });
|
|
462
|
-
this.live.set(task.id, { jobId: job.id, taskId: task.id, executionId: durableExecutionId, execution, controller, launchBarrier:
|
|
464
|
+
this.live.set(task.id, { jobId: job.id, taskId: task.id, executionId: durableExecutionId, execution, controller, launchBarrier: startedAt, workerStarted: false, processSettled: false });
|
|
463
465
|
this.registerExecutionOwner(job.id, task.id, durableExecutionId);
|
|
464
466
|
observability().record({
|
|
465
467
|
stage: 'transition', event: 'task.dispatch', outcome: 'succeeded',
|
package/dist/relayd.js
CHANGED
|
@@ -13,10 +13,10 @@ import { ProjectedDurableStore } from './projected-store.js';
|
|
|
13
13
|
import { discoverLocalRelayStoreSnapshots, relayStorePath, SQLiteRelayStore } from './relay-store.js';
|
|
14
14
|
import { githubAuthContext } from './github-auth.js';
|
|
15
15
|
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
16
|
-
import { eventBusResolutionMessage, publishOrchestratorTaskCompletion, resolveEventBus } from './cli-support.js';
|
|
16
|
+
import { eventBusResolutionMessage, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, resolveEventBus } from './cli-support.js';
|
|
17
17
|
import { eventFor } from './events.js';
|
|
18
18
|
import { applyJobActivateAction } from './job-actions.js';
|
|
19
|
-
import { applyOrchestratorCompleteAction } from './task-actions.js';
|
|
19
|
+
import { applyOrchestratorStartAction, applyOrchestratorCompleteAction } from './task-actions.js';
|
|
20
20
|
import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
21
21
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
22
22
|
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
@@ -308,6 +308,21 @@ export async function runDaemon(argv) {
|
|
|
308
308
|
await pool.reconcileTarget(overview.job.repository, overview.job.prNumber, jobId, 'control.task.create');
|
|
309
309
|
return await store.load(jobId);
|
|
310
310
|
};
|
|
311
|
+
const workspaceTaskStart = async (jobId, taskId) => {
|
|
312
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
313
|
+
if (!overview)
|
|
314
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
315
|
+
const store = runtimeStore(overview.job.repository, overview.job.prNumber);
|
|
316
|
+
const job = await store.load(jobId);
|
|
317
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
318
|
+
if (!task)
|
|
319
|
+
throw new Error(`Task ${taskId} not found`);
|
|
320
|
+
await applyOrchestratorStartAction(task);
|
|
321
|
+
await store.saveTask(task);
|
|
322
|
+
await publishOrchestratorTaskStarted(bus, job, task);
|
|
323
|
+
await pool.reconcileTarget(overview.job.repository, overview.job.prNumber, jobId, 'control.task.start');
|
|
324
|
+
return await store.load(jobId);
|
|
325
|
+
};
|
|
311
326
|
const workspaceTaskComplete = async (jobId, taskId, summary) => {
|
|
312
327
|
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
313
328
|
if (!overview)
|
|
@@ -388,6 +403,7 @@ export async function runDaemon(argv) {
|
|
|
388
403
|
cancel: workspaceJobAction('cancel'),
|
|
389
404
|
jobSetState: workspaceJobSetState,
|
|
390
405
|
taskCreate: workspaceTaskCreate,
|
|
406
|
+
taskStart: workspaceTaskStart,
|
|
391
407
|
taskRetry: workspaceTaskAction('retry'),
|
|
392
408
|
taskCancel: workspaceTaskAction('cancel'),
|
|
393
409
|
taskComplete: workspaceTaskComplete,
|
package/dist/task-actions.js
CHANGED
|
@@ -5,6 +5,7 @@ export const TASK_UNBLOCK_RETRY_ACTION = 'task.unblock.retry';
|
|
|
5
5
|
export const TASK_CANCEL_ACTION = 'task.cancel';
|
|
6
6
|
export const TASK_RELEASE_ACTION = 'task.release';
|
|
7
7
|
export const TASK_HOLD_ACTION = 'task.hold';
|
|
8
|
+
export const TASK_START_ACTION = 'task.start';
|
|
8
9
|
export const TASK_COMPLETE_ACTION = 'task.complete';
|
|
9
10
|
export const TASK_QUEUE_ACTION = 'task.queue';
|
|
10
11
|
function lifecycleRejection(task, action) {
|
|
@@ -79,12 +80,29 @@ export async function applyTaskQueueAction(job, task) {
|
|
|
79
80
|
clearExecution(task);
|
|
80
81
|
return task;
|
|
81
82
|
}
|
|
83
|
+
export async function applyOrchestratorStartAction(task) {
|
|
84
|
+
if (task.adapter !== 'orchestrator')
|
|
85
|
+
throw new Error('task start is only valid for adapter=orchestrator self-tasks');
|
|
86
|
+
const originalState = task.state;
|
|
87
|
+
try {
|
|
88
|
+
await transitionTaskEvent(task, 'task.started');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof Error && error.message.startsWith('No enabled workflow transition')) {
|
|
92
|
+
throw new Error(`Task ${task.id} is ${originalState}; only QUEUED orchestrator tasks can be started`);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
task.attempt = Math.max(task.attempt, 1);
|
|
97
|
+
task.updatedAt = new Date().toISOString();
|
|
98
|
+
return task;
|
|
99
|
+
}
|
|
82
100
|
export async function applyOrchestratorCompleteAction(task, summary) {
|
|
83
101
|
if (task.adapter !== 'orchestrator')
|
|
84
102
|
throw new Error('task complete is only valid for adapter=orchestrator self-tasks');
|
|
85
103
|
const originalState = task.state;
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
if (task.state !== 'RUNNING')
|
|
105
|
+
throw new Error(`Task ${task.id} is ${originalState}; orchestrator task must be started before completion`);
|
|
88
106
|
task.result = { summary };
|
|
89
107
|
clearExecution(task);
|
|
90
108
|
try {
|
|
@@ -92,7 +110,7 @@ export async function applyOrchestratorCompleteAction(task, summary) {
|
|
|
92
110
|
}
|
|
93
111
|
catch (error) {
|
|
94
112
|
if (error instanceof Error && error.message.startsWith('No enabled workflow transition')) {
|
|
95
|
-
throw new Error(`Task ${task.id} is ${originalState}; only
|
|
113
|
+
throw new Error(`Task ${task.id} is ${originalState}; only RUNNING orchestrator tasks can be completed`);
|
|
96
114
|
}
|
|
97
115
|
throw error;
|
|
98
116
|
}
|
|
@@ -127,6 +145,8 @@ export async function dispatchTaskAction(store, request) {
|
|
|
127
145
|
await applyTaskReleaseAction(task);
|
|
128
146
|
else if (request.id === TASK_HOLD_ACTION)
|
|
129
147
|
await applyTaskHoldAction(task);
|
|
148
|
+
else if (request.id === TASK_START_ACTION)
|
|
149
|
+
await applyOrchestratorStartAction(task);
|
|
130
150
|
else if (request.id === TASK_QUEUE_ACTION)
|
|
131
151
|
await applyTaskQueueAction(job, task);
|
|
132
152
|
else
|
package/package.json
CHANGED
|
@@ -32,6 +32,15 @@ If an intermediate result does not need that independent lifecycle, keep it insi
|
|
|
32
32
|
|
|
33
33
|
Keep sibling Tasks at a comparable abstraction level. Do not mix a whole product milestone with tiny implementation steps as peers.
|
|
34
34
|
|
|
35
|
+
### Architecture Lens
|
|
36
|
+
|
|
37
|
+
When the resolved project context exposes a canonical design-principles
|
|
38
|
+
document or architecture contract, identify the principles materially relevant
|
|
39
|
+
to each architecture-sensitive Task. Put only the resulting task-specific
|
|
40
|
+
questions, constraints, or already-decided answers in that Task's input. Do not
|
|
41
|
+
copy the full project principles into every Task. For Agents Relay itself, use
|
|
42
|
+
DESIGN-PRINCIPLES.md and its Architecture Lens.
|
|
43
|
+
|
|
35
44
|
The graph is the explanation for the decomposition: every emitted Task should map to a meaningful boundary in the graph, and important graph boundaries should not be omitted accidentally.
|
|
36
45
|
|
|
37
46
|
### Semantic ownership vs execution flow
|
|
@@ -131,6 +131,14 @@ When success depends on the actual current UI, browser state, store listing, run
|
|
|
131
131
|
|
|
132
132
|
This does not weaken immutable review semantics: final review and specialist review Tasks should continue to declare and inspect one exact immutable target/version.
|
|
133
133
|
|
|
134
|
+
When project context exposes a canonical design-principles or architecture
|
|
135
|
+
contract, Final Review must also verify the reviewed immutable target against
|
|
136
|
+
the principles that materially apply to the Job objective and changed area.
|
|
137
|
+
The reviewer should consume that project context rather than duplicating the
|
|
138
|
+
full architecture contract in the review Task. A second authority path or
|
|
139
|
+
stateful action is a blocking architecture finding when the project contract
|
|
140
|
+
forbids it.
|
|
141
|
+
|
|
134
142
|
## Dependency vs block and authoritative unblock
|
|
135
143
|
|
|
136
144
|
`dependencies` describe normal DAG ordering. Dependency completion does not create an unblock lifecycle event or Planner unblock trigger; Relay simply releases dependent work when its prerequisites succeed. `BLOCKED` is different: it records that execution reached a runtime/external condition that invalidates normal progress.
|