agents-relay 2.0.11 → 2.0.13

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,7 +1,7 @@
1
1
  import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
3
  import { randomUUID, createHash } from 'node:crypto';
4
- export const DECISION_AUDIT_SCHEMA_VERSION = 2;
4
+ export const DECISION_AUDIT_SCHEMA_VERSION = 5;
5
5
  export class JsonlDecisionAuditStore {
6
6
  path;
7
7
  constructor(path) {
@@ -59,3 +59,93 @@ export function makeDecisionAudit(input) {
59
59
  });
60
60
  return { record_type: 'decision', schema_version: DECISION_AUDIT_SCHEMA_VERSION, decision_id: decisionAuditId(), prior_decision_id: input.priorDecisionId, timestamp: input.routing.decidedAt, correlation: { job_id: input.task?.jobId, task_id: input.task?.id, execution_id: input.task?.executionId, attempt: input.task?.attempt }, task: taskAuditFacts(input.task), worker: input.worker, contract: input.contract, policy: { path: input.policy.path, hash: input.policy.hash, snapshot: input.policy.snapshot, rules: input.policy.rules }, candidates: input.candidates.map(candidate), gate_results, eligible_candidates: input.eligibleCandidates.map(candidate), selected: candidate(input.selected), engine: input.routing.decidedBy, fallback: { used: input.routing.decisionEvidence?.fallback ?? false, reason: input.routing.decisionEvidence?.error }, decision_evidence: input.routing.decisionEvidence, prior_failure: input.priorFailure };
61
61
  }
62
+ function workerAuditCandidate(worker) {
63
+ return {
64
+ id: worker.workerId,
65
+ adapter: worker.adapter,
66
+ runtime: worker.runtime ?? null,
67
+ availability: worker.availability ?? null,
68
+ capabilities: [...worker.capabilities],
69
+ trustDomains: worker.trustDomains ?? [],
70
+ region: worker.region ?? null,
71
+ labels: worker.labels ?? {},
72
+ resourceProvider: worker.resourceProvider ?? null,
73
+ resourceHealthy: worker.resourceHealthy ?? null,
74
+ resourceUsedPercent: worker.resourceUsedPercent ?? null,
75
+ resourceRemainingPercent: worker.resourceRemainingPercent ?? null,
76
+ resourceResetAt: worker.resourceResetAt ?? null,
77
+ resourceResetInMinutes: worker.resourceResetInMinutes ?? null,
78
+ resourceWindows: worker.resourceWindows ?? null,
79
+ browserWorkspaceCapacity: worker.browserWorkspaceCapacity ?? null,
80
+ };
81
+ }
82
+ function executionRouteAuditCandidate(route) {
83
+ return {
84
+ id: route.id,
85
+ worker: workerAuditCandidate(route.worker),
86
+ model: {
87
+ id: route.model.id,
88
+ provider: route.model.provider,
89
+ model: route.model.model,
90
+ profile: route.model.profile ?? null,
91
+ reasoning: route.model.reasoning ?? null,
92
+ resourceHealthy: route.model.resourceHealthy ?? null,
93
+ resourceUsedPercent: route.model.resourceUsedPercent ?? null,
94
+ resourceRemainingPercent: route.model.resourceRemainingPercent ?? null,
95
+ resourceResetAt: route.model.resourceResetAt ?? null,
96
+ resourceResetInMinutes: route.model.resourceResetInMinutes ?? null,
97
+ resourceWindows: route.model.resourceWindows ?? null,
98
+ },
99
+ };
100
+ }
101
+ export function makeExecutionDecisionAudit(input) {
102
+ const routeRecords = input.evaluations.map(item => executionRouteAuditCandidate(item.route));
103
+ const eligibleRouteRecords = input.evaluations.filter(item => item.eligible).map(item => executionRouteAuditCandidate(item.route));
104
+ const selectedRecord = executionRouteAuditCandidate(input.selected);
105
+ return {
106
+ record_type: 'decision',
107
+ schema_version: DECISION_AUDIT_SCHEMA_VERSION,
108
+ decision_id: decisionAuditId(),
109
+ timestamp: input.routing.decidedAt,
110
+ correlation: {
111
+ job_id: input.task?.jobId,
112
+ task_id: input.task?.id,
113
+ execution_id: input.task?.executionId,
114
+ attempt: input.task?.attempt,
115
+ },
116
+ task: taskAuditFacts(input.task),
117
+ worker: workerAuditCandidate(input.selected.worker),
118
+ contract: input.contract,
119
+ policy: {
120
+ path: input.policy.path,
121
+ hash: input.policy.hash,
122
+ snapshot: input.policy.snapshot,
123
+ rules: input.policy.rules,
124
+ },
125
+ candidates: routeRecords,
126
+ gate_results: input.evaluations.map(item => ({
127
+ candidate_id: item.route.id,
128
+ eligible: item.eligible,
129
+ rejection_reasons: [...item.rejectionReasons],
130
+ })),
131
+ eligible_candidates: eligibleRouteRecords,
132
+ selected: selectedRecord,
133
+ engine: input.routing.decidedBy,
134
+ fallback: { used: input.routing.decisionEvidence?.fallback ?? false, reason: input.routing.decisionEvidence?.error },
135
+ decision_evidence: input.routing.decisionEvidence,
136
+ decision_input: {
137
+ facts: input.decisionRequest.facts,
138
+ contract: {
139
+ type: input.decisionContract.type,
140
+ question: input.decisionContract.question,
141
+ choices: [...input.decisionContract.choices],
142
+ },
143
+ },
144
+ worker_candidates: input.workers.map(workerAuditCandidate),
145
+ worker_gate_results: input.workerGateEvaluations.map(item => ({
146
+ worker_id: item.workerId,
147
+ eligible: item.eligible,
148
+ rejection_reasons: [...item.rejectionReasons],
149
+ })),
150
+ };
151
+ }
@@ -1,29 +1,44 @@
1
1
  import { chooseWithDecision } from './decision.js';
