agents-relay 2.0.28 → 2.0.29
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 +15 -6
- package/dist/execution-routing.js +98 -95
- package/package.json +1 -1
package/dist/decision-audit.js
CHANGED
|
@@ -135,12 +135,21 @@ export function makeExecutionDecisionAudit(input) {
|
|
|
135
135
|
fallback: { used: input.routing.decisionEvidence?.fallback ?? false, reason: input.routing.decisionEvidence?.error },
|
|
136
136
|
decision_evidence: input.routing.decisionEvidence,
|
|
137
137
|
decision_input: {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
type: 'staged_route_decision',
|
|
139
|
+
stages: input.decisionStages.map(stage => ({
|
|
140
|
+
name: stage.name,
|
|
141
|
+
facts: stage.facts,
|
|
142
|
+
contract: {
|
|
143
|
+
type: stage.contract.type,
|
|
144
|
+
question: stage.contract.question,
|
|
145
|
+
choices: [...stage.contract.choices],
|
|
146
|
+
},
|
|
147
|
+
result: {
|
|
148
|
+
choice: stage.result.choice,
|
|
149
|
+
probabilities: stage.result.probabilities ?? null,
|
|
150
|
+
bypassed: stage.result.bypassed ?? false,
|
|
151
|
+
},
|
|
152
|
+
})),
|
|
144
153
|
},
|
|
145
154
|
worker_candidates: input.workers.map(workerAuditCandidate),
|
|
146
155
|
worker_gate_results: input.workerGateEvaluations.map(item => ({
|
|
@@ -59,67 +59,6 @@ function modelGateReasons(candidate, constraints, excludedRoutes, policy) {
|
|
|
59
59
|
reasons.push('reasoning_constraint');
|
|
60
60
|
return reasons;
|
|
61
61
|
}
|
|
62
|
-
const REASONING_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
63
|
-
function setupName(worker, model) {
|
|
64
|
-
if (worker.adapter === 'chatgpt')
|
|
65
|
-
return 'account-defaults';
|
|
66
|
-
if (model.id.startsWith('default:') || model.id.startsWith('catalog:'))
|
|
67
|
-
return 'default';
|
|
68
|
-
return model.profile ?? model.provider;
|
|
69
|
-
}
|
|
70
|
-
function setupKey(worker, model) {
|
|
71
|
-
return worker.workerId + '/' + setupName(worker, model);
|
|
72
|
-
}
|
|
73
|
-
function configuredDefault(models, setup) {
|
|
74
|
-
if (setup === 'account-defaults')
|
|
75
|
-
return models[0];
|
|
76
|
-
if (setup === 'default')
|
|
77
|
-
return models.find(model => model.id.startsWith('default:'));
|
|
78
|
-
return models.find(model => model.profile === setup && model.id.startsWith(setup + ':'));
|
|
79
|
-
}
|
|
80
|
-
function resolveModelAlias(raw, models, familyHint) {
|
|
81
|
-
const normalized = raw.trim().toLowerCase();
|
|
82
|
-
const exact = models.find(model => model.model.toLowerCase() === normalized);
|
|
83
|
-
if (exact)
|
|
84
|
-
return exact.model;
|
|
85
|
-
if (familyHint) {
|
|
86
|
-
const sameFamily = models.find(model => model.model.toLowerCase() === familyHint.toLowerCase() + '-' + normalized);
|
|
87
|
-
if (sameFamily)
|
|
88
|
-
return sameFamily.model;
|
|
89
|
-
}
|
|
90
|
-
const suffix = [...new Set(models.filter(model => model.model.toLowerCase().endsWith('-' + normalized)).map(model => model.model))];
|
|
91
|
-
return suffix.length === 1 ? suffix[0] : raw.trim();
|
|
92
|
-
}
|
|
93
|
-
function userModelOverrides(rulesText, defaults, models) {
|
|
94
|
-
const result = [];
|
|
95
|
-
for (const line of rulesText.split(/\r?\n/)) {
|
|
96
|
-
const match = line.match(/^\s*([a-z0-9._-]+)\s*:\s*(.+)$/i);
|
|
97
|
-
if (!match)
|
|
98
|
-
continue;
|
|
99
|
-
const label = match[1].toLowerCase();
|
|
100
|
-
const base = defaults.find(model => (model.profile ?? (model.provider === 'openai' ? 'default' : model.provider)).toLowerCase() === label || model.provider.toLowerCase() === label);
|
|
101
|
-
if (!base)
|
|
102
|
-
continue;
|
|
103
|
-
const sameSetupModels = models.filter(model => model.provider === base.provider && (model.profile ?? null) === (base.profile ?? null));
|
|
104
|
-
const familyHint = match[2].match(/\b(gpt-\d+(?:\.\d+)?)-[a-z0-9.-]+/i)?.[1];
|
|
105
|
-
for (const segment of match[2].split(',')) {
|
|
106
|
-
const item = segment.trim().match(/^(.+?)\s+(low|medium|high|xhigh|max|ultra)(?:\s|$)/i);
|
|
107
|
-
if (!item)
|
|
108
|
-
continue;
|
|
109
|
-
const model = resolveModelAlias(item[1].trim(), sameSetupModels, familyHint);
|
|
110
|
-
const reasoning = item[2].toLowerCase();
|
|
111
|
-
if (result.some(candidate => candidate.provider === base.provider && candidate.model === model && (candidate.profile ?? null) === (base.profile ?? null) && candidate.reasoning === reasoning))
|
|
112
|
-
continue;
|
|
113
|
-
result.push({
|
|
114
|
-
...base,
|
|
115
|
-
id: (base.profile ?? 'default') + ':user/' + model,
|
|
116
|
-
model,
|
|
117
|
-
reasoning,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
62
|
function codePlan(model) {
|
|
124
63
|
return {
|
|
125
64
|
five_hour: model.resourceWindows?.fiveHour ? {
|
|
@@ -152,21 +91,32 @@ function workerProviderFacts(routes) {
|
|
|
152
91
|
id: workerId,
|
|
153
92
|
adapter: workerRoutes[0].worker.adapter,
|
|
154
93
|
providers: Object.fromEntries([...providers.entries()].map(([provider, providerRoutes]) => {
|
|
155
|
-
const
|
|
156
|
-
const configured = models.find(model => !model.id.includes(':user/')) ?? models[0];
|
|
94
|
+
const configured = providerRoutes.find(route => !route.model.id.startsWith('catalog:')) ?? providerRoutes[0];
|
|
157
95
|
return [provider, {
|
|
158
|
-
default: { model: configured.model, reasoning: configured.reasoning ?? null },
|
|
159
|
-
code_plan: codePlan(configured),
|
|
96
|
+
default: { model: configured.model.model, reasoning: configured.model.reasoning ?? null },
|
|
97
|
+
code_plan: codePlan(configured.model),
|
|
160
98
|
}];
|
|
161
99
|
})),
|
|
162
100
|
};
|
|
163
101
|
});
|
|
164
102
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
103
|
+
async function decideStage(engine, name, facts, question, choices) {
|
|
104
|
+
if (choices.length === 0)
|
|
105
|
+
throw new Error('No choices available for ' + name + ' stage');
|
|
106
|
+
const contract = { type: 'choice', question, choices };
|
|
107
|
+
if (choices.length === 1)
|
|
108
|
+
return { name, facts, contract, result: { choice: choices[0], bypassed: true } };
|
|
109
|
+
const decision = await engine.decide({ facts }, contract);
|
|
110
|
+
if (decision.type !== 'choice')
|
|
111
|
+
throw new Error('Execution router expected a choice result for ' + name);
|
|
112
|
+
return { name, facts, contract, result: { choice: decision.choice, probabilities: decision.probabilities } };
|
|
113
|
+
}
|
|
114
|
+
function stageFacts(baseFacts, selected, candidates) {
|
|
115
|
+
return JSON.parse(JSON.stringify({
|
|
116
|
+
...baseFacts,
|
|
117
|
+
selected,
|
|
118
|
+
...candidates,
|
|
119
|
+
}));
|
|
170
120
|
}
|
|
171
121
|
function routeId(worker, model) {
|
|
172
122
|
return worker.workerId + '::' + model.id + (model.reasoning ? '@' + model.reasoning : '');
|
|
@@ -203,11 +153,7 @@ export async function discoverExecutionRoutes(workers, requirements, options = {
|
|
|
203
153
|
const evaluations = [];
|
|
204
154
|
for (const worker of workers) {
|
|
205
155
|
const workerReasons = workerGateEvaluations.find(item => item.workerId === worker.workerId)?.rejectionReasons ?? [];
|
|
206
|
-
const
|
|
207
|
-
const configuredModels = discoveredModels.filter(model => !model.id.startsWith('catalog:'));
|
|
208
|
-
const defaults = configuredModels.filter(model => model.id.startsWith('default:') || (model.profile && model.id.startsWith(model.profile + ':')) || worker.adapter === 'chatgpt');
|
|
209
|
-
const advertised = userModelOverrides(policy.rules.text, defaults, discoveredModels);
|
|
210
|
-
const models = [...configuredModels, ...advertised];
|
|
156
|
+
const models = await discoverModelCandidates(worker, registered, options.usage, undefined, options.rulesPath);
|
|
211
157
|
for (const model of models) {
|
|
212
158
|
const rejectionReasons = [...workerReasons, ...modelGateReasons(model, options.routingConstraints, options.excludedRoutes, policy)];
|
|
213
159
|
const route = { id: routeId(worker, model), worker, model };
|
|
@@ -228,7 +174,7 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
228
174
|
const chatgptRunning = options.chatgptRunning ?? 0;
|
|
229
175
|
const taskFacts = options.taskFacts ?? {};
|
|
230
176
|
const taskShape = executionTaskShape(taskFacts);
|
|
231
|
-
const
|
|
177
|
+
const baseFacts = JSON.parse(JSON.stringify({
|
|
232
178
|
context: {
|
|
233
179
|
job: taskFacts.job ?? null,
|
|
234
180
|
task: taskFacts.task ?? taskFacts,
|
|
@@ -249,31 +195,89 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
249
195
|
preferred_chatgpt_concurrency: preferredChatgptConcurrency,
|
|
250
196
|
},
|
|
251
197
|
worker_candidates: workerProviderFacts(routes),
|
|
252
|
-
routing_instruction: '
|
|
198
|
+
routing_instruction: 'Interpret user_input_rules directly. Choose among the runtime-provided legal candidates at each stage. Relay does not parse user_input_rules into model or reasoning choices.',
|
|
253
199
|
}));
|
|
254
|
-
const decisionChoices = routes.map(decisionRouteLabel);
|
|
255
|
-
const decisionRequest = { facts };
|
|
256
|
-
const decisionContract = {
|
|
257
|
-
type: 'choice',
|
|
258
|
-
question: 'Choose one legal route. Each provider default is available. Additional model/reasoning routes exist only when explicitly provided by user_input_rules. Use context, code-plan quota, runtime load, and user_input_rules.',
|
|
259
|
-
choices: decisionChoices,
|
|
260
|
-
};
|
|
261
200
|
let selected = fallback;
|
|
262
201
|
let evidence = { mode: options.decisionMode ?? 'off', fallback: true };
|
|
202
|
+
const decisionStages = [];
|
|
263
203
|
if ((options.decisionMode ?? 'off') !== 'off' && options.decisionEngine) {
|
|
264
204
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
205
|
+
const providerKeys = [...new Set(routes.map(route => route.worker.workerId + '::' + route.model.provider))];
|
|
206
|
+
const providerOptions = providerKeys.map(key => {
|
|
207
|
+
const splitAt = key.lastIndexOf('::');
|
|
208
|
+
const worker = key.slice(0, splitAt);
|
|
209
|
+
const provider = key.slice(splitAt + 2);
|
|
210
|
+
const providerRoutes = routes.filter(route => route.worker.workerId === worker && route.model.provider === provider);
|
|
211
|
+
const configured = providerRoutes.find(route => !route.model.id.startsWith('catalog:')) ?? providerRoutes[0];
|
|
212
|
+
return {
|
|
213
|
+
key,
|
|
214
|
+
worker,
|
|
215
|
+
provider,
|
|
216
|
+
default_model: configured.model.model,
|
|
217
|
+
label: 'worker=' + worker + ' provider=' + provider + ' default_model=' + configured.model.model,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
const providerFacts = stageFacts(baseFacts, {}, {
|
|
221
|
+
provider_candidates: providerOptions.map(({ label: _label, key: _key, ...option }) => option),
|
|
222
|
+
});
|
|
223
|
+
const providerStage = await decideStage(options.decisionEngine, 'provider', providerFacts, 'Choose the best worker/provider pair. Interpret user_input_rules directly together with task context, runtime load, provider defaults, and code-plan quota. When user_input_rules explicitly names providers as routing options, prefer those named providers when applicable; treat unmentioned providers as fallbacks unless other facts clearly require them.', providerOptions.map(option => option.label));
|
|
224
|
+
decisionStages.push(providerStage);
|
|
225
|
+
const selectedProvider = providerOptions.find(option => option.label === providerStage.result.choice);
|
|
226
|
+
if (!selectedProvider)
|
|
227
|
+
throw new Error('Laya selected an unknown worker/provider option');
|
|
228
|
+
const workerId = selectedProvider.worker;
|
|
229
|
+
const provider = selectedProvider.provider;
|
|
230
|
+
const providerRoutes = routes.filter(route => route.worker.workerId === workerId && route.model.provider === provider);
|
|
231
|
+
if (providerRoutes.length === 0)
|
|
232
|
+
throw new Error('Laya selected a worker/provider pair with no eligible routes');
|
|
233
|
+
const modelGroups = new Map();
|
|
234
|
+
for (const route of providerRoutes) {
|
|
235
|
+
const group = modelGroups.get(route.model.model) ?? [];
|
|
236
|
+
group.push(route);
|
|
237
|
+
modelGroups.set(route.model.model, group);
|
|
238
|
+
}
|
|
239
|
+
const modelChoices = [...modelGroups.keys()];
|
|
240
|
+
const modelOptions = modelChoices.map(model => {
|
|
241
|
+
const group = modelGroups.get(model) ?? [];
|
|
242
|
+
const configured = group.some(route => !route.model.id.startsWith('catalog:'));
|
|
243
|
+
return {
|
|
244
|
+
model,
|
|
245
|
+
configured_default: configured,
|
|
246
|
+
supported_reasoning: [...new Set(group.map(route => route.model.reasoning).filter((value) => Boolean(value)))],
|
|
247
|
+
label: 'model=' + model + ' configured_default=' + String(configured),
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
const modelFacts = stageFacts(baseFacts, { worker: workerId, provider }, {
|
|
251
|
+
model_candidates: modelOptions.map(({ label: _label, ...option }) => option),
|
|
252
|
+
});
|
|
253
|
+
const modelStage = await decideStage(options.decisionEngine, 'model', modelFacts, 'Choose the best model for the selected worker/provider. Interpret user_input_rules directly. Candidate model names come from current runtime/configuration and are not derived from the free-text rules. If user_input_rules explicitly maps a model to the current task type, size, complexity, or other current condition, follow that mapping instead of the provider default.', modelOptions.map(option => option.label));
|
|
254
|
+
decisionStages.push(modelStage);
|
|
255
|
+
const selectedModel = modelOptions.find(option => option.label === modelStage.result.choice);
|
|
256
|
+
if (!selectedModel)
|
|
257
|
+
throw new Error('Laya selected an unknown model option');
|
|
258
|
+
const modelRoutes = modelGroups.get(selectedModel.model) ?? [];
|
|
259
|
+
const reasoningChoices = [...new Set(modelRoutes.map(route => route.model.reasoning ?? 'provider-default'))];
|
|
260
|
+
const reasoningOptions = reasoningChoices.map(reasoning => ({
|
|
261
|
+
reasoning,
|
|
262
|
+
label: 'reasoning=' + reasoning,
|
|
263
|
+
}));
|
|
264
|
+
const reasoningFacts = stageFacts(baseFacts, { worker: workerId, provider, model: selectedModel.model }, {
|
|
265
|
+
reasoning_candidates: reasoningChoices,
|
|
266
|
+
});
|
|
267
|
+
const reasoningStage = await decideStage(options.decisionEngine, 'reasoning', reasoningFacts, 'Choose the reasoning level for the selected model. Interpret user_input_rules directly. If user_input_rules explicitly assigns a reasoning level to the selected model for the current conditions, follow that assignment. Use provider-default only when the model does not expose a specific reasoning level.', reasoningOptions.map(option => option.label));
|
|
268
|
+
decisionStages.push(reasoningStage);
|
|
269
|
+
const selectedReasoning = reasoningOptions.find(option => option.label === reasoningStage.result.choice);
|
|
270
|
+
if (!selectedReasoning)
|
|
271
|
+
throw new Error('Laya selected an unknown reasoning option');
|
|
272
|
+
const reasoning = selectedReasoning.reasoning === 'provider-default' ? null : selectedReasoning.reasoning;
|
|
273
|
+
const proposed = modelRoutes.find(route => (route.model.reasoning ?? null) === reasoning);
|
|
274
|
+
if (!proposed)
|
|
275
|
+
throw new Error('Laya selected a model/reasoning combination with no eligible route');
|
|
276
|
+
selected = proposed;
|
|
272
277
|
evidence = {
|
|
273
278
|
mode: options.decisionMode ?? 'active',
|
|
274
279
|
engine: options.decisionEngine.name,
|
|
275
|
-
proposed:
|
|
276
|
-
probabilities: decision.probabilities,
|
|
280
|
+
proposed: providerStage.result.choice + '/' + selectedModel.model + '@' + selectedReasoning.reasoning,
|
|
277
281
|
proposedFields: {
|
|
278
282
|
worker: selected.worker.workerId,
|
|
279
283
|
provider: selected.model.provider,
|
|
@@ -319,8 +323,7 @@ export async function routeExecution(workers, requirements, options = {}) {
|
|
|
319
323
|
},
|
|
320
324
|
policy,
|
|
321
325
|
routing,
|
|
322
|
-
|
|
323
|
-
decisionContract,
|
|
326
|
+
decisionStages,
|
|
324
327
|
});
|
|
325
328
|
routing.decisionId = audit.decision_id;
|
|
326
329
|
if (options.auditStore)
|