agents-relay 1.0.127 → 1.0.128

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.
@@ -2,7 +2,6 @@ import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
2
2
  import { schedule, transitionTaskEvent, transitionJobEvent } from './scheduler.js';
3
3
  import { implementationComplete, implementationTasks } from './runtime-status.js';
4
4
  import { eventFor } from './events.js';
5
- import { selectReviewer } from './reviewer-routing.js';
6
5
  import { routeWorker, workerCandidates } from './worker-routing.js';
7
6
  import { discoverModelCandidates, routeModel } from './model-routing.js';
8
7
  import { ReconcilerRuntimeBase } from './reconciler-runtime.js';
@@ -51,56 +50,57 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
51
50
  const configured = this.options.reviewer;
52
51
  const registered = await this.store.listAgents();
53
52
  const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
54
- const workers = workerCandidates(this.options.adapters, registered, usage).filter(candidate => {
53
+ let workers = workerCandidates(this.options.adapters, registered, usage).filter(candidate => {
55
54
  if (configured?.workerId && configured.workerId !== candidate.workerId)
56
55
  return false;
57
56
  if (configured?.adapter && configured.adapter !== candidate.adapter)
58
57
  return false;
59
58
  return true;
60
59
  });
61
- // Explicit reviewer routing is a caller-owned hard constraint and remains
62
- // readable in isolated/test runtimes that do not instantiate that adapter.
60
+ // Explicit reviewer runtime/routing is a caller-owned hard constraint and remains
61
+ // usable in isolated/test runtimes that do not instantiate that adapter. It still
62
+ // flows through the same Worker Router path as every other executable task.
63
63
  if (workers.length === 0 && configured?.adapter && configured.routing) {
64
- const candidate = {
65
- workerId: configured.workerId ?? configured.adapter,
66
- adapter: configured.adapter,
67
- capabilities: ['model'],
68
- runtime: configured.adapter,
69
- routing: configured.routing,
70
- };
71
- return {
72
- candidate,
73
- selection: { workerId: candidate.workerId, adapter: candidate.adapter, capabilities: [...candidate.capabilities], selectedBy: 'worker-router:reviewer-diversity:eligible-fallback', selectedAt: new Date().toISOString() },
74
- reason: 'eligible-fallback',
75
- };
76
- }
77
- const previews = new Map();
78
- for (const worker of workers) {
79
- try {
80
- const models = await discoverModelCandidates(worker, registered, usage);
81
- const preview = await routeModel(worker, models, { decisionMode: 'off', constraints: configured?.routing });
82
- previews.set(worker.workerId, { ...worker, routing: preview.routing });
83
- }
84
- catch {
85
- continue;
86
- }
87
- }
88
- const result = selectReviewer([...previews.values()], implementationTasks, configured?.requirements ?? { capabilities: ['model'] });
89
- const selectedWorker = workers.find(worker => worker.workerId === result.selection.workerId);
90
- if (selectedWorker) {
91
- result.selection.resourceHealthy = selectedWorker.resourceHealthy;
92
- result.selection.resourceProvider = selectedWorker.resourceProvider;
93
- result.selection.resourceUsedPercent = selectedWorker.resourceUsedPercent;
94
- const models = await discoverModelCandidates(selectedWorker, registered, usage);
95
- const routedModel = await routeModel(selectedWorker, models, {
96
- decisionEngine: this.options.decisionEngine,
97
- decisionMode: this.options.decisionMode,
98
- constraints: configured?.routing,
99
- taskFacts: { role: 'reviewer', implementationWorkers: implementationTasks.map(task => task.workerSelection?.workerId ?? task.adapter) },
100
- });
101
- result.candidate.routing = routedModel.routing;
64
+ workers = [{
65
+ workerId: configured.workerId ?? configured.adapter,
66
+ adapter: configured.adapter,
67
+ capabilities: ['model'],
68
+ runtime: configured.adapter,
69
+ }];
102
70
  }
103
- return result;
71
+ const implementationWorkers = implementationTasks.map(task => task.workerSelection?.workerId ?? task.adapter);
72
+ const implementationRoutes = implementationTasks.filter(task => task.routing).map(task => ({
73
+ provider: task.routing.provider,
74
+ model: task.routing.model,
75
+ }));
76
+ const taskFacts = {
77
+ role: 'reviewer',
78
+ reviewer_diversity: {
79
+ preference: 'Prefer a worker/provider-model identity different from implementation when otherwise suitable; this is a soft preference below user environment rules.',
80
+ implementation_workers: implementationWorkers,
81
+ implementation_routes: implementationRoutes,
82
+ },
83
+ };
84
+ const runtimeSummary = this.options.runtimeSummaryProvider?.();
85
+ const routedWorker = await routeWorker(workers, configured?.requirements ?? { capabilities: ['model'] }, {
86
+ decisionEngine: this.options.decisionEngine,
87
+ decisionMode: this.options.decisionMode,
88
+ taskFacts,
89
+ usage,
90
+ chatgptRunning: runtimeSummary?.chatgptRunning ?? 0,
91
+ });
92
+ const models = await discoverModelCandidates(routedWorker.candidate, registered, usage);
93
+ const routedModel = await routeModel(routedWorker.candidate, models, {
94
+ decisionEngine: this.options.decisionEngine,
95
+ decisionMode: this.options.decisionMode,
96
+ constraints: configured?.routing,
97
+ taskFacts,
98
+ });
99
+ return {
100
+ candidate: { ...routedWorker.candidate, routing: routedModel.routing },
101
+ selection: routedWorker.selection,
102
+ reason: 'worker-router',
103
+ };
104
104
  }
105
105
  async routeExecutableTasks(job) {
106
106
  const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
@@ -2,6 +2,7 @@ import { chooseWithDecision } from './decision.js';
2
2
  import { loadWorkerRouterRules } from './routing-policy.js';
3
3
  export const DEFAULT_CHATGPT_PREFERRED_CONCURRENCY = 10;
4
4
  export const DEFAULT_WORKER_ROUTER_PRINCIPLES = [
5
+ 'User-provided environment_rules are higher-priority routing guidance than these built-in defaults. Follow user guidance when it conflicts with a default unless doing so would violate an authoritative hard constraint.',
5
6
  'Worker Router chooses only the execution worker. Model Router chooses provider/model/profile after worker selection.',
6
7
  'Prefer ChatGPT when both workers can execute the task because it does not consume Codex coding-plan tokens and normally has access to a stronger ChatGPT Sol model.',
7
8
  'Prefer Codex when the task requires native/local execution or computer-use abilities that are unavailable through ChatGPT plus MacBridge.',
@@ -86,12 +87,8 @@ export function satisfiesWorker(candidate, requirements) {
86
87
  export function eligibleWorkerCandidates(candidates, requirements) {
87
88
  return candidates.filter(candidate => satisfiesWorker(candidate, requirements));
88
89
  }
89
- function deterministicWorker(eligible, chatgptRunning = 0, preferredChatgptConcurrency = DEFAULT_CHATGPT_PREFERRED_CONCURRENCY) {
90
- const chatgptAtPreferenceLimit = chatgptRunning >= preferredChatgptConcurrency;
91
- return [...eligible].map((candidate, index) => ({ candidate, index })).sort((left, right) => {
92
- const preference = (item) => chatgptAtPreferenceLimit ? (item.adapter === 'codex' ? 0 : 1) : (item.adapter === 'chatgpt' ? 0 : 1);
93
- return preference(left.candidate) - preference(right.candidate) || left.index - right.index;
94
- })[0].candidate;
90
+ function deterministicWorker(eligible) {
91
+ return eligible.find(candidate => candidate.adapter === 'codex') ?? eligible[0];
95
92
  }
96
93
  function selection(selected, selectedBy, decisionEvidence) {
97
94
  return {
@@ -125,7 +122,7 @@ export async function routeWorker(candidates, requirements, options = {}) {
125
122
  throw new Error('No eligible worker satisfies the authoritative worker constraints');
126
123
  const preferredChatgptConcurrency = options.preferredChatgptConcurrency ?? DEFAULT_CHATGPT_PREFERRED_CONCURRENCY;
127
124
  const chatgptRunning = options.chatgptRunning ?? 0;
128
- const deterministic = deterministicWorker(eligible, chatgptRunning, preferredChatgptConcurrency);
125
+ const deterministic = deterministicWorker(eligible);
129
126
  if (eligible.length === 1) {
130
127
  const evidence = { mode: options.decisionMode ?? 'off', fallback: true };
131
128
  return { candidate: deterministic, selection: selection(deterministic, 'worker-router:constraint', evidence) };
@@ -134,8 +131,9 @@ export async function routeWorker(candidates, requirements, options = {}) {
134
131
  const facts = JSON.parse(JSON.stringify({
135
132
  task: options.taskFacts ?? {},
136
133
  explicit_constraints: { runtime: requirements.runtime ?? null },
137
- routing_principles: DEFAULT_WORKER_ROUTER_PRINCIPLES,
134
+ decision_precedence: ['authoritative_hard_constraints', 'environment_rules', 'routing_principles', 'resource_and_runtime_preferences'],
138
135
  environment_rules: rules.text,
136
+ routing_principles: DEFAULT_WORKER_ROUTER_PRINCIPLES,
139
137
  usage_snapshot: options.usage ?? null,
140
138
  runtime_load: {
141
139
  chatgpt_running: chatgptRunning,
@@ -158,7 +156,7 @@ export async function routeWorker(candidates, requirements, options = {}) {
158
156
  }));
159
157
  const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', { facts }, {
160
158
  type: 'choice',
161
- question: 'Choose the best execution worker for this task. Use the task description, current usage, routing principles, and environment rules. Prefer ChatGPT when both can do the work; choose Codex when native/local execution is necessary. Do not choose a provider or model here.',
159
+ question: 'Choose the best execution worker for this task. Authoritative hard constraints come first. Then follow user-provided environment rules ahead of built-in routing principles when they conflict. Use task facts, reviewer-diversity context when present, current usage, and runtime load as supporting evidence. Do not choose a provider or model here.',
162
160
  choices: eligible.map(item => item.workerId),
163
161
  }, () => deterministic.workerId);
164
162
  const selected = eligible.find(item => item.workerId === choice.choice) ?? deterministic;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.127",
3
+ "version": "1.0.128",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,50 +0,0 @@
1
- function satisfies(candidate, requirements) {
2
- if (candidate.availability === 'offline')
3
- return false;
4
- if (requirements?.runtime && candidate.runtime !== requirements.runtime)
5
- return false;
6
- return true;
7
- }
8
- function producedWorkerIds(tasks) {
9
- return new Set(tasks
10
- .filter(task => task.agentName !== 'reviewer')
11
- .map(task => task.workerSelection?.workerId ?? task.adapter));
12
- }
13
- function producedRoutes(tasks) {
14
- return new Set(tasks
15
- .filter(task => task.agentName !== 'reviewer' && task.routing)
16
- .map(task => `${task.routing?.provider}\0${task.routing?.model}`));
17
- }
18
- /** Select a reviewer after authoritative worker constraints, then prefer execution diversity. */
19
- export function selectReviewer(candidates, implementationTasks, requirements) {
20
- const eligible = candidates.filter(candidate => satisfies(candidate, requirements));
21
- if (eligible.length === 0)
22
- throw new Error('No eligible reviewer worker satisfies the declared requirements');
23
- const usedWorkers = producedWorkerIds(implementationTasks);
24
- const usedRoutes = producedRoutes(implementationTasks);
25
- const ranked = eligible.map((candidate, index) => ({
26
- candidate,
27
- index,
28
- differentWorker: !usedWorkers.has(candidate.workerId),
29
- differentRoute: !usedRoutes.has(`${candidate.routing.provider}\0${candidate.routing.model}`),
30
- })).sort((left, right) => {
31
- if (left.differentWorker !== right.differentWorker)
32
- return left.differentWorker ? -1 : 1;
33
- if (left.differentRoute !== right.differentRoute)
34
- return left.differentRoute ? -1 : 1;
35
- return left.index - right.index;
36
- });
37
- const selected = ranked[0];
38
- const reason = selected.differentWorker ? 'worker-diversity' : selected.differentRoute ? 'provider-model-diversity' : 'eligible-fallback';
39
- return {
40
- candidate: selected.candidate,
41
- selection: {
42
- workerId: selected.candidate.workerId,
43
- adapter: selected.candidate.adapter,
44
- capabilities: [...selected.candidate.capabilities],
45
- selectedBy: `worker-router:reviewer-diversity:${reason}`,
46
- selectedAt: new Date().toISOString(),
47
- },
48
- reason,
49
- };
50
- }