agents-relay 2.0.140 → 2.0.141
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.
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
import { eligibleWorkerCandidates, workerCandidates } from './worker-routing.js';
|
|
2
2
|
import { routeExecution } from './execution-routing.js';
|
|
3
|
+
import { eventFor } from './events.js';
|
|
4
|
+
export function codexFallbackRouting(task, adapters, registered, usage) {
|
|
5
|
+
const requirements = task.workerRequirements ?? { capabilities: [] };
|
|
6
|
+
const excludedWorkers = new Set((task.routingExclusions ?? []).map(item => item.workerId).filter((id) => Boolean(id)));
|
|
7
|
+
const excludedAdapters = new Set((task.routingExclusions ?? []).map(item => item.adapter).filter((adapter) => Boolean(adapter)));
|
|
8
|
+
const candidates = eligibleWorkerCandidates(workerCandidates(adapters, registered, usage), requirements, { strictCapabilities: true })
|
|
9
|
+
.filter(candidate => candidate.adapter === 'codex' && !excludedWorkers.has(candidate.workerId) && !excludedAdapters.has(candidate.adapter));
|
|
10
|
+
const excludedRoutes = (task.routingExclusions ?? []).map(item => ({ provider: item.provider, model: item.model, ...(item.profile ? { profile: item.profile } : {}) }));
|
|
11
|
+
return { candidates, excludedRoutes };
|
|
12
|
+
}
|
|
3
13
|
export async function codexFallbackCompatible(task, adapters, registered, usage) {
|
|
4
14
|
if (task.adapter !== 'chatgpt' || task.chatgptThreadDelivery || task.workerRequirements?.runtime === 'chatgpt')
|
|
5
15
|
return false;
|
|
6
16
|
const requirements = task.workerRequirements ?? { capabilities: [] };
|
|
7
|
-
const candidates =
|
|
8
|
-
.filter(candidate => candidate.adapter === 'codex');
|
|
17
|
+
const { candidates, excludedRoutes } = codexFallbackRouting(task, adapters, registered, usage);
|
|
9
18
|
if (candidates.length === 0)
|
|
10
19
|
return false;
|
|
11
20
|
try {
|
|
12
|
-
await routeExecution(candidates, requirements, { registered, usage, taskFacts: { task } });
|
|
21
|
+
await routeExecution(candidates, requirements, { registered, usage, taskFacts: { task }, excludedRoutes });
|
|
13
22
|
return true;
|
|
14
23
|
}
|
|
15
24
|
catch {
|
|
16
25
|
return false;
|
|
17
26
|
}
|
|
18
27
|
}
|
|
28
|
+
export async function quarantineUnavailableCodexFallback(transitions, emit, jobId, taskId, error) {
|
|
29
|
+
const transition = await transitions.apply(jobId, taskId, 'worker.platform_failure', {
|
|
30
|
+
eligible: (_job, task) => task.state === 'QUEUED' && task.adapter === 'chatgpt' && task.platformFailure?.safeToReroute === true,
|
|
31
|
+
facts: () => ({ platformFallbackSafe: false }),
|
|
32
|
+
mutate: (_job, task) => { task.error = `Codex fallback route became unavailable: ${error instanceof Error ? error.message : String(error)}`; },
|
|
33
|
+
});
|
|
34
|
+
if (transition?.decision.accepted)
|
|
35
|
+
await emit(eventFor(jobId, taskId, transition.task.parentTaskId, 'task.blocked', 'blocked', transition.task.error ?? 'Codex fallback route unavailable', 'user', { reason: transition.task.failureReason, phase: 'fallback-routing' }));
|
|
36
|
+
}
|
|
19
37
|
export function persistChatGptPlatformFailure(task, original, executionId, classification, evidence, safeToReroute) {
|
|
20
38
|
const route = original?.routing;
|
|
21
39
|
const workerId = original?.workerSelection?.workerId ?? 'chatgpt';
|
|
@@ -11,6 +11,7 @@ import { createTaskEntity } from './workflow/entity-creation.js';
|
|
|
11
11
|
import { localSourceHead, localWorkingHead } from './local-git-merge.js';
|
|
12
12
|
import { chatgptThreadRequirements, resolveReviewerThreadDestination, resolveImplementationTaskThreadDestination, prepareReviewerThreadDelivery } from './reviewer-thread.js';
|
|
13
13
|
import { reviewGenerationMatches } from './review-generation-fence.js';
|
|
14
|
+
import { codexFallbackRouting, quarantineUnavailableCodexFallback } from './chatgpt-platform-fallback.js';
|
|
14
15
|
export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
15
16
|
async currentHeadSha(job) {
|
|
16
17
|
// GitHub PR head facts are audit-only. Source identity is local Git.
|
|
@@ -268,9 +269,11 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
268
269
|
return { task, routed: false };
|
|
269
270
|
const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
|
|
270
271
|
const registered = await this.store.listAgents();
|
|
272
|
+
const codexFallback = task.platformFailure?.safeToReroute === true && task.adapter === 'chatgpt';
|
|
273
|
+
const fallbackRouting = codexFallback ? codexFallbackRouting(task, this.options.adapters, registered, usage) : undefined;
|
|
271
274
|
const excludedWorkers = new Set((task.routingExclusions ?? []).map(item => item.workerId).filter((id) => Boolean(id)));
|
|
272
275
|
const excludedAdapters = new Set((task.routingExclusions ?? []).map(item => item.adapter).filter((adapter) => Boolean(adapter)));
|
|
273
|
-
const candidates = workerCandidates(this.options.adapters, registered, usage)
|
|
276
|
+
const candidates = fallbackRouting?.candidates ?? workerCandidates(this.options.adapters, registered, usage)
|
|
274
277
|
.filter(candidate => !excludedWorkers.has(candidate.workerId))
|
|
275
278
|
.filter(candidate => !excludedAdapters.has(candidate.adapter))
|
|
276
279
|
.filter(candidate => (implementationThread || reviewerThread) ? candidate.adapter === 'chatgpt' : true);
|
|
@@ -301,7 +304,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
301
304
|
const parentExcludedRoutes = parentRouteFailedBeforeStart && parent?.routing
|
|
302
305
|
? [{ provider: parent.routing.provider, model: parent.routing.model, profile: parent.routing.profile }]
|
|
303
306
|
: [];
|
|
304
|
-
const taskExcludedRoutes = (task.routingExclusions ?? []).map(item => ({ provider: item.provider, model: item.model, profile: item.profile }));
|
|
307
|
+
const taskExcludedRoutes = fallbackRouting?.excludedRoutes ?? (task.routingExclusions ?? []).map(item => ({ provider: item.provider, model: item.model, profile: item.profile }));
|
|
305
308
|
const excludedRoutes = [...taskExcludedRoutes, ...parentExcludedRoutes];
|
|
306
309
|
const routed = await routeExecution(candidates, requirements, {
|
|
307
310
|
decisionEngine: this.options.decisionEngine,
|
|
@@ -325,6 +328,17 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
325
328
|
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'model.selected', 'queued', 'Selected ' + task.routing.model + ' · thinking ' + (task.routing.reasoning ?? 'unknown'), 'user', { routing: task.routing }, { agent: 'relay', component: 'agents-relay' }));
|
|
326
329
|
return { task, routed: true };
|
|
327
330
|
}
|
|
331
|
+
async routeTaskForExecutionFailClosed(job, task) {
|
|
332
|
+
try {
|
|
333
|
+
return await this.routeTaskForExecution(job, task);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (task.adapter !== 'chatgpt' || !task.platformFailure?.safeToReroute)
|
|
337
|
+
throw error;
|
|
338
|
+
await quarantineUnavailableCodexFallback(this.taskTransitions, event => this.emit(event), job.id, task.id, error);
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
328
342
|
async applyReviewEvent(event) {
|
|
329
343
|
const job = await this.store.load(event.job_id);
|
|
330
344
|
const task = job.tasks.find(item => item.id === event.task_id && item.agentName === 'reviewer');
|
package/dist/reconciler.js
CHANGED
|
@@ -374,7 +374,10 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
374
374
|
await bindPlannedReviewHead(this.store, freshTask, await this.currentHeadSha(freshJob));
|
|
375
375
|
const implementationThread = resolveImplementationTaskThreadDestination(freshJob, freshTask);
|
|
376
376
|
if ((!freshTask.routing || ((implementationThread || (freshTask.agentName === 'reviewer' && resolveReviewerThreadDestination(freshJob, this.options.reviewer?.threadId))) && freshTask.adapter !== 'chatgpt')) && freshTask.adapter !== 'orchestrator') {
|
|
377
|
-
|
|
377
|
+
const routed = await this.routeTaskForExecutionFailClosed(freshJob, freshTask);
|
|
378
|
+
if (!routed)
|
|
379
|
+
return;
|
|
380
|
+
freshTask = routed.task;
|
|
378
381
|
}
|
|
379
382
|
if (['codex', 'chatgpt'].includes(freshTask.adapter) && !freshTask.routing) {
|
|
380
383
|
throw new Error('Task ' + freshTask.id + ' requires routing metadata before execution claim');
|