agents-relay 1.0.94 → 1.0.96
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 +7 -1
- package/dist/decision.js +3 -3
- package/dist/execution-context.js +1 -1
- package/dist/reconciler.js +6 -1
- package/dist/troubleshooter.js +76 -0
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +6 -0
- package/skills/agents-relay/agents/troubleshooter.agent.md +52 -0
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Planner-created tasks use stable IDs and the existing parent/subtask tree, so
|
|
|
70
70
|
retries and daemon restarts do not create duplicate work.
|
|
71
71
|
Task producers must follow `skills/agents-relay/contracts/task-job-contract.md`; that shared skill contract defines story size, lifecycle events, review, and auto-merge semantics.
|
|
72
72
|
|
|
73
|
-
Worker/model routing is code-driven: Relay gathers authoritative worker/model/resource facts, applies hard gates, then can call the generic typed `decide(request, contract)` boundary for judgment among legal candidates. The optional Laya implementation is a transient CLI subprocess per decision (no resident model/service), with deterministic fallback
|
|
73
|
+
Worker/model routing is code-driven: Relay gathers authoritative worker/model/resource facts, applies hard gates, then can call the generic typed `decide(request, contract)` boundary for judgment among legal candidates. The optional Laya implementation is a transient CLI subprocess per decision (no resident model/service), with active Laya decisions and deterministic fallback on decision failure. See [Worker Router, Model Router, and typed decisions](docs/worker-model-routers.md).
|
|
74
74
|
All task producers use the same story-size contract: `S`≈5 minutes, `M`≈15 minutes, and `L`≈45 minutes of normal-reasoning execution, with `M` as the default. `high` reasoning doubles that overall window. ChatGPT Browser Worker start-ack timeout is derived as one third of the effective overall timeout, clamped between 1 and 5 minutes. Orchestrators and planners should choose `storySize`, not raw timeout milliseconds; `--timeout` remains only as an explicit compatibility/debug override.
|
|
75
75
|
|
|
76
76
|
Autonomous jobs can also be scheduled without a Relay database. The job marker
|
|
@@ -187,3 +187,9 @@ GitHub PR state is authoritative for terminal lifecycle: a merged PR reconciles
|
|
|
187
187
|
`adapter: orchestrator` is reserved for synchronous self-work by the exact orchestrator turn that creates the Task. Relay deliberately does not auto-start these Tasks during reconciliation. The creator must perform the work before ending the turn and terminalize the same Task with `npx agents-relay task complete --task-id <id> --summary <summary> ...`. If work should continue asynchronously or in a later turn, use a real worker adapter instead.
|
|
188
188
|
|
|
189
189
|
For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be created as a durable Task before execution. For managed jobs, the GitHub PR body is the canonical Job surface: hidden Job metadata marker plus the visible Markdown objective.
|
|
190
|
+
|
|
191
|
+
### Automatic task troubleshooting
|
|
192
|
+
|
|
193
|
+
When an ACTIVE Job contains a current-run `FAILED` or `BLOCKED` task, reconciliation creates one P0-priority `troubleshooter` recovery task for that exact failed attempt. 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.
|
|
194
|
+
|
|
195
|
+
The Troubleshooter diagnoses before retrying. A task-local or transient blocker is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the blocker is removed does it retry the original 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.
|
package/dist/decision.js
CHANGED
|
@@ -84,7 +84,7 @@ export class LayaCliDecisionEngine {
|
|
|
84
84
|
timeoutMs;
|
|
85
85
|
env;
|
|
86
86
|
constructor(options = {}) {
|
|
87
|
-
this.command = options.command ??
|
|
87
|
+
this.command = options.command ?? 'uvx';
|
|
88
88
|
this.args = options.args ?? layaArgsFromEnv();
|
|
89
89
|
this.timeoutMs = options.timeoutMs ?? Number(process.env.AGENTS_RELAY_LAYA_TIMEOUT_MS ?? 30_000);
|
|
90
90
|
this.env = options.env;
|
|
@@ -168,8 +168,8 @@ function layaArgsFromEnv() {
|
|
|
168
168
|
throw new Error('AGENTS_RELAY_LAYA_ARGS must be a JSON string array');
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
-
export function decisionMode(
|
|
172
|
-
return
|
|
171
|
+
export function decisionMode() {
|
|
172
|
+
return 'active';
|
|
173
173
|
}
|
|
174
174
|
export async function chooseWithDecision(engine, mode, request, contract, fallback) {
|
|
175
175
|
validateDecisionRequest(request, contract);
|
|
@@ -283,5 +283,5 @@ Runtime capabilities:
|
|
|
283
283
|
${runtimes}
|
|
284
284
|
|
|
285
285
|
Delegation contract:
|
|
286
|
-
Nested work MUST be created as an Agents Relay durable child task with parentTaskId lineage. Do not directly spawn arbitrary child agents. Leaf workers do not inherit top-level XChat orchestration duties merely because xchat-orchestrator is visible
|
|
286
|
+
${context.role === 'troubleshooter' ? 'This is an orchestrator-side recovery role. Follow the selected Troubleshooter contract for P0/self-job repair authority and use durable Agents Relay Jobs/Tasks for delegated work.' : 'Nested work MUST be created as an Agents Relay durable child task with parentTaskId lineage. Do not directly spawn arbitrary child agents. Leaf workers do not inherit top-level XChat orchestration duties merely because xchat-orchestrator is visible.'}`;
|
|
287
287
|
}
|
package/dist/reconciler.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ReconcilerReviewBase } from './reconciler-review.js';
|
|
|
11
11
|
import { executionEventSource } from './reconciler-runtime.js';
|
|
12
12
|
export { executionEventSource } from './reconciler-runtime.js';
|
|
13
13
|
import { CallbackWorkflowEffectHandler } from './workflow/effects.js';
|
|
14
|
+
import { pendingTroubleshooterTasks } from './troubleshooter.js';
|
|
14
15
|
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
15
16
|
export function compareTaskPriority(a, b, jobPriority = 'P2') { const ar = PRIORITY_RANK[a.priority ?? jobPriority]; const br = PRIORITY_RANK[b.priority ?? jobPriority]; return ar - br || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
|
|
16
17
|
export async function managedWorkerInput(job, task, context) {
|
|
@@ -131,6 +132,10 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
131
132
|
return job;
|
|
132
133
|
await this.recoverLostExecutions(job, now);
|
|
133
134
|
await this.recoverPlannerChildren(job);
|
|
135
|
+
for (const recovery of pendingTroubleshooterTasks(await this.store.load(jobId))) {
|
|
136
|
+
await this.store.appendTask(recovery);
|
|
137
|
+
await this.emit(eventFor(job.id, recovery.id, null, "troubleshooter.created", "queued", "Troubleshooter task " + recovery.id + " created for failed/blocked work", "orchestrator", { agentName: "troubleshooter" }));
|
|
138
|
+
}
|
|
134
139
|
await this.routeExecutableTasks(await this.store.load(jobId));
|
|
135
140
|
for (const task of job.tasks)
|
|
136
141
|
if (task.state === 'CANCELLED') {
|
|
@@ -313,7 +318,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
313
318
|
}
|
|
314
319
|
let executionContext;
|
|
315
320
|
try {
|
|
316
|
-
const role = task.agentName === 'reviewer' ? 'reviewer' : task.parentTaskId ? 'nested-worker' : 'worker';
|
|
321
|
+
const role = task.agentName === 'reviewer' ? 'reviewer' : task.agentName === 'troubleshooter' ? 'troubleshooter' : task.parentTaskId ? 'nested-worker' : 'worker';
|
|
317
322
|
executionContext = await this.resolvedContext(job, task, role);
|
|
318
323
|
}
|
|
319
324
|
catch (error) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { taskOverallTimeoutMs } from './types.js';
|
|
2
|
+
function slug(value) {
|
|
3
|
+
return value.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 72) || 'task';
|
|
4
|
+
}
|
|
5
|
+
export function troubleshooterTaskId(task) {
|
|
6
|
+
return `troubleshoot-${slug(task.id)}-a${task.attempt}`;
|
|
7
|
+
}
|
|
8
|
+
function failureInput(job, task) {
|
|
9
|
+
return `Recover the failed/blocked task below and make it move again if technically safe.
|
|
10
|
+
|
|
11
|
+
Original repository: ${job.repository}
|
|
12
|
+
Original PR: #${job.prNumber}
|
|
13
|
+
Original job: ${job.id}
|
|
14
|
+
Original task: ${task.id}
|
|
15
|
+
Original task URL: ${task.taskUrl ?? 'unknown'}
|
|
16
|
+
Original state: ${task.state}
|
|
17
|
+
Original attempt: ${task.attempt}
|
|
18
|
+
Original worker: ${task.workerSelection?.workerId ?? task.adapter}
|
|
19
|
+
Original routing: ${task.routing ? JSON.stringify(task.routing) : 'unrouted'}
|
|
20
|
+
Failure reason: ${task.failureReason ?? task.error ?? 'unknown'}
|
|
21
|
+
|
|
22
|
+
MANDATORY EVENT EVIDENCE BEFORE DIAGNOSIS:
|
|
23
|
+
Retrieve this task's lifecycle event history before classifying the root cause or retrying:
|
|
24
|
+
GET http://127.0.0.1:8787/api/events?job=${encodeURIComponent(job.id)}&task=${encodeURIComponent(task.id)}&limit=200
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
Follow the selected Troubleshooter agent contract. Diagnose root cause from durable task state + event history + relevant runtime logs before retry. 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.`;
|
|
28
|
+
}
|
|
29
|
+
function belongsToCurrentRun(job, task) {
|
|
30
|
+
const slot = job.schedule?.currentRunSlot;
|
|
31
|
+
return !slot || task.createdAt >= slot;
|
|
32
|
+
}
|
|
33
|
+
export function pendingTroubleshooterTasks(job) {
|
|
34
|
+
const existing = new Set(job.tasks.filter(task => task.agentName === 'troubleshooter').map(task => task.id));
|
|
35
|
+
const recoveries = [];
|
|
36
|
+
for (const task of job.tasks) {
|
|
37
|
+
if (!['FAILED', 'BLOCKED'].includes(task.state) || task.kind === 'planner' || task.agentName === 'troubleshooter' || !belongsToCurrentRun(job, task))
|
|
38
|
+
continue;
|
|
39
|
+
const id = troubleshooterTaskId(task);
|
|
40
|
+
if (existing.has(id))
|
|
41
|
+
continue;
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
recoveries.push({
|
|
44
|
+
jobId: job.id,
|
|
45
|
+
id,
|
|
46
|
+
kind: 'work',
|
|
47
|
+
priority: 'P0',
|
|
48
|
+
storySize: 'M',
|
|
49
|
+
projectName: job.title,
|
|
50
|
+
agentName: 'troubleshooter',
|
|
51
|
+
parentTaskId: null,
|
|
52
|
+
dependencies: [],
|
|
53
|
+
capabilities: ['troubleshoot', 'recovery'],
|
|
54
|
+
workerRequirements: { capabilities: ['model', 'local_repo', 'iterative_tools'], runtime: 'codex' },
|
|
55
|
+
adapter: 'codex',
|
|
56
|
+
input: failureInput(job, task),
|
|
57
|
+
output: { kind: 'task_pr' },
|
|
58
|
+
continuation: null,
|
|
59
|
+
continuationDeliveredAt: null,
|
|
60
|
+
state: 'QUEUED',
|
|
61
|
+
attempt: 0,
|
|
62
|
+
maxAttempts: 1,
|
|
63
|
+
leaseOwner: null,
|
|
64
|
+
leaseExpiresAt: null,
|
|
65
|
+
executionId: null,
|
|
66
|
+
threadId: null,
|
|
67
|
+
result: null,
|
|
68
|
+
plannerResult: null,
|
|
69
|
+
error: null,
|
|
70
|
+
timeoutMs: taskOverallTimeoutMs('M'),
|
|
71
|
+
createdAt: now,
|
|
72
|
+
updatedAt: now,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return recoveries;
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -111,3 +111,9 @@ Planner-created child Tasks remain descriptive agent work and must include worke
|
|
|
111
111
|
GitHub PR/job/task markers remain durable workflow state. Events are the authoritative execution-state transition signal for model-backed tasks. Runtime events, process exits, browser submission receipts, and conversation text are evidence only.
|
|
112
112
|
|
|
113
113
|
Keep secrets, credentials, private prompts, and large private payloads out of durable markers and events.
|
|
114
|
+
|
|
115
|
+
### Automatic task troubleshooting
|
|
116
|
+
|
|
117
|
+
When an ACTIVE Job contains a current-run `FAILED` or `BLOCKED` task, reconciliation creates one P0-priority `troubleshooter` recovery task for that exact failed attempt. 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.
|
|
118
|
+
|
|
119
|
+
The Troubleshooter diagnoses before retrying. A task-local or transient blocker is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the blocker is removed does it retry the original 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.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: troubleshooter
|
|
3
|
+
description: Recover failed or blocked Agents Relay tasks by diagnosing the root cause, repairing recoverable blockers, and retrying the original task only after the blocker is removed.
|
|
4
|
+
capabilities: [troubleshoot, recovery, orchestration, local_repo, iterative_tools]
|
|
5
|
+
runtime: codex
|
|
6
|
+
delegation: [p0-self-job, relay-task-retry]
|
|
7
|
+
restrictions: [preserve-failure-evidence, no-blind-retry, no-human-blocker-bypass]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Troubleshooter Agent
|
|
11
|
+
|
|
12
|
+
You are the recovery orchestrator for a failed or blocked Agents Relay task.
|
|
13
|
+
|
|
14
|
+
Your goal is not to explain the failure. Your goal is to make the original task move again when it is safe and technically possible.
|
|
15
|
+
|
|
16
|
+
Read and follow the shared Task / Job contract and orchestrator P0/self-job workflow before acting.
|
|
17
|
+
|
|
18
|
+
## Recovery loop
|
|
19
|
+
|
|
20
|
+
1. Retrieve the durable original Job and failed/blocked Task from GitHub.
|
|
21
|
+
2. **Mandatory before diagnosis:** retrieve the failed Task's task-scoped lifecycle event history. When the local Relay dashboard is available, use `GET http://127.0.0.1:8787/api/events?job=<job_id>&task=<task_id>&limit=200`. Inspect event order, type/status, source, data/phase, execution IDs, duplicates, missing expected lifecycle events, and late events. Then inspect task error/failureReason, worker selection, routing, attempt count, task input/output contract, PR state, and relevant runtime/service logs.
|
|
22
|
+
3. Classify the blocker:
|
|
23
|
+
- transient execution/delivery/lease failure;
|
|
24
|
+
- task-local implementation or validation failure;
|
|
25
|
+
- shared worker/runtime/tool/Agents Relay defect;
|
|
26
|
+
- genuine external/human blocker.
|
|
27
|
+
4. Diagnose the smallest root cause from the combined durable Task + event sequence + logs. The final `task.error` is only one piece of evidence. Never retry first and investigate later.
|
|
28
|
+
5. Repair:
|
|
29
|
+
- For a transient failure, verify the transient condition is gone.
|
|
30
|
+
- For task-local failure, make the smallest durable repair in the original Job/PR when that is within scope.
|
|
31
|
+
- For a shared platform/runtime defect, become the active orchestrator for a P0/self-job in the repository that owns the broken component. Create the durable P0 Job and its orchestrator-owned Task, fix it directly, validate, merge, and complete any required publish/deploy reconciliation.
|
|
32
|
+
- For a genuine human/external blocker, do not fake progress. Publish task.blocked with the exact action required.
|
|
33
|
+
6. Only after the blocker is removed, run `agents-relay task retry` for the original Task and reconcile its Job.
|
|
34
|
+
7. Verify durable evidence that the original Task moved: at minimum QUEUED after retry, preferably a new execution / task.started / observable progress.
|
|
35
|
+
8. Publish this Troubleshooter Task's terminal event only after the recovery outcome is durable.
|
|
36
|
+
|
|
37
|
+
## Rules
|
|
38
|
+
|
|
39
|
+
- Never erase or rewrite the original failure evidence.
|
|
40
|
+
- Never blindly retry a deterministic failure. Event-history inspection is a hard precondition for retry; if event history cannot be retrieved, record that observability blocker explicitly rather than pretending diagnosis is complete.
|
|
41
|
+
- Never create a second P0 Job for the same root cause when a suitable active one already exists; reuse/adopt/reconcile it.
|
|
42
|
+
- P0 fixes must follow the P0/self-job contract: durable Job first, orchestrator-owned Task immediately, direct execution by this recovery orchestrator, no reviewer gate unless explicitly required.
|
|
43
|
+
- Repository PR creation/adoption/repair still goes through Agents Relay.
|
|
44
|
+
- Keep fixes narrowly scoped; do not use a failed application task as permission for broad platform refactoring.
|
|
45
|
+
- If a P0 fix publishes a package or changes a managed service, verify the released artifact and dependent service health before retrying the original Task.
|
|
46
|
+
- Do not mark yourself successful merely because a repair PR was opened. Recovery succeeds when the original Task is actually retryable and has been retried.
|
|
47
|
+
|
|
48
|
+
## Example
|
|
49
|
+
|
|
50
|
+
A final review fails because the ChatGPT browser worker reports `Temporary Chat toggle was not uniquely observed`.
|
|
51
|
+
|
|
52
|
+
That is not a defect in the reviewed PR. Inspect the browser-worker/browser-harness path, repair that shared runtime through a P0/self-job in its owning repository, validate/deploy the fix, then retry the original final-review Task and verify its new execution begins.
|