agents-relay 2.0.132 → 2.0.133
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/cli-help.js +2 -1
- package/dist/cli.js +14 -2
- package/dist/reconciler-review.js +1 -1
- package/dist/relayd-control.js +1 -1
- package/dist/task-actions.js +24 -2
- package/dist/troubleshooter.js +1 -1
- package/package.json +1 -1
package/dist/cli-help.js
CHANGED
|
@@ -160,7 +160,8 @@ Required: the normal Job storage options.`,
|
|
|
160
160
|
Required: --task-id ID and the normal Job storage options.`,
|
|
161
161
|
'task recover': `Retry one failed/blocked durable Task from its active automatic Troubleshooter without cancelling that recovery Task.
|
|
162
162
|
|
|
163
|
-
Required: --task-id ID, --recovery-task-id TROUBLESHOOTER_ID, and the normal Job storage options.
|
|
163
|
+
Required: --task-id ID, --recovery-task-id TROUBLESHOOTER_ID, and the normal Job storage options.
|
|
164
|
+
Optional: --failure-kind adapter_failure|model_failure|provider_failure|task_failure|unknown --reason TEXT [--evidence TEXT] [--reroute] [--exclude-current-route]. Routing changes are atomic with recovery. The recovery Task must be an active Troubleshooter whose parentTaskId is the original Task.`,
|
|
164
165
|
'task cancel': `Cancel one durable Task.
|
|
165
166
|
|
|
166
167
|
Required: --task-id ID and the normal Job storage options.`,
|
package/dist/cli.js
CHANGED
|
@@ -70,6 +70,18 @@ async function runManagedTaskCommand(action, args) {
|
|
|
70
70
|
throw new Error(`Task ${id} missing after ${action}`);
|
|
71
71
|
console.log(JSON.stringify(task, null, 2));
|
|
72
72
|
}
|
|
73
|
+
function recoveryDiagnosisFromArgs(args) {
|
|
74
|
+
const failureKind = arg(args, '--failure-kind');
|
|
75
|
+
if (!failureKind)
|
|
76
|
+
return undefined;
|
|
77
|
+
const reason = arg(args, '--reason');
|
|
78
|
+
if (!reason.trim())
|
|
79
|
+
throw new Error('--failure-kind requires --reason');
|
|
80
|
+
if (!['adapter_failure', 'model_failure', 'provider_failure', 'task_failure', 'unknown'].includes(failureKind))
|
|
81
|
+
throw new Error('Invalid --failure-kind');
|
|
82
|
+
const reroute = args.includes('--reroute');
|
|
83
|
+
return { failureKind, reason, evidence: arg(args, '--evidence') || undefined, reroute, excludeCurrentRoute: args.includes('--exclude-current-route') };
|
|
84
|
+
}
|
|
73
85
|
async function runTaskCommand(action, args) {
|
|
74
86
|
if (!['create', 'update', 'show', 'context', 'comment', 'comments', 'list', 'retry', 'recover', 'unblock', 'escalate', 'resume-troubleshooter', 'cancel', 'supersede'].includes(action))
|
|
75
87
|
throw new Error(usage);
|
|
@@ -136,7 +148,7 @@ async function runTaskCommand(action, args) {
|
|
|
136
148
|
const recoveryTaskId = arg(args, '--recovery-task-id');
|
|
137
149
|
if (!recoveryTaskId)
|
|
138
150
|
throw new Error('task recover requires --recovery-task-id');
|
|
139
|
-
const job = await relayControlCommand(args, { command: 'task.recover', jobId, taskId, recoveryTaskId });
|
|
151
|
+
const job = await relayControlCommand(args, { command: 'task.recover', jobId, taskId, recoveryTaskId, recoveryDiagnosis: recoveryDiagnosisFromArgs(args) });
|
|
140
152
|
console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
|
|
141
153
|
return;
|
|
142
154
|
}
|
|
@@ -271,7 +283,7 @@ async function runTaskCommand(action, args) {
|
|
|
271
283
|
const recoveryTaskId = action === 'recover' ? arg(args, '--recovery-task-id') : '';
|
|
272
284
|
if (action === 'recover' && !recoveryTaskId)
|
|
273
285
|
throw new Error('task recover requires --recovery-task-id');
|
|
274
|
-
const result = await dispatchTaskAction(store, { id: action === 'recover' ? 'task.recover' : 'task.retry', jobId: job.id, taskId: id, ...(recoveryTaskId ? { recoveryTaskId } : {}) });
|
|
286
|
+
const result = await dispatchTaskAction(store, { id: action === 'recover' ? 'task.recover' : 'task.retry', jobId: job.id, taskId: id, ...(recoveryTaskId ? { recoveryTaskId, recoveryDiagnosis: recoveryDiagnosisFromArgs(args) } : {}) });
|
|
275
287
|
if (result.job.state === 'DRAFT') {
|
|
276
288
|
await applyJobActivateAction(result.job);
|
|
277
289
|
await store.saveJob(result.job);
|
|
@@ -301,7 +301,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
301
301
|
const routed = await routeExecution(candidates, requirements, {
|
|
302
302
|
decisionEngine: this.options.decisionEngine,
|
|
303
303
|
decisionMode: this.options.decisionMode,
|
|
304
|
-
taskFacts: { ...taskFacts, recovery_parent_failed_route: excludedRoutes[0] ?? null },
|
|
304
|
+
taskFacts: { ...taskFacts, recovery: task.recoveryContext ?? null, recovery_parent_failed_route: excludedRoutes[0] ?? null },
|
|
305
305
|
usage,
|
|
306
306
|
registered,
|
|
307
307
|
chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
|
package/dist/relayd-control.js
CHANGED
|
@@ -158,7 +158,7 @@ export function createWorkspaceControl(deps) {
|
|
|
158
158
|
const recoveryTaskId = typeof body.recoveryTaskId === 'string' ? body.recoveryTaskId : '';
|
|
159
159
|
if (!taskId || !recoveryTaskId)
|
|
160
160
|
throw new Error('task recover requires taskId and recoveryTaskId');
|
|
161
|
-
await dispatchTaskAction(store, { id: 'task.recover', jobId, taskId, recoveryTaskId });
|
|
161
|
+
await dispatchTaskAction(store, { id: 'task.recover', jobId, taskId, recoveryTaskId, recoveryDiagnosis: body.recoveryDiagnosis });
|
|
162
162
|
await pool.reconcileLocalJob(jobId, 'control.task.recover');
|
|
163
163
|
return await store.load(jobId);
|
|
164
164
|
}
|
package/dist/task-actions.js
CHANGED
|
@@ -156,6 +156,28 @@ export async function supersedeCancelledTask(store, jobId, taskId, reason) {
|
|
|
156
156
|
await store.saveTask(task);
|
|
157
157
|
return { job: await store.load(jobId), task };
|
|
158
158
|
}
|
|
159
|
+
function recordRecoveryRouting(task, request) {
|
|
160
|
+
const diagnosis = request.recoveryDiagnosis;
|
|
161
|
+
if (!diagnosis)
|
|
162
|
+
return;
|
|
163
|
+
if (!['adapter_failure', 'model_failure', 'provider_failure', 'task_failure', 'unknown'].includes(diagnosis.failureKind))
|
|
164
|
+
throw new Error('Invalid recovery failureKind');
|
|
165
|
+
if (!diagnosis.reason.trim())
|
|
166
|
+
throw new Error('Recovery diagnosis requires reason');
|
|
167
|
+
if (diagnosis.excludeCurrentRoute && !diagnosis.reroute)
|
|
168
|
+
throw new Error('Cannot exclude route without reroute');
|
|
169
|
+
if (diagnosis.excludeCurrentRoute && task.routing) {
|
|
170
|
+
const route = task.routing;
|
|
171
|
+
if (!task.routingExclusions?.some(item => item.provider === route.provider && item.model === route.model && item.profile === route.profile)) {
|
|
172
|
+
task.routingExclusions = [...(task.routingExclusions ?? []), { provider: route.provider, model: route.model, ...(route.profile ? { profile: route.profile } : {}), reason: diagnosis.reason, at: new Date().toISOString() }];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
task.recoveryContext = { failureKind: diagnosis.failureKind, reason: diagnosis.reason, ...(diagnosis.evidence ? { evidence: diagnosis.evidence } : {}), recoveryTaskId: request.recoveryTaskId, at: new Date().toISOString(), reroute: diagnosis.reroute };
|
|
176
|
+
if (diagnosis.reroute) {
|
|
177
|
+
task.routing = undefined;
|
|
178
|
+
task.workerSelection = undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
159
181
|
/** Compatibility bridge: domain mutation only; adapters own wake/reconcile/output behavior. */
|
|
160
182
|
export async function dispatchTaskAction(store, request) {
|
|
161
183
|
if (store.mutateJob && [TASK_RETRY_ACTION, TASK_RECOVER_ACTION, TASK_CANCEL_ACTION].includes(request.id)) {
|
|
@@ -177,7 +199,7 @@ export async function dispatchTaskAction(store, request) {
|
|
|
177
199
|
}
|
|
178
200
|
else if (request.id === TASK_RECOVER_ACTION) {
|
|
179
201
|
const proposal = { path: 'exact_original_retry', targetTaskId: task.id, rootCause: task.failureReason ?? task.error ?? 'durable failure', solution: `Recover exact original Task via ${request.recoveryTaskId}` };
|
|
180
|
-
const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
|
|
202
|
+
const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); recordRecoveryRouting(task, request); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
|
|
181
203
|
if (!decision.accepted)
|
|
182
204
|
throw new Error(`Task resolution graph rejected exact-original recovery for ${task.id}`);
|
|
183
205
|
}
|
|
@@ -211,7 +233,7 @@ export async function dispatchTaskAction(store, request) {
|
|
|
211
233
|
if (!recovery || recovery.agentName !== 'troubleshooter' || recovery.parentTaskId !== task.id || !['QUEUED', 'RUNNING'].includes(recovery.state))
|
|
212
234
|
throw new Error(`task recover requires an active Troubleshooter recovery for ${task.id}`);
|
|
213
235
|
const proposal = { path: 'exact_original_retry', targetTaskId: task.id, rootCause: task.failureReason ?? task.error ?? 'durable failure', solution: `Recover exact original Task via ${request.recoveryTaskId}` };
|
|
214
|
-
const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
|
|
236
|
+
const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); recordRecoveryRouting(task, request); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
|
|
215
237
|
if (!decision.accepted)
|
|
216
238
|
throw new Error(`Task resolution graph rejected exact-original recovery for ${task.id}`);
|
|
217
239
|
}
|
package/dist/troubleshooter.js
CHANGED
|
@@ -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. Select exactly one graph-enabled resolution path: exact_original_retry, same_job_dependency, blocker_job_dependency (with merge or verified mode), escalation_gate, or cancel_supersede.
|
|
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. Select exactly one graph-enabled resolution path: exact_original_retry, same_job_dependency, blocker_job_dependency (with merge or verified mode), escalation_gate, or cancel_supersede. For confirmed adapter/model/provider failure, include --failure-kind, --reason, --evidence and --reroute in task recover; use --exclude-current-route only if the exact failed route should not be selected again. For semantic task failure use --failure-kind task_failure without rerouting. For uncertain failures use --failure-kind unknown and preserve routing unless evidence justifies rerouting. The recovery transaction stores diagnosis, clears old routing only on request, and next execution automatically reroutes with the evidence and exclusions. Troubleshooter must then reactivate/retry this exact original task and verify progress. Use the dedicated agents-relay task recover path with --task-id ${task.id} and --recovery-task-id ${recoveryId}; never use ordinary task retry from a Troubleshooter because manual retry intentionally supersedes active recovery. Do not manually choose a provider/model or create a replacement task; no replacement_task action exists. 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;
|