agents-relay 2.0.44 → 2.0.46

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 CHANGED
@@ -38,6 +38,7 @@ RelayStore owns local operational execution facts; GitHub remains the human-read
38
38
  | Scheduled run | [`workflow/scheduled-run.mmd`](workflow/scheduled-run.mmd) |
39
39
  | Schedule cycle | [`workflow/schedule-cycle.mmd`](workflow/schedule-cycle.mmd) |
40
40
  | Troubleshooter | [`workflow/troubleshooter.mmd`](workflow/troubleshooter.mmd) |
41
+ | Escalation | [`workflow/escalation.mmd`](workflow/escalation.mmd) |
41
42
  | Related-task action policy | [`workflow/task-related-action.mmd`](workflow/task-related-action.mmd) |
42
43
 
43
44
  These paths always represent the current canonical graphs. Historical graph definitions are available through Git history, tags, and commits rather than parallel versioned files in `workflow/`. See [`docs/workflow-engine.md`](docs/workflow-engine.md) for the executable profile and runtime boundary.
@@ -248,7 +249,23 @@ For work that was completed outside the relay but must be represented truthfully
248
249
 
249
250
  When an ACTIVE Job contains a current-run `FAILED` task, reconciliation may create one P0-priority `troubleshooter` recovery task for that exact failed attempt. `BLOCKED` work is not troubleshooter-owned: it remains blocked until authoritative unblock transitions it to `UNBLOCKED`. The recovery task is routed to a local Codex-capable worker with repository/tool access and runs under the bundled Troubleshooter agent contract. Troubleshooter diagnosis is event-first: it must inspect the failed task's task-scoped lifecycle event history (including ordering, duplicate/missing/late events, source and phase data) before classifying root cause or retrying.
250
251
 
251
- The Troubleshooter diagnoses before retrying. A task-local or transient execution cause is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the failure cause is repaired does it retry the original FAILED task and verify durable progress. Historical failures from older scheduled autonomous runs do not spawn recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
252
+ The Troubleshooter diagnoses before retrying. A task-local or transient execution cause is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. A suspected human/external blocker enters the canonical [`workflow/escalation.mmd`](workflow/escalation.mmd) graph through `agents-relay escalation`; Troubleshooter does not decide the escalation result branch itself.
253
+
254
+ ```mermaid
255
+ flowchart LR
256
+ Failed[Failed task] --> Troubleshooter
257
+ Troubleshooter -->|technical| Repair[Repair / retry]
258
+ Troubleshooter -->|suspected human blocker| Escalation[Escalation graph]
259
+ Escalation --> Invoke[neo escalation]
260
+ Invoke --> Codex[Escalation Agent + elevated Local Codex]
261
+ Codex -->|resolved| Resume[Resume recovery]
262
+ Codex -->|failed| Technical[Technical recovery evidence]
263
+ Codex -->|needs_user| User[Notify user + task.blocked]
264
+ User --> Unblock[Authoritative unblock]
265
+ Unblock --> Repair
266
+ ```
267
+
268
+ The escalation graph is the authority for what happens after Neo returns: `resolved` selects resume recovery, `failed` selects technical-failure handling, and only `needs_user` selects the block-for-user action. Notification mechanics stay caller-described through `--on-needs-user`; Relay does not bind WeChat, Discord, iMessage, or another channel. Only after the failure cause is repaired does Troubleshooter retry the original FAILED task and verify durable progress. Historical failures from older scheduled autonomous runs do not spawn recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
252
269
 
253
270
 
254
271
  ### Authoritative unblock and Planner history
