agents-relay 2.0.12 → 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.
- package/dist/decision-audit.js +22 -16
- package/dist/execution-routing.js +47 -22
- package/package.json +1 -1
package/dist/decision-audit.js
CHANGED
|
@@ -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 =
|
|
4
|
+
export const DECISION_AUDIT_SCHEMA_VERSION = 5;
|
|
5
5
|
export class JsonlDecisionAuditStore {
|
|
6
6
|
path;
|
|
7
7
|
constructor(path) {
|
|
@@ -79,16 +79,6 @@ function workerAuditCandidate(worker) {
|
|
|
79
79
|
browserWorkspaceCapacity: worker.browserWorkspaceCapacity ?? null,
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
function workerGateResult(worker, requirements) {
|
|
83
|
-
const rejectionReasons = [];
|
|
84
|
-
if (worker.availability === 'offline')
|
|
85
|
-
rejectionReasons.push('worker_offline');
|
|
86
|
-
if (requirements.runtime && worker.runtime !== requirements.runtime)
|
|
87
|
-
rejectionReasons.push('runtime_constraint');
|
|
88
|
-
if (worker.adapter === 'chatgpt' && worker.browserWorkspaceCapacity?.available === 0)
|
|
89
|
-
rejectionReasons.push('browser_workspace_exhausted');
|
|
90
|
-
return { worker_id: worker.workerId, eligible: rejectionReasons.length === 0, rejection_reasons: rejectionReasons };
|
|
91
|
-
}
|
|
92
82
|
function executionRouteAuditCandidate(route) {
|
|
93
83
|
return {
|
|
94
84
|
id: route.id,
|
|
@@ -109,9 +99,9 @@ function executionRouteAuditCandidate(route) {
|
|
|
109
99
|
};
|
|
110
100
|
}
|
|
111
101
|
export function makeExecutionDecisionAudit(input) {
|
|
112
|
-
const routeRecords = 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));
|
|
113
104
|
const selectedRecord = executionRouteAuditCandidate(input.selected);
|
|
114
|
-
const workerGateResults = input.workers.map(worker => workerGateResult(worker, input.requirements));
|
|
115
105
|
return {
|
|
116
106
|
record_type: 'decision',
|
|
117
107
|
schema_version: DECISION_AUDIT_SCHEMA_VERSION,
|
|
@@ -133,13 +123,29 @@ export function makeExecutionDecisionAudit(input) {
|
|
|
133
123
|
rules: input.policy.rules,
|
|
134
124
|
},
|
|
135
125
|
candidates: routeRecords,
|
|
136
|
-
gate_results:
|
|
137
|
-
|
|
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,
|
|
138
132
|
selected: selectedRecord,
|
|
139
133
|
engine: input.routing.decidedBy,
|
|
140
134
|
fallback: { used: input.routing.decisionEvidence?.fallback ?? false, reason: input.routing.decisionEvidence?.error },
|
|
141
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
|
+
},
|
|
142
144
|
worker_candidates: input.workers.map(workerAuditCandidate),
|
|
143
|
-
worker_gate_results:
|
|
145
|
+
worker_gate_results: input.workerGateEvaluations.map(item => ({
|
|
146
|
+
worker_id: item.workerId,
|
|
147
|
+
eligible: item.eligible,
|
|
148
|
+
rejection_reasons: [...item.rejectionReasons],
|
|
149
|
+
})),
|
|
144
150
|
};
|
|
145
151
|
}
|
|
@@ -2,28 +2,43 @@ import { chooseWithDecision } from './decision.js';
|
|
|
2
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
|
|
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
|
|
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
|
-
|
|
24
|
+
reasons.push('resource_unhealthy');
|
|
14
25
|
if ((excludedRoutes ?? []).some(route => sameModelRoute(candidate, route)))
|
|
15
|
-
|
|
16
|
-
if (!policyAllows(candidate, policy.policy))
|
|
17
|
-
|
|
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
|
-
|
|
34
|
+
reasons.push('provider_constraint');
|
|
20
35
|
if (constraints?.model && candidate.model !== constraints.model)
|
|
21
|
-
|
|
36
|
+
reasons.push('model_constraint');
|
|
22
37
|
if (constraints?.profile && candidate.profile !== constraints.profile)
|
|
23
|
-
|
|
38
|
+
reasons.push('profile_constraint');
|
|
24
39
|
if (constraints?.reasoning && candidate.reasoning && candidate.reasoning !== constraints.reasoning)
|
|
25
|
-
|
|
26
|
-
return
|
|
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
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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 = {
|
|
@@ -138,7 +160,8 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
138
160
|
task: options.auditTask,
|
|
139
161
|
workers,
|
|
140
162
|
requirements,
|
|
141
|
-
|
|
163
|
+
evaluations,
|
|
164
|
+
workerGateEvaluations,
|
|
142
165
|
selected,
|
|
143
166
|
contract: {
|
|
144
167
|
question: 'execution-route-selection',
|
|
@@ -150,6 +173,8 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
150
173
|
},
|
|
151
174
|
policy,
|
|
152
175
|
routing,
|
|
176
|
+
decisionRequest,
|
|
177
|
+
decisionContract,
|
|
153
178
|
});
|
|
154
179
|
routing.decisionId = audit.decision_id;
|
|
155
180
|
if (options.auditStore)
|