2
- import { makeDecisionAudit } from './decision-audit.js';
2
+ import { makeExecutionDecisionAudit } from './decision-audit.js';
3
3
  import { discoverModelCandidates } from './model-routing.js';
4
4
  import { loadExecutionRouterRules, loadRoutingPolicy, policyAllows } from './routing-policy.js';
5
- import { DEFAULT_CHATGPT_PREFERRED_CONCURRENCY, eligibleWorkerCandidates } from './worker-routing.js';
5
+ import { DEFAULT_CHATGPT_PREFERRED_CONCURRENCY } from './worker-routing.js';
6
6
  function sameModelRoute(candidate, route) {
7
7
  return (!route.provider || candidate.provider === route.provider)
8
8
  && (!route.model || candidate.model === route.model)
9
9
  && (route.profile === undefined || candidate.profile === route.profile);
10
10
  }
11
- function modelUsable(candidate, constraints, excludedRoutes, policy) {
11
+ function workerGateReasons(worker, requirements) {
12
+ const reasons = [];
13
+ if (worker.availability === 'offline')
14
+ reasons.push('worker_offline');
15
+ if (requirements.runtime && worker.runtime !== requirements.runtime)
16
+ reasons.push('runtime_constraint');
17
+ if (worker.adapter === 'chatgpt' && worker.browserWorkspaceCapacity?.available === 0)
18
+ reasons.push('browser_workspace_exhausted');
19
+ return reasons;
20
+ }
21
+ function modelGateReasons(candidate, constraints, excludedRoutes, policy) {
22
+ const reasons = [];
12
23
  if (candidate.resourceHealthy === false)
13
- return false;
24
+ reasons.push('resource_unhealthy');
14
25
  if ((excludedRoutes ?? []).some(route => sameModelRoute(candidate, route)))
15
- return false;
16
- if (!policyAllows(candidate, policy.policy))
17
- return false;
26
+ reasons.push('prior_failed_route');
27
+ if (!policyAllows(candidate, policy.policy)) {
28
+ if (policy.policy.disabledProviders?.includes(candidate.provider))
29
+ reasons.push('policy_disabled_provider');
30
+ if (candidate.profile && policy.policy.disabledProfiles?.includes(candidate.profile))
31
+ reasons.push('policy_disabled_profile');
32
+ }
18
33
  if (constraints?.provider && candidate.provider !== constraints.provider)
19
- return false;
34
+ reasons.push('provider_constraint');
20
35
  if (constraints?.model && candidate.model !== constraints.model)
21
- return false;
36
+ reasons.push('model_constraint');
22
37
  if (constraints?.profile && candidate.profile !== constraints.profile)
23
- return false;
38
+ reasons.push('profile_constraint');
24
39
  if (constraints?.reasoning && candidate.reasoning && candidate.reasoning !== constraints.reasoning)
25
- return false;
26
- return true;
40
+ reasons.push('reasoning_constraint');
41
+ return reasons;
27
42
  }
28
43
  function routeId(worker, model) {
29
44
  return `${worker.workerId}::${model.id}`;
@@ -51,22 +66,27 @@ function workerSelection(worker, selectedBy, evidence) {
51
66
  };
52
67
  }
53
68
  export async function discoverExecutionRoutes(workers, requirements, options = {}) {
54
- const legalWorkers = eligibleWorkerCandidates(workers, requirements);
55
69
  const registered = options.registered ?? [];
56
70
  const policy = await loadRoutingPolicy(options.policyPath, options.rulesPath);
57
- const routes = [];
58
- for (const worker of legalWorkers) {
71
+ const workerGateEvaluations = workers.map(worker => {
72
+ const rejectionReasons = workerGateReasons(worker, requirements);
73
+ return { workerId: worker.workerId, eligible: rejectionReasons.length === 0, rejectionReasons };
74
+ });
75
+ const evaluations = [];
76
+ for (const worker of workers) {
77
+ const workerReasons = workerGateEvaluations.find(item => item.workerId === worker.workerId)?.rejectionReasons ?? [];
59
78
  const models = await discoverModelCandidates(worker, registered, options.usage, undefined, options.rulesPath);
60
79
  for (const model of models) {
61
- if (!modelUsable(model, options.routingConstraints, options.excludedRoutes, policy))
62
- continue;
63
- routes.push({ id: routeId(worker, model), worker, model });
80
+ const rejectionReasons = [...workerReasons, ...modelGateReasons(model, options.routingConstraints, options.excludedRoutes, policy)];
81
+ const route = { id: routeId(worker, model), worker, model };
82
+ evaluations.push({ route, eligible: rejectionReasons.length === 0, rejectionReasons });
64
83
  }
65
84
  }
66
- return { routes, policy };
85
+ const routes = evaluations.filter(item => item.eligible).map(item => item.route);
86
+ return { routes, evaluations, workerGateEvaluations, policy };
67
87
  }
68
88
  export async function routeExecution(workers, requirements, options = {}) {
69
- const { routes, policy } = await discoverExecutionRoutes(workers, requirements, options);
89
+ const { routes, evaluations, workerGateEvaluations, policy } = await discoverExecutionRoutes(workers, requirements, options);
70
90
  if (routes.length === 0)
71
91
  throw new Error('No executable worker+model route satisfies authoritative constraints');
72
92
  const fallback = fallbackRoute(routes);
@@ -118,11 +138,13 @@ export async function routeExecution(workers, requirements, options = {}) {
118
138
  },
119
139
  })),