@@ -0,0 +1,56 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { textOption, arg } from './cli-support.js';
3
+ import { runEscalationGraph } from './workflow/escalation.js';
4
+ function runNeoEscalation(request) {
5
+ const args = ['escalation', '--intent', request.intent, '--blocked-on', request.blockedOn];
6
+ if (request.context)
7
+ args.push('--context', request.context);
8
+ if (request.onNeedsUser)
9
+ args.push('--on-needs-user', request.onNeedsUser);
10
+ if (request.cwd)
11
+ args.push('--cwd', request.cwd);
12
+ return new Promise((resolve, reject) => {
13
+ const child = spawn('neo', args, { cwd: request.cwd ?? process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
14
+ let stdout = '';
15
+ let stderr = '';
16
+ child.stdout.setEncoding('utf8').on('data', chunk => { stdout += chunk; });
17
+ child.stderr.setEncoding('utf8').on('data', chunk => { stderr += chunk; });
18
+ child.on('error', reject);
19
+ child.on('close', code => {
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(stdout.trim());
23
+ }
24
+ catch {
25
+ reject(new Error(`neo escalation returned invalid JSON (exit ${code ?? 'unknown'}): ${stderr || stdout}`));
26
+ return;
27
+ }
28
+ if (!['resolved', 'failed', 'needs_user'].includes(parsed.status)) {
29
+ reject(new Error(`neo escalation returned invalid status ${String(parsed.status)}`));
30
+ return;
31
+ }
32
+ resolve(parsed);
33
+ });
34
+ });
35
+ }
36
+ export async function runEscalationCommand(args) {
37
+ const intent = await textOption(args, '--intent', '--intent-file');
38
+ const blockedOn = await textOption(args, '--blocked-on', '--blocked-on-file');
39
+ if (!intent.trim())
40
+ throw new Error('escalation requires --intent or --intent-file');
41
+ if (!blockedOn.trim())
42
+ throw new Error('escalation requires --blocked-on or --blocked-on-file');
43
+ const context = await textOption(args, '--context', '--context-file');
44
+ const onNeedsUser = await textOption(args, '--on-needs-user', '--on-needs-user-file');
45
+ const request = {
46
+ intent,
47
+ blockedOn,
48
+ ...(context.trim() ? { context } : {}),
49
+ ...(onNeedsUser.trim() ? { onNeedsUser } : {}),
50
+ ...(arg(args, '--cwd') ? { cwd: arg(args, '--cwd') } : {}),
51
+ };
52
+ const outcome = await runEscalationGraph(request, { invoke: runNeoEscalation });
53
+ console.log(JSON.stringify({ state: outcome.state, ...outcome.result }, null, 2));
54
+ if (outcome.state === 'technical_failure')
55
+ process.exitCode = 2;
56
+ }
package/dist/cli-help.js CHANGED
@@ -1,4 +1,4 @@
1
- export const usage = 'agents-relay <job|task|plan|blocker|projection|context|init|record|agent-register|agent-discover|status|reconcile|serve> [options]';
1
+ export const usage = 'agents-relay <job|task|plan|blocker|projection|escalation|context|init|record|agent-register|agent-discover|status|reconcile|serve> [options]';
2
2
  const help = {
3
3
  root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
4
4
 
@@ -11,6 +11,7 @@ Commands:
11
11
  plan Invoke the Planner for an existing managed Job
12
12
  blocker add|resolve|show Manage cross-Job blocker dependencies
13
13
  projection conflicts|resolve Inspect and repair deterministic GitHub projection conflicts
14
+ escalation Run the canonical escalation graph and Neo escalation executor
14
15
  context resolve Resolve shared project instructions, skills, agents, and runtimes
15
16
  init Create a local demo job or initialize a GitHub job
16
17
  record Backfill a completed task into durable history
@@ -79,6 +80,7 @@ Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head S
79
80
  'job cancel': `Cancel the durable Job and close its backing pull request.
80
81
 
81
82
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
83
+ escalation: `Run the canonical escalation workflow for a suspected human/external blocker.\n\nRequired: --intent TEXT|--intent-file PATH and --blocked-on TEXT|--blocked-on-file PATH. Optional: --context/--context-file, --on-needs-user/--on-needs-user-file, --cwd. The executable workflow/escalation.mmd graph invokes Neo escalation, then owns the resolved / failed / needs_user branch. Only needs_user selects the block-for-user path.`,
82
84
  status: `Show live aggregate runtime status from the running Agents Relay service.
83
85
 
84
86
  Usage:
package/dist/cli.js CHANGED
@@ -23,6 +23,7 @@ import { applyJobActivateAction, applyJobCancelAction } from './job-actions.js';
23
23
  import { resolveExecutionContext } from './execution-context.js';
24
24
  import { defaultWorkspaceRoot, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
25
25
  import { runBlockerCommand } from './cli-blocker.js';
26
+ import { runEscalationCommand } from './cli-escalation.js';
26
27
  import { runProjectionCommand } from './cli-projection.js';
27
28
  import { helpText, usage } from './cli-help.js';
28
29
  import { parseAgentGraph } from './agent-graph.js';
@@ -578,6 +579,10 @@ async function main() {
578
579
  await runProjectionCommand(action ?? '', projectionArgs);
579
580
  return;
580
581
  }
582
+ if (command === 'escalation') {
583
+ await runEscalationCommand(args);
584
+ return;
585
+ }
581
586
  if (command === 'init') {
582
587
  const now = new Date().toISOString();
583
588
  const initialSchedule = scheduleFromArgs(args);
@@ -26,7 +26,7 @@ Retrieve this task's lifecycle event history before classifying the root cause o
26
26
  GET http://127.0.0.1:8787/api/events?job=${encodeURIComponent(job.id)}&task=${encodeURIComponent(task.id)}&limit=200
27
27
  Inspect event order, type/status, source, data/phase, duplicate events, missing expected lifecycle events, late events, and execution identifiers. If the event stream points to delivery/runtime failure, inspect the corresponding Relay/worker/tool logs too. Do not treat the final task.error string as the complete root cause.
28
28
 
29
- Follow the selected Troubleshooter agent contract. Diagnose root cause from durable task state + event history + relevant runtime logs before retry. The final task.error string alone is never sufficient. Classify the evidence as model/provider/runtime, worker/tool/infrastructure, semantic task failure, or timeout/unknown. Only model/provider/runtime evidence may call the narrow model re-decision action; that action gathers fresh usage, reloads current policy, updates this exact Task's routing, and does not retry it. Troubleshooter must then reactivate/retry this exact original task and verify progress. Do not manually choose a provider/model or create a replacement task. If the blocker is a shared worker/runtime/tool defect, create and directly complete a P0/self-job in the owning repository, including required release/deploy verification. Retry this exact original task only after the blocker is removed, reconcile the original job, and verify durable progress.`;
29
+ Follow the selected Troubleshooter agent contract. Diagnose root cause from durable task state + event history + relevant runtime logs before retry. The final task.error string alone is never sufficient. Classify the evidence as model/provider/runtime, worker/tool/infrastructure, semantic task failure, or timeout/unknown. Only model/provider/runtime evidence may call the narrow model re-decision action; that action gathers fresh usage, reloads current policy, updates this exact Task's routing, and does not retry it. Troubleshooter must then reactivate/retry this exact original task and verify progress. Do not manually choose a provider/model or create a replacement task. If the blocker is a shared worker/runtime/tool defect, create and directly complete a P0/self-job in the owning repository, including required release/deploy verification. If diagnosis suggests a genuine human/external blocker, do not publish task.blocked directly and do not implement the result branch procedurally. Call agents-relay escalation with the original intent, blocker, and relevant context. Pass any caller-defined notification plan through --on-needs-user/--on-needs-user-file; do not hard-code a channel. The canonical escalation graph invokes Neo escalation and owns the resolved/failed/needs_user branch; follow its selected outcome. Retry this exact original task only after the blocker is removed, reconcile the original job, and verify durable progress.`;
30
30
  }
31
31
  function belongsToCurrentRun(job, task) {
32
32
  const slot = job.schedule?.currentRunSlot;
@@ -0,0 +1,69 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { GraphRuntime } from './graph-v2.js';
3
+ import { dispatchWorkflowGraph } from './dispatcher.js';
4
+ const source = readFileSync(new URL('../../workflow/escalation.mmd', import.meta.url), 'utf8');
5
+ const statusGuard = (status) => (facts) => facts.durableFacts.escalationStatus === status;
6
+ export const escalationGraph = new GraphRuntime(source, {
7
+ states: new Set(['suspected_human_blocker', 'invoking', 'resolved', 'technical_failure', 'needs_user']),
8
+ guards: {
9
+ escalationResolved: statusGuard('resolved'),
10
+ escalationFailed: statusGuard('failed'),
11
+ escalationNeedsUser: statusGuard('needs_user'),
12
+ },
13
+ actions: {
14
+ 'escalation.invoke': { id: 'escalation.invoke' },
15
+ 'escalation.resume_recovery': { id: 'escalation.resume_recovery' },
16
+ 'escalation.technical_failure': { id: 'escalation.technical_failure' },
17
+ 'escalation.block_for_user': { id: 'escalation.block_for_user' },
18
+ },
19
+ events: {
20
+ 'escalation.requested': { id: 'escalation.requested' },
21
+ 'escalation.result': { id: 'escalation.result' },
22
+ },
23
+ });
24
+ export async function runEscalationGraph(request, effects) {
25
+ let result = null;
26
+ const requestDecision = await dispatchWorkflowGraph(escalationGraph, {
27
+ initialState: 'suspected_human_blocker',
28
+ input: { transport: 'event', id: 'escalation.requested', payload: {} },
29
+ graphName: 'escalation',
30
+ executeAction: async (actionId) => {
31
+ if (actionId !== 'escalation.invoke')
32
+ throw new Error(`Unexpected escalation request action ${actionId}`);
33
+ result = await effects.invoke(request);
34
+ return {};
35
+ },
36
+ });
37
+ if (!requestDecision.accepted || requestDecision.target !== 'invoking' || !result) {
38
+ throw new Error('Escalation graph did not invoke the escalation executor');
39
+ }
40
+ const escalationResult = result;
41
+ const resultDecision = await dispatchWorkflowGraph(escalationGraph, {
42
+ initialState: 'invoking',
43
+ input: { transport: 'event', id: 'escalation.result', payload: {} },
44
+ durableFacts: { escalationStatus: escalationResult.status },
45
+ graphName: 'escalation',
46
+ executeAction: async (actionId) => {
47
+ if (actionId === 'escalation.resume_recovery')
48
+ await effects.resumeRecovery?.(escalationResult);
49
+ else if (actionId === 'escalation.technical_failure')
50
+ await effects.technicalFailure?.(escalationResult);
51
+ else if (actionId === 'escalation.block_for_user')
52
+ await effects.blockForUser?.(escalationResult);
53
+ else
54
+ throw new Error(`Unexpected escalation result action ${actionId}`);
55
+ return {};
56
+ },
57
+ });
58
+ if (!resultDecision.accepted)
59
+ throw new Error(`Escalation graph rejected result ${escalationResult.status}`);
60
+ if (!['resolved', 'technical_failure', 'needs_user'].includes(resultDecision.target)) {
61
+ throw new Error(`Escalation graph returned unexpected state ${resultDecision.target}`);
62
+ }
63
+ return {
64
+ state: resultDecision.target,
65
+ result: escalationResult,
66
+ requestDecision,
67
+ resultDecision,
68
+ };
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.44",
3
+ "version": "2.0.46",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@ Read and follow the shared Task / Job contract and orchestrator P0/self-job work
29
29
  - For a transient failure, verify the transient condition is gone.
30
30
  - For task-local failure, make the smallest durable repair in the original Job/PR when that is within scope.
31
31
  - For a shared platform/runtime defect, first create or reuse the owning blocker P0/self-job. If it can be completed inside this recovery execution, fix/verify it and retry the original Task. If it cannot be completed now, register the original Task with `agents-relay blocker add` on that blocker Job and finish this Troubleshooter task; do not stay running while waiting for another Job.
32
- - For a genuine human/external blocker, do not fake progress. Publish task.blocked with the exact action required.
32
+ - For a suspected genuine human/external blocker, do not publish `task.blocked` and do not branch on Neo's result yourself. Invoke `agents-relay escalation` with the original intent, blocker, and relevant context. If the recovery context defines a preferred way to contact the user, pass that description through `--on-needs-user` or `--on-needs-user-file`; Relay must not hard-code a channel. The canonical `workflow/escalation.mmd` graph invokes Neo escalation and owns the `resolved` / `failed` / `needs_user` decision. Follow the graph-selected outcome: resume recovery, preserve technical-failure evidence, or publish `task.blocked` with the exact returned user action and notification evidence.
33
33
  6. For an immediate repair, only after the blocker is removed run `agents-relay task retry` for the original Task and reconcile its Job. For deferred repair, rely on the durable blocker relation: merge-mode blockers wake matching dependents after merge; release/deploy/runtime blockers use `--resolution verified` and `agents-relay blocker resolve` only after verification.
34
34
  7. Verify durable evidence that the original Task moved: at minimum QUEUED after retry, preferably a new execution / task.started / observable progress.
35
35
  8. Publish this Troubleshooter Task's terminal event only after the recovery outcome is durable.
@@ -0,0 +1,28 @@
1
+ flowchart LR
2
+ %% relay: initial suspected
3
+ suspected["state: suspected_human_blocker"]
4
+ invoking["state: invoking"]
5
+ resolved["state: resolved"]
6
+ technical["state: technical_failure"]
7
+ user["state: needs_user"]
8
+
9
+ invoke["action: escalation.invoke"]
10
+ isResolved["guard: escalationResolved"]
11
+ isFailed["guard: escalationFailed"]
12
+ isNeedsUser["guard: escalationNeedsUser"]
13
+ resume["action: escalation.resume_recovery"]
14
+ technicalAction["action: escalation.technical_failure"]
15
+ block["action: escalation.block_for_user"]
16
+
17
+ suspected -->|on: escalation.requested| invoke
18
+ invoke --> invoking
19
+
20
+ invoking -->|on: escalation.result| isResolved
21
+ isResolved -->|true| resume
22
+ resume --> resolved
23
+ isResolved -->|false| isFailed
24
+ isFailed -->|true| technicalAction
25
+ technicalAction --> technical
26
+ isFailed -->|false| isNeedsUser
27
+ isNeedsUser -->|true| block
28
+ block --> user