agents-relay 1.0.172 → 1.0.174
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/planner.js +10 -0
- package/dist/pool.js +12 -1
- package/dist/reconciler.js +22 -0
- package/dist/relayd.js +17 -10
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +1 -1
- package/skills/agents-relay/agents/planner.agent.md +12 -0
- package/skills/agents-relay/contracts/task-job-contract.md +55 -0
package/dist/planner.js
CHANGED
|
@@ -5,6 +5,15 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
6
6
|
import { executionContextPrompt } from './execution-context.js';
|
|
7
7
|
import { parseAgentGraph } from './agent-graph.js';
|
|
8
|
+
/** Business-task ownership only; planner rounds remain provenance records. */
|
|
9
|
+
export function semanticTaskTree(tasks) {
|
|
10
|
+
const work = tasks.filter(task => task.kind !== 'planner');
|
|
11
|
+
const ids = new Set(work.map(task => task.id));
|
|
12
|
+
return {
|
|
13
|
+
roots: work.filter(task => !task.parentTaskId || !ids.has(task.parentTaskId)).map(task => task.id),
|
|
14
|
+
tasks: work.map(task => ({ id: task.id, parent_task_id: task.parentTaskId, state: task.state, dependencies: [...task.dependencies], planning_decision_id: task.planningDecisionId, planning_round: task.planningRound, review: task.review })),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
8
17
|
function plannerTrigger(job, manual = false) {
|
|
9
18
|
if (manual)
|
|
10
19
|
return { source: 'manual' };
|
|
@@ -329,6 +338,7 @@ export function plannerInput(context) {
|
|
|
329
338
|
previous_planner_task_id: context.previousPlannerTaskId,
|
|
330
339
|
plan_trigger: context.trigger,
|
|
331
340
|
legacy_change_cycle: context.job.changeCycle,
|
|
341
|
+
semantic_task_tree: semanticTaskTree(context.tasks),
|
|
332
342
|
project: project ? { name: project.name, path: project.path } : undefined,
|
|
333
343
|
visible_agents: visibleAgents ?? [],
|
|
334
344
|
tasks: context.tasks.map(task => ({
|
package/dist/pool.js
CHANGED
|
@@ -176,7 +176,18 @@ export class RepositoryWorkerPool {
|
|
|
176
176
|
capacity -= allowance;
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
async reconcileKnown(discovered) {
|
|
179
|
+
async reconcileKnown(discovered) {
|
|
180
|
+
for (const item of discovered.filter(value => value.githubState === 'OPEN' && value.job.state === 'ACTIVE')) {
|
|
181
|
+
this.jobTargets.set(item.job.id, { repository: item.job.repository, prNumber: item.job.prNumber });
|
|
182
|
+
const store = new GitHubStore(this.client, item.job.repository, item.job.prNumber, this.trustedAuthors);
|
|
183
|
+
const reconciler = this.makeReconciler(store, 0);
|
|
184
|
+
if (typeof reconciler.recoverStartupOrphans === 'function')
|
|
185
|
+
await this.reconcileExclusive(item.job.id, async () => {
|
|
186
|
+
await withGitHubRequestContext(this.client, 'pool.startup-orphan-recovery', () => reconciler.recoverStartupOrphans(item.job.id));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
await this.reconcileDiscovered(discovered);
|
|
190
|
+
}
|
|
180
191
|
async reconcile() {
|
|
181
192
|
if (this.isRateLimited())
|
|
182
193
|
return;
|
package/dist/reconciler.js
CHANGED
|
@@ -214,6 +214,8 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
214
214
|
this.reconciling = false;
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
async recoverStartupOrphans(jobId) { const job = await this.store.load(jobId); if (job.state === 'ACTIVE' && (await this.store.pullRequestState?.() ?? 'OPEN') === 'OPEN')
|
|
218
|
+
await this.recoverOrphanedLocalExecutions(job); return this.store.load(jobId); }
|
|
217
219
|
async cancelActiveTasks(jobId, reason) {
|
|
218
220
|
const job = await this.store.load(jobId);
|
|
219
221
|
for (const task of job.tasks.filter(item => !['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(item.state))) {
|
|
@@ -262,6 +264,26 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
262
264
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'lease_expired' }));
|
|
263
265
|
}
|
|
264
266
|
}
|
|
267
|
+
async recoverOrphanedLocalExecutions(job) {
|
|
268
|
+
for (const stale of job.tasks.filter(t => t.state === 'RUNNING' && t.executionId && t.leaseOwner && t.leaseOwner !== this.options.owner && t.adapter !== 'chatgpt')) {
|
|
269
|
+
const task = (await this.store.load(job.id)).tasks.find(t => t.id === stale.id);
|
|
270
|
+
if (!task || task.state !== 'RUNNING' || task.executionId !== stale.executionId || task.leaseOwner !== stale.leaseOwner || task.leaseExpiresAt !== stale.leaseExpiresAt || task.adapter === 'chatgpt' || task.leaseOwner === this.options.owner)
|
|
271
|
+
continue;
|
|
272
|
+
await transitionTaskEvent(task, 'task.failed');
|
|
273
|
+
task.error = 'Orphaned after relayd restart';
|
|
274
|
+
task.leaseOwner = null;
|
|
275
|
+
task.leaseExpiresAt = null;
|
|
276
|
+
task.executionId = null;
|
|
277
|
+
if (task.attempt < task.maxAttempts) {
|
|
278
|
+
await transitionTaskEvent(task, 'retry');
|
|
279
|
+
await this.store.saveTask(task);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
task.error += ' after maximum attempts';
|
|
283
|
+
await this.store.saveTask(task);
|
|
284
|
+
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'startup_orphaned' }));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
265
287
|
async planJob(jobId, guidance) {
|
|
266
288
|
return executeManualPlan(this.store, jobId, guidance, this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), (job, task) => this.launchPlanner(job, task));
|
|
267
289
|
}
|
package/dist/relayd.js
CHANGED
|
@@ -120,7 +120,7 @@ export async function runDaemon(argv) {
|
|
|
120
120
|
cachedOverviews = parsed;
|
|
121
121
|
}
|
|
122
122
|
catch { /* first run or invalid cache */ }
|
|
123
|
-
const refreshAll = async (full = false) => {
|
|
123
|
+
const refreshAll = async (full = false, failOnError = false) => {
|
|
124
124
|
if (client.rateLimitStatus().limited)
|
|
125
125
|
return cachedOverviews;
|
|
126
126
|
const scanRepositories = full ? repositories : recoveryScanRepositories(repositories, cachedOverviews, dormantProbeIndex++);
|
|
@@ -139,6 +139,8 @@ export async function runDaemon(argv) {
|
|
|
139
139
|
return cachedOverviews;
|
|
140
140
|
}
|
|
141
141
|
catch (error) {
|
|
142
|
+
if (failOnError)
|
|
143
|
+
throw error;
|
|
142
144
|
process.stderr.write(`warning: workspace job refresh unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
143
145
|
return cachedOverviews;
|
|
144
146
|
}
|
|
@@ -275,7 +277,6 @@ export async function runDaemon(argv) {
|
|
|
275
277
|
taskCancel: workspaceTaskAction('cancel'),
|
|
276
278
|
taskSetState: workspaceTaskSetState,
|
|
277
279
|
});
|
|
278
|
-
await once(server, 'listening');
|
|
279
280
|
const reconcileSafely = async () => {
|
|
280
281
|
if (client.rateLimitStatus().limited)
|
|
281
282
|
return;
|
|
@@ -301,17 +302,23 @@ export async function runDaemon(argv) {
|
|
|
301
302
|
}
|
|
302
303
|
};
|
|
303
304
|
const watchdogDefault = webhook ? SERVICE_DEFAULTS.webhookWatchdogMs : SERVICE_DEFAULTS.watchdogMs;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
let interval;
|
|
306
|
+
let scheduleInterval;
|
|
307
|
+
const stop = () => { if (interval)
|
|
308
|
+
clearInterval(interval); if (scheduleInterval)
|
|
309
|
+
clearInterval(scheduleInterval); void stopWatch().finally(() => server.close(() => process.exit(0))); };
|
|
309
310
|
process.once('SIGINT', stop);
|
|
310
311
|
process.once('SIGTERM', stop);
|
|
312
|
+
await once(server, 'listening');
|
|
313
|
+
const startupItems = await refreshAll(true, true); // void refreshAll(true)
|
|
314
|
+
await pool.reconcileKnown(startupItems);
|
|
315
|
+
// Startup recovery must finish before normal polling or scheduled work can run.
|
|
316
|
+
await scheduleHeartbeatSafely();
|
|
317
|
+
interval = setInterval(() => void reconcileSafely(), Number(value(argv, '--interval', String(watchdogDefault))));
|
|
318
|
+
interval.unref();
|
|
319
|
+
scheduleInterval = setInterval(() => void scheduleHeartbeatSafely(), SCHEDULE_HEARTBEAT_MS);
|
|
320
|
+
scheduleInterval.unref();
|
|
311
321
|
console.log('Dashboard listening; repository worker pool active');
|
|
312
|
-
void refreshAll(true)
|
|
313
|
-
.then(async (items) => { await scheduleHeartbeatSafely(); await pool.reconcileKnown(items); })
|
|
314
|
-
.catch(error => process.stderr.write(`warning: startup reconciliation unavailable: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
315
322
|
}
|
|
316
323
|
if (isEntrypoint(import.meta.url))
|
|
317
324
|
runDaemon(process.argv.slice(2)).catch(error => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
|
package/package.json
CHANGED
|
@@ -101,7 +101,7 @@ Direct orchestrator-created Tasks do not require a Plan. The orchestrator may cr
|
|
|
101
101
|
|
|
102
102
|
For explicit `plan` and autonomous continuation, Relay invokes the same planner agent as a pure decision function. The planner receives durable Job/Task state, previous planner rounds, and resolved execution context, then returns structured `{ objective_status, assessment, next_tasks }` output. The planner does not call Relay CLI and does not persist Tasks itself.
|
|
103
103
|
|
|
104
|
-
Each successful planner invocation is one real `plan-N` planning round. Relay persists that planner result, then creates
|
|
104
|
+
Each successful planner invocation is one real `plan-N` planning round. Relay persists that planner result, then creates returned work Tasks with provenance back to that planning round. `parentTaskId` expresses semantic ownership among work Tasks; it is not inherited from the Plan and must never point to a `plan-N` record. `dependencies` independently express execution prerequisites. Historical `planner-N` records remain readable. Only planner execution creates planner rounds; Relay never manufactures synthetic `plan-*` records for direct Task creation. In the dashboard, planner rounds remain provenance/grouping metadata, not semantic parents.
|
|
105
105
|
|
|
106
106
|
A zero-task autonomous Job remains idle. After a direct orchestrator-created initial batch completes, Relay may start the first real planner round. Fixed Jobs do not automatically create planner rounds.
|
|
107
107
|
|
|
@@ -34,6 +34,17 @@ Keep sibling Tasks at a comparable abstraction level. Do not mix a whole product
|
|
|
34
34
|
|
|
35
35
|
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
36
|
|
|
37
|
+
### Semantic ownership vs execution flow
|
|
38
|
+
|
|
39
|
+
The planning graph and the durable Task tree are two different views:
|
|
40
|
+
|
|
41
|
+
- The graph and `dependencies` express execution flow and prerequisites.
|
|
42
|
+
- `parentTaskId` expresses semantic ownership of an artifact or milestone.
|
|
43
|
+
- A later planning round must inspect the existing Task tree and attach work to the existing artifact it changes. Do not turn a later round into a new root merely because its work follows an earlier Task.
|
|
44
|
+
- `plan-N` records are planning provenance, not business milestones, and must never be used as `parentTaskId`.
|
|
45
|
+
|
|
46
|
+
For example, produce `architecture-v2` as a root milestone, then place `architecture-review`, `architecture-revision-1`, and `architecture-rereview-1` under it. The revision may depend on the review, and the re-review may depend on the revision; those dependencies do not determine their semantic parent. An independent `graph-runtime-v2` milestone remains a root sibling. A troubleshooter belongs under the failed Task (or its artifact subtree), and specialist review work stays under its review/artifact context.
|
|
47
|
+
|
|
37
48
|
### Examples
|
|
38
49
|
|
|
39
50
|
Prefer:
|
|
@@ -69,6 +80,7 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
69
80
|
- When a Task depends on mutable repository or product state, distinguish a frozen evidence target from the current execution head. A SHA may freeze that Task's own review/source evidence, but do not copy an upstream SHA into a dependent release, deploy, publication, or other follow-on Task unless the objective explicitly requires that same frozen target. After dependencies complete, downstream Tasks must resolve and verify the current durable Job/PR head before acting.
|
|
70
81
|
- When fresh UI, browser, store, runtime, or visual evidence is material to the Task outcome, say so explicitly in the Task input and require evidence from the real current state. Existing screenshots, prepared assets, or prior observations are not substitutes for fresh evidence unless the objective explicitly allows reuse.
|
|
71
82
|
- When a Job needs multi-perspective review, use the visible `review-chair` agent or an Agent Graph to select specialist roles dynamically. Keep specialist Tasks as independent siblings against the same immutable target/version, and leave worker/model/provider routing to Relay.
|
|
83
|
+
- Treat `parentTaskId` as semantic ownership, not execution order. Select an existing non-planner Task as the parent when a later-round revision, re-review, specialist review, or troubleshooting Task belongs to that artifact/milestone; a terminal parent remains valid historical ownership. Use `dependencies` independently for prerequisites, including cross-parent dependencies. Never use a `plan-N` Task as a parent, and never invent a synthetic parent merely to group a planning round.
|
|
72
84
|
- A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
|
|
73
85
|
- Use dependencies only for Task IDs inside the current Job, and only when execution order actually matters. Never encode cross-Job or cross-repository dependencies in `dependencies`.
|
|
74
86
|
- When the current Job is a prerequisite for another durable Job/Task, emit that dependent in top-level `unblocks`. The current blocker Job owns this authoritative reverse relation. Dependent-side `blockedBy` is optional observability metadata and is not the source of truth. Use `resolutionMode: "merge"` when blocker PR merge is sufficient; use `"verified"` when publication, deployment, runtime health, human approval, or another post-merge fact must be verified.
|
|
@@ -22,6 +22,34 @@ For a newly created PR (`job create --head`), standalone objective Markdown is r
|
|
|
22
22
|
Any task producer — orchestrator, planner, or nested task-producing agent — must follow this contract.
|
|
23
23
|
Task producers choose semantic task properties such as objective, dependencies, worker requirements, priority, and `storySize`. They do not normally choose an adapter, provider/model, or raw timeout milliseconds.
|
|
24
24
|
|
|
25
|
+
### Planning graph and semantic Task tree
|
|
26
|
+
|
|
27
|
+
The planning graph expresses execution flow. The Task tree expresses semantic
|
|
28
|
+
ownership. `dependencies` answer which durable outcomes must succeed before a
|
|
29
|
+
Task can run; `parentTaskId` answers which durable artifact, milestone, or
|
|
30
|
+
parent outcome the Task belongs to. These are independent relationships and
|
|
31
|
+
must not be inferred from one another.
|
|
32
|
+
|
|
33
|
+
For a multi-round artifact workflow, keep the business tree compact and
|
|
34
|
+
milestone-oriented:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
Job
|
|
38
|
+
├── Architecture v2
|
|
39
|
+
│ ├── Review
|
|
40
|
+
│ ├── Revision 1
|
|
41
|
+
│ └── Re-review 1
|
|
42
|
+
└── Graph Runtime v2
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The revision may depend on Review and Re-review may depend on Revision 1, but
|
|
46
|
+
both remain semantically owned by Architecture v2. A later Planner round must
|
|
47
|
+
inspect existing Tasks and attach review, change, validation-rerun, release
|
|
48
|
+
correction, or recovery work to the existing artifact subtree. An independent
|
|
49
|
+
milestone is a root sibling. `plan-N` records are planning provenance/history,
|
|
50
|
+
never semantic parents. Existing terminal Tasks are historical evidence and
|
|
51
|
+
must not be rewritten merely to improve the tree.
|
|
52
|
+
|
|
25
53
|
### Task input vs result
|
|
26
54
|
|
|
27
55
|
A Task input records **what is known and what has already been decided before execution**. A Task result records **what execution discovered and what was actually done**.
|
|
@@ -98,6 +126,33 @@ For the ChatGPT Browser Worker guardian, for example, the Job defines `pass ->
|
|
|
98
126
|
|
|
99
127
|
### Optional Review Chair Tasks
|
|
100
128
|
|
|
129
|
+
### Semantic Task tree and planning provenance
|
|
130
|
+
|
|
131
|
+
The planning graph, semantic Task tree, and dependency graph are separate:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
plan-2 (planning provenance only)
|
|
135
|
+
├─ revision (parentTaskId: milestone, dependencies: [review])
|
|
136
|
+
└─ re-review (parentTaskId: milestone, dependencies: [revision])
|
|
137
|
+
independent-milestone (parentTaskId: null, dependencies: [revision])
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`parentTaskId` answers which durable artifact or milestone owns this work. It
|
|
141
|
+
may point to an existing terminal work Task when a later planning round
|
|
142
|
+
continues that artifact, or to another work Task in the same batch. It must
|
|
143
|
+
never point to a `plan-N`/historical `planner-N` record and must not be used
|
|
144
|
+
just to group a round. `dependencies` answer what must finish first and may
|
|
145
|
+
cross semantic subtrees. Reviews, changes-required revisions, re-reviews,
|
|
146
|
+
specialist reviews, and troubleshooter recovery belong under the relevant
|
|
147
|
+
artifact/milestone; independent milestones remain roots. Planning provenance
|
|
148
|
+
is retained separately in `planningDecisionId` and `planningRound`, and
|
|
149
|
+
historical terminal Tasks are immutable.
|
|
150
|
+
|
|
151
|
+
Incorrect: make every generated Task a child of the current `plan-N`, or use
|
|
152
|
+
`parentTaskId` as a substitute for a dependency. Correct: attach a re-review
|
|
153
|
+
to the original milestone, set its dependency to the revision Task, and leave
|
|
154
|
+
the original milestone's terminal record unchanged.
|
|
155
|
+
|
|
101
156
|
Projects may use the shared `review-chair` agent as a normal Task or Agent
|
|
102
157
|
Graph node. The chair discovers specialist agent roles from the resolved
|
|
103
158
|
visible-agent catalog, records selection reasons, and creates sibling durable
|