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.
- package/dist/decision-audit.js +91 -1
- package/dist/execution-routing.js +52 -27
- 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) {
|
|
@@ -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 {
|
|
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 = {
|
|
@@ -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 =
|
|
159
|
+
const audit = makeExecutionDecisionAudit({
|
|
138
160
|
task: options.auditTask,
|
|
139
|
-
|
|
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)
|