agents-relay 2.0.168 → 2.0.170

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.
Files changed (73) hide show
  1. package/README.md +13 -19
  2. package/dist/adapters.js +3 -9
  3. package/dist/blockers.js +3 -2
  4. package/dist/cli-help.js +3 -3
  5. package/dist/cli-job-create.js +2 -4
  6. package/dist/cli-support.js +5 -16
  7. package/dist/cli.js +9 -11
  8. package/dist/dashboard-html.js +5 -9
  9. package/dist/dashboard-style.js +0 -1
  10. package/dist/execution-recovery.js +1 -1
  11. package/dist/force-restate-validation.js +11 -1
  12. package/dist/interactive-resume.js +1 -1
  13. package/dist/job-actions.js +1 -1
  14. package/dist/job-pause.js +1 -1
  15. package/dist/legacy-blocked-task-migration.js +1 -1
  16. package/dist/local-authority-store.js +0 -57
  17. package/dist/local-git-merge.js +1 -2
  18. package/dist/local-merge-delivery.js +1 -1
  19. package/dist/markers.js +5 -6
  20. package/dist/planned-review-head.js +2 -2
  21. package/dist/planner-runtime.js +3 -2
  22. package/dist/planner.js +2 -7
  23. package/dist/pool.js +7 -36
  24. package/dist/reconciler-review.js +4 -5
  25. package/dist/reconciler.js +11 -36
  26. package/dist/relay-create.js +3 -3
  27. package/dist/relay-job-snapshot-reads.js +6 -2
  28. package/dist/relay-store.js +2 -2
  29. package/dist/relay-workflow-operations.js +2 -2
  30. package/dist/relayd-control.js +9 -7
  31. package/dist/relayd-utilities.js +0 -4
  32. package/dist/relayd.js +7 -24
  33. package/dist/runtime-status.js +0 -19
  34. package/dist/single-job-task-control.js +1 -1
  35. package/dist/store.js +1 -1
  36. package/dist/task-actions.js +3 -3
  37. package/dist/task-recovery.js +2 -2
  38. package/dist/task-transitions.js +61 -0
  39. package/dist/troubleshooter-fallback.js +1 -1
  40. package/dist/troubleshooter.js +1 -4
  41. package/dist/types.js +1 -2
  42. package/dist/unblock.js +5 -9
  43. package/dist/worker-rejection.js +1 -1
  44. package/dist/workflow/authoritative-conditions.js +1 -1
  45. package/dist/workflow/blocker-resolution-v2.js +2 -4
  46. package/dist/workflow/contracts.js +0 -1
  47. package/dist/workflow/definitions-v2.js +3 -6
  48. package/dist/workflow/durable-transition.js +1 -1
  49. package/dist/workflow/force-restate-validation-v2.js +2 -1
  50. package/dist/workflow/orchestrator-rescue.js +1 -1
  51. package/dist/workflow/planner-orchestration-v2.js +2 -5
  52. package/dist/workflow/planner-trigger-v2.js +1 -2
  53. package/dist/workflow/review-v2.js +0 -1
  54. package/dist/workflow/task-reconcile.js +1 -7
  55. package/dist/workflow-effect-failures.js +1 -1
  56. package/dist/workspace.js +1 -1
  57. package/package.json +3 -4
  58. package/skills/agents-relay/SKILL.md +6 -6
  59. package/skills/agents-relay/agents/planner.agent.md +3 -3
  60. package/skills/agents-relay/contracts/orchestrator-workflows.md +2 -2
  61. package/skills/agents-relay/contracts/task-job-contract.md +5 -5
  62. package/workflow/blocker-resolution.mmd +1 -7
  63. package/workflow/force-restate-validation.mmd +7 -2
  64. package/workflow/planner-orchestration.mmd +2 -5
  65. package/workflow/planner-trigger.mmd +2 -9
  66. package/workflow/review-lifecycle.mmd +2 -8
  67. package/dist/e2e-injection.js +0 -76
  68. package/dist/scheduled-jobs.js +0 -67
  69. package/dist/scheduler.js +0 -214
  70. package/dist/workflow/schedule-cycle-v2.js +0 -25
  71. package/dist/workflow/scheduled-run-v2.js +0 -30
  72. package/workflow/schedule-cycle.mmd +0 -23
  73. package/workflow/scheduled-run.mmd +0 -59