120
140
  }));
121
- const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', { facts }, {
141
+ const decisionRequest = { facts };
142
+ const decisionContract = {
122
143
  type: 'choice',
123
144
  question: 'Choose the best complete execution route for this task. The listed candidates are already executable. Treat all non-impossibility signals as soft facts, follow free-text user guidance, and choose worker+model together in this one decision.',
124
145
  choices: routes.map(route => route.id),
125
- }, () => fallback.id);
146
+ };
147
+ const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', decisionRequest, decisionContract, () => fallback.id);
126
148
  const selected = routes.find(route => route.id === choice.choice) ?? fallback;
127
149
  const selectedBy = choice.evidence.engine && !choice.evidence.fallback ? 'execution-router:laya' : 'execution-router:fallback';
128
150
  const routing = {
@@ -134,9 +156,13 @@ export async function routeExecution(workers, requirements, options = {}) {
134
156
  decidedAt: new Date().toISOString(),
135
157
  decisionEvidence: choice.evidence,
136
158
  };
137
- const audit = makeDecisionAudit({
159
+ const audit = makeExecutionDecisionAudit({
138
160
  task: options.auditTask,
139
- worker: { id: selected.worker.workerId, adapter: selected.worker.adapter, capabilities: [...selected.worker.capabilities], route_id: selected.id },
161
+ workers,
162
+ requirements,
163
+ evaluations,
164
+ workerGateEvaluations,
165
+ selected,
140
166
  contract: {
141
167
  question: 'execution-route-selection',
142
168
  task: options.taskFacts ?? {},
@@ -146,10 +172,9 @@ export async function routeExecution(workers, requirements, options = {}) {
146
172
  candidateRoutes: routes.map(route => ({ id: route.id, workerId: route.worker.workerId, adapter: route.worker.adapter })),
147
173
  },
148
174
  policy,
149
- candidates: routes.map(route => ({ ...route.model, id: route.id })),
150
- eligibleCandidates: routes.map(route => ({ ...route.model, id: route.id })),
151
- selected: { ...selected.model, id: selected.id },
152
175
  routing,
176
+ decisionRequest,
177
+ decisionContract,
153
178
  });
154
179
  routing.decisionId = audit.decision_id;
155
180
  if (options.auditStore)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.11",
3
+ "version": "2.0.13",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {