agents-relay 1.0.130 → 1.0.131
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/blockers.js
CHANGED
|
@@ -11,8 +11,8 @@ export async function registerBlockerDependency(options) {
|
|
|
11
11
|
const task = dependentJob.tasks.find(item => item.id === options.dependentTaskId);
|
|
12
12
|
if (!task)
|
|
13
13
|
throw new Error(`Dependent task ${options.dependentTaskId} not found`);
|
|
14
|
-
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
15
|
-
throw new Error(`Dependent task ${task.id} is not blocked/failed (${task.state})`);
|
|
14
|
+
if (!['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state))
|
|
15
|
+
throw new Error(`Dependent task ${task.id} is not waiting/blocked/failed (${task.state})`);
|
|
16
16
|
const now = options.now ?? new Date().toISOString();
|
|
17
17
|
const relation = {
|
|
18
18
|
repository: dependentJob.repository,
|
|
@@ -48,7 +48,7 @@ export async function registerBlockerDependency(options) {
|
|
|
48
48
|
function obsoleteReason(task, relation) {
|
|
49
49
|
if (!task)
|
|
50
50
|
return 'dependent task no longer exists';
|
|
51
|
-
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
51
|
+
if (!['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state))
|
|
52
52
|
return `dependent task advanced to ${task.state}`;
|
|
53
53
|
if (task.attempt !== relation.blockedAttempt)
|
|
54
54
|
return `dependent task advanced from attempt ${relation.blockedAttempt} to ${task.attempt}`;
|
|
@@ -81,8 +81,7 @@ export async function resolveBlockerDependencies(options) {
|
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
const task = targetJob.tasks.find(item => item.id === relation.taskId);
|
|
84
|
-
const
|
|
85
|
-
const matches = !!task && ['FAILED', 'BLOCKED'].includes(task.state) && task.attempt === relation.blockedAttempt && sameBlocker(task.blockedBy, blockerRef);
|
|
84
|
+
const matches = !!task && ['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state) && task.attempt === relation.blockedAttempt;
|
|
86
85
|
if (!matches) {
|
|
87
86
|
relation.state = 'obsolete';
|
|
88
87
|
relation.resolvedAt = now;
|
|
@@ -91,8 +90,13 @@ export async function resolveBlockerDependencies(options) {
|
|
|
91
90
|
obsolete += 1;
|
|
92
91
|
continue;
|
|
93
92
|
}
|
|
94
|
-
task.
|
|
95
|
-
|
|
93
|
+
if (task.state === 'DRAFT') {
|
|
94
|
+
await transitionTaskEvent(task, 'release');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
task.maxAttempts = extendRetryBudget(task);
|
|
98
|
+
await transitionTaskEvent(task, 'retry');
|
|
99
|
+
}
|
|
96
100
|
task.error = null;
|
|
97
101
|
task.leaseOwner = null;
|
|
98
102
|
task.leaseExpiresAt = null;
|
package/dist/planner.js
CHANGED
|
@@ -129,6 +129,33 @@ export function parsePlannerResult(value) {
|
|
|
129
129
|
throw new Error('Planner assessment must be a string');
|
|
130
130
|
if (!Array.isArray(root.next_tasks))
|
|
131
131
|
throw new Error('Planner next_tasks must be an array');
|
|
132
|
+
const unblockKeys = new Set();
|
|
133
|
+
const unblocksRaw = root.unblocks === undefined ? [] : root.unblocks;
|
|
134
|
+
if (!Array.isArray(unblocksRaw))
|
|
135
|
+
throw new Error('Planner unblocks must be an array when provided');
|
|
136
|
+
const unblocks = unblocksRaw.map((item, index) => {
|
|
137
|
+
const unblock = record(item, `unblocks[${index}]`);
|
|
138
|
+
const repository = optionalString(unblock.repository, `unblocks[${index}].repository`);
|
|
139
|
+
const jobId = optionalString(unblock.jobId, `unblocks[${index}].jobId`);
|
|
140
|
+
const taskId = optionalString(unblock.taskId, `unblocks[${index}].taskId`);
|
|
141
|
+
if (!repository || !/^[^/\s]+\/[^/\s]+$/.test(repository))
|
|
142
|
+
throw new Error(`Planner unblocks[${index}].repository must be OWNER/REPO`);
|
|
143
|
+
if (!jobId)
|
|
144
|
+
throw new Error(`Planner unblocks[${index}].jobId is required`);
|
|
145
|
+
if (!taskId)
|
|
146
|
+
throw new Error(`Planner unblocks[${index}].taskId is required`);
|
|
147
|
+
const prNumber = unblock.prNumber;
|
|
148
|
+
if (!Number.isInteger(prNumber) || Number(prNumber) <= 0)
|
|
149
|
+
throw new Error(`Planner unblocks[${index}].prNumber must be a positive integer`);
|
|
150
|
+
const resolutionMode = unblock.resolutionMode === undefined ? undefined : String(unblock.resolutionMode);
|
|
151
|
+
if (resolutionMode !== undefined && resolutionMode !== 'merge' && resolutionMode !== 'verified')
|
|
152
|
+
throw new Error(`Planner unblocks[${index}].resolutionMode must be merge or verified`);
|
|
153
|
+
const key = `${repository}#${prNumber}:${jobId}:${taskId}`;
|
|
154
|
+
if (unblockKeys.has(key))
|
|
155
|
+
throw new Error(`Planner unblocks contains duplicate dependent ${key}`);
|
|
156
|
+
unblockKeys.add(key);
|
|
157
|
+
return { repository, prNumber: Number(prNumber), jobId, taskId, resolutionMode: resolutionMode };
|
|
158
|
+
});
|
|
132
159
|
const ids = new Set();
|
|
133
160
|
const next_tasks = root.next_tasks.map((item, index) => {
|
|
134
161
|
const task = record(item, `next_tasks[${index}]`);
|
|
@@ -169,7 +196,7 @@ export function parsePlannerResult(value) {
|
|
|
169
196
|
continuation: task.continuation, maxAttempts, timeoutMs
|
|
170
197
|
};
|
|
171
198
|
});
|
|
172
|
-
return { objective_status: status, assessment: root.assessment, next_tasks };
|
|
199
|
+
return { objective_status: status, assessment: root.assessment, next_tasks, unblocks };
|
|
173
200
|
}
|
|
174
201
|
export function parsePlannerOutput(output) {
|
|
175
202
|
const trimmed = output.trim();
|
package/dist/reconciler.js
CHANGED
|
@@ -491,6 +491,10 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
491
491
|
try {
|
|
492
492
|
const result = parsePlannerResult(raw);
|
|
493
493
|
const executionContext = await this.plannerContext(job, task);
|
|
494
|
+
if (result.unblocks.length > 0 && !this.store.registerBlockerDependent)
|
|
495
|
+
throw new Error('Durable store does not support Planner cross-Job unblock registration');
|
|
496
|
+
for (const unblock of result.unblocks)
|
|
497
|
+
await this.store.registerBlockerDependent?.(job.id, unblock);
|
|
494
498
|
const children = result.next_tasks.map(spec => this.plannerChild(job, task, spec, executionContext));
|
|
495
499
|
for (const child of children) {
|
|
496
500
|
const existing = job.tasks.find(item => item.id === child.id);
|
|
@@ -499,7 +503,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
499
503
|
}
|
|
500
504
|
// Persist the completed decision before exposing executable children.
|
|
501
505
|
task.plannerResult = result;
|
|
502
|
-
task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks } };
|
|
506
|
+
task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks, unblocks: result.unblocks } };
|
|
503
507
|
task.error = null;
|
|
504
508
|
task.plannerRetryFeedback = undefined;
|
|
505
509
|
task.leaseOwner = null;
|
package/dist/store-github.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobBody, renderTaskComment, JOB_MARKER } from './markers.js';
|
|
2
2
|
import { isTaskForJob, sameTaskDefinition } from './store-task-definition.js';
|
|
3
|
-
import { resolveBlockerDependencies } from './blockers.js';
|
|
3
|
+
import { registerBlockerDependency, resolveBlockerDependencies } from './blockers.js';
|
|
4
4
|
const TASK_WRITE_OVERLAY = new Map();
|
|
5
5
|
export class GitHubStore {
|
|
6
6
|
client;
|
|
@@ -118,6 +118,16 @@ export class GitHubStore {
|
|
|
118
118
|
return ref && sha ? { ref, sha } : null;
|
|
119
119
|
}
|
|
120
120
|
async headSha() { return (await this.pullRequestHead())?.sha ?? null; }
|
|
121
|
+
async registerBlockerDependent(jobId, dependent) {
|
|
122
|
+
await registerBlockerDependency({
|
|
123
|
+
blockerStore: this,
|
|
124
|
+
blockerJobId: jobId,
|
|
125
|
+
dependentStore: new GitHubStore(this.client, dependent.repository, dependent.prNumber, this.trustedAuthors),
|
|
126
|
+
dependentJobId: dependent.jobId,
|
|
127
|
+
dependentTaskId: dependent.taskId,
|
|
128
|
+
resolutionMode: dependent.resolutionMode,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
121
131
|
async resolveBlockerDependents(jobId, resolution) {
|
|
122
132
|
return await resolveBlockerDependencies({ blockerStore: this, blockerJobId: jobId, resolution, dependentStore: (repository, prNumber) => new GitHubStore(this.client, repository, prNumber, this.trustedAuthors) });
|
|
123
133
|
}
|
package/package.json
CHANGED
|
@@ -67,7 +67,8 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
67
67
|
- Do not repeat completed work or recreate an existing Task unless replacement work is explicitly needed.
|
|
68
68
|
- Prefer independently verifiable Tasks with stable IDs, but do not split work solely to make Tasks smaller.
|
|
69
69
|
- A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
|
|
70
|
-
- Use dependencies only when execution order actually matters.
|
|
70
|
+
- 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`.
|
|
71
|
+
- 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.
|
|
71
72
|
- Describe each child Task precisely enough for worker-router to decide from the Task itself. Do not invent Codex/ChatGPT capability labels. workerRequirements is optional and should normally be omitted; use an explicit runtime only when the objective already requires that runtime.
|
|
72
73
|
- Do not choose codex/chatgpt, provider/model, or usage probes.
|
|
73
74
|
- Every child Task must declare exactly one output: `task_pr` or `file`.
|
|
@@ -77,7 +78,7 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
77
78
|
|
|
78
79
|
## Output
|
|
79
80
|
|
|
80
|
-
Return exactly one JSON object with `objective_status`, `assessment`, and `
|
|
81
|
+
Return exactly one JSON object with `objective_status`, `assessment`, `next_tasks`, and optional top-level `unblocks`.
|
|
81
82
|
|
|
82
83
|
For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
|
|
83
84
|
|
|
@@ -86,7 +87,7 @@ For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
|
|
|
86
87
|
|
|
87
88
|
For `objective_status: "satisfied"`, `assessment` may be a concise evidence-based completion assessment without a graph.
|
|
88
89
|
|
|
89
|
-
Each item in next_tasks contains id, input, output, and storySize. It may also contain priority, projectName, agentName, dependencies, capabilities, workerRequirements, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
|
|
90
|
+
Each item in next_tasks contains id, input, output, and storySize. `unblocks`, when present, is an array of `{repository, prNumber, jobId, taskId, resolutionMode?}` records identifying Tasks that must remain waiting until the current Job resolves their blocker. It may also contain priority, projectName, agentName, dependencies, capabilities, workerRequirements, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
|
|
90
91
|
|
|
91
92
|
### Exact `next_tasks` serialization
|
|
92
93
|
|
|
@@ -105,6 +106,7 @@ A valid non-satisfied response:
|
|
|
105
106
|
{
|
|
106
107
|
"objective_status": "in_progress",
|
|
107
108
|
"assessment": "```mermaid\nflowchart LR\n A[Evidence] --> B[Content package]\n```\nOne daily package is the durable Task boundary.",
|
|
109
|
+
"unblocks": [{"repository":"org/app","prNumber":42,"jobId":"dependent-job","taskId":"consume-output","resolutionMode":"merge"}],
|
|
108
110
|
"next_tasks": [
|
|
109
111
|
{
|
|
110
112
|
"id": "episode-2026-09-26",
|
|
@@ -101,9 +101,9 @@ A Task with `adapter: orchestrator` is synchronous self-work owned by the exact
|
|
|
101
101
|
|
|
102
102
|
## Cross-Job blocker dependencies
|
|
103
103
|
|
|
104
|
-
Task `dependencies` remain local to one Job and must contain only Task IDs from that Job. Shared runtime/tool/platform blockers use a separate durable cross-Job relation. The blocker Job owns reverse `unblocks` records identifying dependent repository, PR, Job, Task, and blocked attempt. The dependent Task may carry a lightweight `blockedBy` reference for observability and deduplication.
|
|
104
|
+
Task `dependencies` remain local to one Job and must contain only Task IDs from that Job. Shared runtime/tool/platform blockers use a separate durable cross-Job relation. The blocker Job owns reverse `unblocks` records identifying dependent repository, PR, Job, Task, and blocked attempt. The dependent Task may carry a lightweight `blockedBy` reference for observability and deduplication. Planner output follows the same ownership rule: when the current Job is the prerequisite, Planner declares dependents through top-level `unblocks`; Relay persists those declarations on the blocker Job via the blocker subsystem. Planner must never encode cross-Job dependencies as local `dependencies`. A dependent may be registered while its Task is still `DRAFT`, and blocker resolution releases that Task when the blocker resolves.
|
|
105
105
|
|
|
106
|
-
Blocker resolution is generation-safe and idempotent: Relay
|
|
106
|
+
Blocker resolution is generation-safe and idempotent: Relay releases a dependent only when it is still DRAFT/FAILED/BLOCKED on the recorded attempt. The blocker-side `unblocks` record is authoritative; dependent-side `blockedBy` may be absent. Otherwise the relation becomes obsolete. Merge-mode blockers resolve when the blocker PR is observed merged. Blockers requiring package publication, deployment, runtime health, human/external verification, or another post-merge fact use verified mode and resolve only through the explicit verified-resolution path. Existing failure evidence is preserved; DRAFT dependents use the normal release transition and FAILED/BLOCKED dependents use the normal retry transition to QUEUED.
|
|
107
107
|
|
|
108
108
|
## Worker lifecycle
|
|
109
109
|
|