package/dist/scheduler.js DELETED
@@ -1,214 +0,0 @@
1
- import { implementationTasks } from './runtime-status.js';
2
- import { dispatchJobLifecycle, dispatchTaskLifecycle, dispatchTaskLifecycleSync } from './workflow/lifecycle.js';
3
- import { reconcileTaskDependencies } from './workflow/task-reconcile.js';
4
- export { dependencyReady, detectCycles } from './workflow/task-reconcile.js';
5
- import { dispatchScheduleCycle } from './workflow/schedule-cycle-v2.js';
6
- import { observability } from './observability.js';
7
- export function completionGateSatisfied(job) {
8
- if (job.schedule)
9
- return false;
10
- if (job.executionMode !== 'autonomous')
11
- return true;
12
- const latest = [...(job.plans ?? [])].sort((a, b) => a.round - b.round).at(-1);
13
- const previousReviewFinished = job.tasks.some(task => task.agentName === 'reviewer' && task.state === 'SUCCEEDED');
14
- const planSatisfied = latest?.status === 'SUCCEEDED' && (latest.objectiveStatus === 'satisfied'
15
- || (latest.objectiveStatus === 'in_progress' && latest.plannerResult?.next_tasks?.length === 0 && previousReviewFinished));
16
- const legacySatisfied = (job.plans?.length ?? 0) === 0 && job.tasks.some(task => task.kind === 'planner' && task.state === 'SUCCEEDED' && task.plannerResult?.objective_status === 'satisfied');
17
- return (planSatisfied || legacySatisfied) && implementationTasks(job).every(task => task.state === 'SUCCEEDED');
18
- }
19
- export function schedule(job) {
20
- if (job.pause)
21
- return job;
22
- reconcileTaskDependencies(job);
23
- const scopedTasks = job.schedule?.currentRunSlot
24
- ? job.tasks.filter(task => Date.parse(task.createdAt) >= Date.parse(job.schedule?.currentRunSlot ?? ''))
25
- : job.tasks;
26
- if (job.schedule) {
27
- const slot = Date.parse(job.schedule.currentRunSlot ?? '');
28
- const scopedPlans = (job.plans ?? []).filter(plan => !Number.isFinite(slot) || Date.parse(plan.createdAt) >= slot).sort((a, b) => a.round - b.round || a.createdAt.localeCompare(b.createdAt));
29
- const latestPlan = scopedPlans.at(-1);
30
- const legacyLatest = scopedPlans.length === 0 ? scopedTasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)).at(-1) : undefined;
31
- dispatchScheduleCycle({
32
- hasCurrentSlot: Boolean(job.schedule.currentRunSlot),
33
- plannerSatisfied: latestPlan ? latestPlan.status === 'SUCCEEDED' && latestPlan.objectiveStatus === 'satisfied' : legacyLatest?.state === 'SUCCEEDED' && legacyLatest.plannerResult?.objective_status === 'satisfied',
34
- }, () => {
35
- const slot = job.schedule.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
36
- const lastRun = job.schedule.lastRunAt ? Date.parse(job.schedule.lastRunAt) : Number.NaN;
37
- if (Number.isFinite(slot) && (!Number.isFinite(lastRun) || lastRun < slot))
38
- job.schedule.lastRunAt = new Date().toISOString();
39
- job.schedule.currentRunSlot = null;
40
- job.updatedAt = new Date().toISOString();
41
- });
42
- }
43
- return job;
44
- }
45
- export function extendRetryBudget(task, windowSize = 3) {
46
- if (!Number.isInteger(windowSize) || windowSize < 1)
47
- throw new Error('Retry window size must be a positive integer');
48
- const usedOrBudgeted = Math.max(task.maxAttempts, task.attempt);
49
- return Math.max(windowSize, (Math.floor(usedOrBudgeted / windowSize) + 1) * windowSize);
50
- }
51
- export function tryTransitionTaskEvent(task, event, context = {}) {
52
- return dispatchTaskLifecycleSync(task, event, context);
53
- }
54
- export async function transitionTaskEvent(task, event, context = {}, executeEffect, persist) {
55
- const from = task.state;
56
- const decision = await dispatchTaskLifecycle(task, event, context, executeEffect, persist);
57
- if (!decision.accepted || !decision.target)
58
- throw new Error(decision.reason === 'ambiguous'
59
- ? `Ambiguous workflow transition from ${from} on ${event}`
60
- : `No enabled workflow transition from ${from} on ${event}`);
61
- observability().record({
62
- stage: 'transition',
63
- event,
64
- outcome: 'succeeded',
65
- message: `Task ${task.id} ${from} -> ${decision.target}`,
66
- correlation: { jobId: task.jobId, taskId: task.id, executionId: task.executionId ?? undefined },
67
- transition: { graph: 'task-lifecycle', from, to: decision.target, actions: decision.actions },
68
- });
69
- return decision;
70
- }
71
- export async function transitionJobEvent(job, event, context = {}, executeEffect, persist) {
72
- const from = job.state;
73
- const decision = await dispatchJobLifecycle(job, event, context, executeEffect, persist);
74
- if (!decision.accepted || !decision.target)
75
- throw new Error(decision.reason === 'ambiguous'
76
- ? `Ambiguous workflow transition from ${from} on ${event}`
77
- : `No enabled workflow transition from ${from} on ${event}`);
78
- observability().record({
79
- stage: 'transition',
80
- event,
81
- outcome: 'succeeded',
82
- message: `Job ${job.id} ${from} -> ${decision.target}`,
83
- correlation: { repository: job.repository, prNumber: job.prNumber, jobId: job.id },
84
- transition: { graph: 'job-lifecycle', from, to: decision.target, actions: decision.actions },
85
- });
86
- return decision;
87
- }
88
- export const SCHEDULE_RESOLUTION_MS = 600_000;
89
- const CRON_PART = /^(?:\*|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/;
90
- function cronField(raw, name, minimum, maximum) {
91
- const values = new Set();
92
- for (const part of raw.split(',')) {
93
- const match = CRON_PART.exec(part);
94
- if (!match)
95
- throw new Error(`Invalid schedule ${name}: ${part}`);
96
- const [whole, firstText, lastText, stepText] = match;
97
- if (whole === undefined)
98
- continue;
99
- const step = stepText === undefined ? 1 : Number(stepText);
100
- if (!Number.isInteger(step) || step < 1)
101
- throw new Error(`Invalid schedule ${name} step: ${part}`);
102
- let start = minimum;
103
- let end = maximum;
104
- if (firstText !== undefined) {
105
- start = Number(firstText);
106
- end = lastText === undefined ? (stepText === undefined ? start : maximum) : Number(lastText);
107
- if (start < minimum || end > maximum || start > end)
108
- throw new Error(`Invalid schedule ${name} range: ${part}`);
109
- }
110
- for (let value = start; value <= end; value += step) {
111
- if (value > maximum)
112
- break;
113
- values.add(name === 'weekday' && value === 7 ? 0 : value);
114
- }
115
- }
116
- if (values.size === 0)
117
- throw new Error(`Invalid schedule ${name}: ${raw}`);
118
- return values;
119
- }
120
- export function parseCron(cron) {
121
- const fields = cron.trim().split(/\s+/);
122
- if (fields.length !== 5)
123
- throw new Error('Schedule cron must contain exactly five fields');
124
- const parsed = {
125
- minute: cronField(fields[0], 'minute', 0, 59),
126
- hour: cronField(fields[1], 'hour', 0, 23),
127
- dayOfMonth: cronField(fields[2], 'day-of-month', 1, 31),
128
- month: cronField(fields[3], 'month', 1, 12),
129
- weekday: cronField(fields[4], 'weekday', 0, 7),
130
- };
131
- for (const minute of parsed.minute)
132
- if (minute % (SCHEDULE_RESOLUTION_MS / 60_000) !== 0)
133
- throw new Error(`Schedule minute ${minute} is finer than the 10-minute scheduler resolution`);
134
- return parsed;
135
- }
136
- function timezoneParts(time, timeZone) {
137
- const parts = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', weekday: 'short', hourCycle: 'h23' }).formatToParts(time);
138
- const value = (type) => parts.find(part => part.type === type)?.value ?? '';
139
- const weekdays = new Map([['Sun', 0], ['Mon', 1], ['Tue', 2], ['Wed', 3], ['Thu', 4], ['Fri', 5], ['Sat', 6]]);
140
- const weekday = weekdays.get(value('weekday'));
141
- if (weekday === undefined)
142
- throw new Error(`Unable to resolve timezone weekday ${timeZone}`);
143
- return { year: Number(value('year')), month: Number(value('month')), day: Number(value('day')), hour: Number(value('hour')), minute: Number(value('minute')), weekday };
144
- }
145
- function cronMatches(fields, time, timeZone) {
146
- const local = timezoneParts(time, timeZone);
147
- if (!fields.minute.has(local.minute) || !fields.hour.has(local.hour) || !fields.month.has(local.month))
148
- return false;
149
- const domWildcard = fields.dayOfMonth.size === 31;
150
- const dowWildcard = fields.weekday.size === 7;
151
- const domMatch = fields.dayOfMonth.has(local.day);
152
- const dowMatch = fields.weekday.has(local.weekday);
153
- const dayMatch = domWildcard && dowWildcard ? true : domWildcard ? dowMatch : dowWildcard ? domMatch : domMatch || dowMatch;
154
- return dayMatch;
155
- }
156
- export function nextCronRun(cron, timeZone, after = new Date()) {
157
- const fields = parseCron(cron);
158
- if (!isValidTimeZone(timeZone))
159
- throw new Error(`Invalid schedule timezone: ${timeZone}`);
160
- const candidate = new Date(Math.floor((after.getTime() + 60_000) / 60_000) * 60_000);
161
- const limit = candidate.getTime() + 366 * 24 * 60 * 60_000;
162
- while (candidate.getTime() < limit) {
163
- if (cronMatches(fields, candidate, timeZone))
164
- return new Date(candidate);
165
- candidate.setUTCMinutes(candidate.getUTCMinutes() + 1);
166
- }
167
- throw new Error(`Schedule ${cron} has no next run within one year`);
168
- }
169
- export function isValidTimeZone(timeZone) {
170
- try {
171
- new Intl.DateTimeFormat('en-US', { timeZone });
172
- return true;
173
- }
174
- catch {
175
- return false;
176
- }
177
- }
178
- export function parseJobSchedule(options) {
179
- const fields = parseCron(options.cron);
180
- if (!isValidTimeZone(options.timezone))
181
- throw new Error(`Invalid schedule timezone: ${options.timezone}`);
182
- const missedRunPolicy = options.missedRunPolicy ?? 'skip';
183
- const concurrencyPolicy = options.concurrencyPolicy ?? 'skip';
184
- if (!['run', 'skip'].includes(missedRunPolicy))
185
- throw new Error('Invalid missed-run policy');
186
- if (!['skip', 'queue'].includes(concurrencyPolicy))
187
- throw new Error('Invalid schedule concurrency policy');
188
- const next = options.nextRunAt ? new Date(options.nextRunAt) : nextCronRun(options.cron, options.timezone);
189
- if (Number.isNaN(next.getTime()))
190
- throw new Error('Invalid schedule next-run time');
191
- const aligned = new Date(Math.floor(next.getTime() / 60_000) * 60_000);
192
- if (!cronMatches(fields, aligned, options.timezone))
193
- throw new Error('Schedule next-run time does not match the cron expression');
194
- return {
195
- cron: options.cron.trim(),
196
- timezone: options.timezone,
197
- nextRunAt: aligned.toISOString(),
198
- missedRunPolicy,
199
- concurrencyPolicy,
200
- lastScheduledSlot: null,
201
- currentRunSlot: null,
202
- lastRunAt: null,
203
- };
204
- }
205
- export function nextScheduleRun(schedule, after = new Date()) {
206
- return nextCronRun(schedule.cron, schedule.timezone, after);
207
- }
208
- export function bucketStart(time = new Date()) {
209
- return new Date(Math.floor(time.getTime() / SCHEDULE_RESOLUTION_MS) * SCHEDULE_RESOLUTION_MS);
210
- }
211
- export function scheduleBucket(time) {
212
- const bucket = bucketStart(time);
213
- return `${bucket.toISOString().slice(0, 4)}${bucket.toISOString().slice(5, 7)}${bucket.toISOString().slice(8, 10)}T${bucket.toISOString().slice(11, 13)}${bucket.toISOString().slice(14, 16)}Z`;
214
- }
@@ -1,25 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { GraphRuntime } from './graph-v2.js';
3
- import { dispatchWorkflowGraphSync } from './dispatcher.js';
4
- const source = readFileSync(new URL('../../workflow/schedule-cycle.mmd', import.meta.url), 'utf8');
5
- const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
- export const scheduleCycleV2 = new GraphRuntime(source, {
7
- states: new Set(['idle', 'check_satisfied', 'waiting', 'finished']),
8
- guards: {
9
- hasCurrentSlot: boolGuard('hasCurrentSlot'),
10
- plannerSatisfied: boolGuard('plannerSatisfied'),
11
- },
12
- actions: { 'schedule.finishSlot': { id: 'schedule.finishSlot' } },
13
- events: { 'schedule.reconcile': { id: 'schedule.reconcile' } },
14
- });
15
- export function dispatchScheduleCycle(facts, onFinish) {
16
- return dispatchWorkflowGraphSync(scheduleCycleV2, {
17
- initialState: 'idle', input: { transport: 'event', id: 'schedule.reconcile', payload: {} }, durableFacts: facts, graphName: 'schedule-cycle',
18
- isTransientState: state => state.startsWith('check_'), maxSteps: 2,
19
- executeAction: actionId => { if (actionId === 'schedule.finishSlot')
20
- onFinish();
21
- else
22
- throw new Error('Unsupported schedule cycle action ' + actionId); return {}; },
23
- });
24
- }
25
- export function decideScheduleCycle(facts) { return dispatchScheduleCycle(facts, () => { }); }
@@ -1,30 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { GraphRuntime } from './graph-v2.js';
3
- import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
4
- const source = readFileSync(new URL('../../workflow/scheduled-run.mmd', import.meta.url), 'utf8');
5
- const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
- export const scheduledRunV2 = new GraphRuntime(source, {
7
- states: new Set([
8
- 'idle', 'check_unclaimed', 'check_job', 'check_active', 'check_concurrency',
9
- 'check_queue', 'check_missed', 'skip_idempotent', 'skip_active', 'claimed',
10
- ]),
11
- guards: {
12
- slotDue: boolGuard('slotDue'),
13
- slotUnclaimed: boolGuard('slotUnclaimed'),
14
- jobActive: boolGuard('jobActive'),
15
- hasActiveTasks: boolGuard('hasActiveTasks'),
16
- concurrencySkip: boolGuard('concurrencySkip'),
17
- concurrencyQueue: boolGuard('concurrencyQueue'),
18
- runMissed: boolGuard('runMissed'),
19
- },
20
- actions: Object.fromEntries([
21
- 'schedule.skipActiveAdvance', 'schedule.claimFromSlot', 'schedule.claimFromNow',
22
- ].map(id => [id, { id }])),
23
- events: { 'schedule.tick': { id: 'schedule.tick' } },
24
- });
25
- export async function dispatchScheduledRun(facts, executeAction) {
26
- return dispatchWorkflowGraph(scheduledRunV2, { initialState: 'idle', input: { transport: 'event', id: 'schedule.tick', payload: {} }, durableFacts: facts, graphName: 'scheduled-run', isTransientState: state => state.startsWith('check_'), maxSteps: 8, executeAction });
27
- }
28
- export function decideScheduledRun(facts) {
29
- return dispatchWorkflowGraphSync(scheduledRunV2, { initialState: 'idle', input: { transport: 'event', id: 'schedule.tick', payload: {} }, durableFacts: facts, graphName: 'scheduled-run', isTransientState: state => state.startsWith('check_'), maxSteps: 8 });
30
- }
@@ -1,23 +0,0 @@
1
- ---
2
- title: Schedule Cycle
3
- description: Reconciles scheduled execution slots and finishes verified completed slots.
4
- ---
5
- flowchart LR
6
- %% relay: initial idle
7
- idle["state: idle"]
8
- checkSatisfied["state: check_satisfied"]
9
- waiting["state: waiting"]
10
- finished["state: finished"]
11
-
12
- hasSlot["guard: hasCurrentSlot"]
13
- satisfied["guard: plannerSatisfied"]
14
- finish["action: schedule.finishSlot"]
15
-
16
- idle -->|on: schedule.reconcile| hasSlot
17
- hasSlot -->|false| waiting
18
- hasSlot -->|true| checkSatisfied
19
-
20
- checkSatisfied -->|on: schedule.reconcile| satisfied
21
- satisfied -->|false| waiting
22
- satisfied -->|true| finish
23
- finish --> finished
@@ -1,59 +0,0 @@
1
- ---
2
- title: Scheduled Run
3
- description: Controls scheduled-run lifecycle and guarded execution.
4
- ---
5
- flowchart LR
6
- %% relay: initial idle
7
- idle["state: idle"]
8
- checkUnclaimed["state: check_unclaimed"]
9
- checkJob["state: check_job"]
10
- checkActive["state: check_active"]
11
- checkConcurrency["state: check_concurrency"]
12
- checkQueue["state: check_queue"]
13
- checkMissed["state: check_missed"]
14
- skipIdempotent["state: skip_idempotent"]
15
- skipActive["state: skip_active"]
16
- claimed["state: claimed"]
17
-
18
- slotDue["guard: slotDue"]
19
- slotUnclaimed["guard: slotUnclaimed"]
20
- jobActive["guard: jobActive"]
21
- hasActiveTasks["guard: hasActiveTasks"]
22
- concurrencySkip["guard: concurrencySkip"]
23
- concurrencyQueue["guard: concurrencyQueue"]
24
- runMissed["guard: runMissed"]
25
-
26
- skipAdvance["action: schedule.skipActiveAdvance"]
27
- claimFromSlot["action: schedule.claimFromSlot"]
28
- claimFromNow["action: schedule.claimFromNow"]
29
-
30
- idle -->|on: schedule.tick| slotDue
31
- slotDue -->|false| skipIdempotent
32
- slotDue -->|true| checkUnclaimed
33
-
34
- checkUnclaimed -->|on: schedule.tick| slotUnclaimed
35
- slotUnclaimed -->|false| skipIdempotent
36
- slotUnclaimed -->|true| checkJob
37
-
38
- checkJob -->|on: schedule.tick| jobActive
39
- jobActive -->|false| skipIdempotent
40
- jobActive -->|true| checkActive
41
-
42
- checkActive -->|on: schedule.tick| hasActiveTasks
43
- hasActiveTasks -->|false| checkMissed
44
- hasActiveTasks -->|true| checkConcurrency
45
-
46
- checkConcurrency -->|on: schedule.tick| concurrencySkip
47
- concurrencySkip -->|true| skipAdvance
48
- skipAdvance --> skipActive
49
- concurrencySkip -->|false| checkQueue
50
-
51
- checkQueue -->|on: schedule.tick| concurrencyQueue
52
- concurrencyQueue -->|true| skipActive
53
- concurrencyQueue -->|false| checkMissed
54
-
55
- checkMissed -->|on: schedule.tick| runMissed
56
- runMissed -->|true| claimFromSlot
57
- claimFromSlot --> claimed
58
- runMissed -->|false| claimFromNow
59
- claimFromNow --> claimed