@noetic-tools/core 5.5.0 → 5.6.0
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/builders/control-flow-builders.d.ts +6 -4
- package/dist/builders/control-flow-builders.js +3 -2
- package/dist/builders/decide-builders.d.ts +86 -0
- package/dist/builders/decide-builders.js +230 -0
- package/dist/builders/dynamic-workflow.js +1 -0
- package/dist/builders/workflow-hydrator.js +81 -2
- package/dist/conditions/conditions.d.ts +9 -3
- package/dist/conditions/conditions.js +84 -25
- package/dist/harness/agent-harness.d.ts +16 -3
- package/dist/harness/agent-harness.js +63 -6
- package/dist/harness/decide-call.d.ts +19 -0
- package/dist/harness/decide-call.js +483 -0
- package/dist/harness/deps/adapters.d.ts +1 -0
- package/dist/harness/deps/adapters.js +1 -0
- package/dist/harness/deps/context.d.ts +1 -1
- package/dist/harness/deps/context.js +1 -1
- package/dist/harness/deps/types.d.ts +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.js +5 -1
- package/dist/interpreter/action-deps.d.ts +1 -1
- package/dist/interpreter/action-deps.js +1 -1
- package/dist/interpreter/action-types.d.ts +1 -1
- package/dist/interpreter/collect-tools.js +2 -0
- package/dist/interpreter/execute-control.js +1 -1
- package/dist/interpreter/execute-decide.d.ts +10 -0
- package/dist/interpreter/execute-decide.js +417 -0
- package/dist/interpreter/execute.js +8 -3
- package/dist/observability/genai-attributes.d.ts +6 -0
- package/dist/observability/genai-attributes.js +6 -0
- package/dist/portable/context-layers.d.ts +1 -1
- package/dist/portable/context-layers.js +1 -1
- package/dist/schemas/workflow.d.ts +1 -1
- package/dist/util/status-code.d.ts +2 -0
- package/dist/util/status-code.js +7 -0
- package/package.json +7 -6
- package/schema/noetic-workflow.schema.json +396 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { choiceValues, createMessage, DecideOutputError, extractAssistantText, frameworkCast, scoreLevels, } from '@noetic-tools/types';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { statusOf } from '../util/status-code.js';
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region Public API
|
|
6
|
+
/**
|
|
7
|
+
* Answer a decide request. Decision models (those whose OpenRouter output
|
|
8
|
+
* modality is `decisions`) answer natively with their own distributions; any
|
|
9
|
+
* other model answers through a structured-output call that estimates them.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export async function callDecide(request, deps) {
|
|
14
|
+
if (deps.client && (await isDecisionModel(request.model, deps))) {
|
|
15
|
+
return callNative(request, deps.client);
|
|
16
|
+
}
|
|
17
|
+
return callEstimated(request, deps);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region Model detection
|
|
21
|
+
function isDecisionModel(model, deps) {
|
|
22
|
+
const cached = deps.nativeModels.get(model);
|
|
23
|
+
if (cached) {
|
|
24
|
+
return cached.then((native) => native ?? false);
|
|
25
|
+
}
|
|
26
|
+
const lookup = lookupDecisionModel(model, deps.client);
|
|
27
|
+
deps.nativeModels.set(model, lookup);
|
|
28
|
+
// A failed lookup says nothing about the model, so the next call asks again.
|
|
29
|
+
void lookup.then((native) => {
|
|
30
|
+
if (native === undefined) {
|
|
31
|
+
deps.nativeModels.delete(model);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return lookup.then((native) => native ?? false);
|
|
35
|
+
}
|
|
36
|
+
/** `undefined` when the lookup itself failed, as opposed to the model not being one. */
|
|
37
|
+
async function lookupDecisionModel(model, client) {
|
|
38
|
+
const slash = model.indexOf('/');
|
|
39
|
+
if (!client || slash <= 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const { data } = await client.models.get({
|
|
44
|
+
author: model.slice(0, slash),
|
|
45
|
+
slug: model.slice(slash + 1),
|
|
46
|
+
});
|
|
47
|
+
return data.architecture.outputModalities.includes('decisions');
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
// An unknown model is not a decision model: the estimated path surfaces the
|
|
51
|
+
// provider's own error for it. Anything else may pass, so it is not cached.
|
|
52
|
+
return statusOf(e) === 404 ? false : undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region Native path
|
|
57
|
+
async function callNative(request, client) {
|
|
58
|
+
const questions = {};
|
|
59
|
+
for (const [id, question] of Object.entries(request.questions)) {
|
|
60
|
+
questions[id] = toNativeQuestion(question);
|
|
61
|
+
}
|
|
62
|
+
const response = await client.alpha.decisions.create({
|
|
63
|
+
decisionsRequest: {
|
|
64
|
+
model: request.model,
|
|
65
|
+
state: toNativeState(request.instructions, request.state),
|
|
66
|
+
questions,
|
|
67
|
+
},
|
|
68
|
+
}, {
|
|
69
|
+
signal: request.signal,
|
|
70
|
+
});
|
|
71
|
+
const answers = {};
|
|
72
|
+
for (const [id, question] of Object.entries(request.questions)) {
|
|
73
|
+
const answer = fromNativeAnswer(question, response.answers[id]);
|
|
74
|
+
if (answer) {
|
|
75
|
+
answers[id] = answer;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
answers,
|
|
80
|
+
source: 'native',
|
|
81
|
+
modelId: response.model,
|
|
82
|
+
usage: {
|
|
83
|
+
inputTokens: response.usage.inputTokens,
|
|
84
|
+
outputTokens: response.usage.outputTokens,
|
|
85
|
+
},
|
|
86
|
+
cost: response.usage.cost,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The native APIs require `instructions` on every question, while Noetic lets a
|
|
91
|
+
* boolean question carry its meaning in `criteria` alone; and TypeSafe needs
|
|
92
|
+
* both criteria sides or neither.
|
|
93
|
+
*/
|
|
94
|
+
function toNativeQuestion(question) {
|
|
95
|
+
switch (question.type) {
|
|
96
|
+
case 'boolean':
|
|
97
|
+
return toNativeBoolean(question);
|
|
98
|
+
case 'choice': {
|
|
99
|
+
const criteria = {};
|
|
100
|
+
for (const value of choiceValues(question.choices)) {
|
|
101
|
+
criteria[value] = describeChoice(question, value) ?? null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
type: 'choice',
|
|
105
|
+
instructions: question.instructions ?? 'Pick the option that best fits the state.',
|
|
106
|
+
criteria,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case 'score':
|
|
110
|
+
return {
|
|
111
|
+
type: 'score',
|
|
112
|
+
instructions: question.instructions ?? 'Place the state on this scale.',
|
|
113
|
+
criteria: scoreLevels(question.levels).map((l) => l.description ? `${l.label}: ${l.description}` : l.label),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function toNativeBoolean(question) {
|
|
118
|
+
const yes = question.criteria?.true;
|
|
119
|
+
const no = question.criteria?.false;
|
|
120
|
+
if (yes && no) {
|
|
121
|
+
const instructions = question.instructions ?? yes;
|
|
122
|
+
return {
|
|
123
|
+
type: 'noul',
|
|
124
|
+
instructions,
|
|
125
|
+
criteria: {
|
|
126
|
+
true: yes,
|
|
127
|
+
false: no,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// One side alone cannot be sent as criteria, so it joins the instructions.
|
|
132
|
+
const side = yes ? `Yes means: ${yes}` : no ? `No means: ${no}` : '';
|
|
133
|
+
return {
|
|
134
|
+
type: 'noul',
|
|
135
|
+
instructions: [
|
|
136
|
+
question.instructions,
|
|
137
|
+
side,
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(' '),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function describeChoice(question, value) {
|
|
144
|
+
if (Array.isArray(question.choices)) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return frameworkCast(question.choices)[value];
|
|
148
|
+
}
|
|
149
|
+
/** Named parts become keys; unnamed text and images run in order under `conversation`. */
|
|
150
|
+
function toNativeState(instructions, parts) {
|
|
151
|
+
const state = {};
|
|
152
|
+
if (instructions) {
|
|
153
|
+
state.instructions = instructions;
|
|
154
|
+
}
|
|
155
|
+
const conversation = [];
|
|
156
|
+
for (const part of parts) {
|
|
157
|
+
if (part.type === 'image') {
|
|
158
|
+
conversation.push({
|
|
159
|
+
type: 'image_url',
|
|
160
|
+
image_url: {
|
|
161
|
+
url: part.imageUrl,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (part.type === 'json') {
|
|
167
|
+
state[part.name] = part.value;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (part.name) {
|
|
171
|
+
state[part.name] = part.text;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
conversation.push(part.text);
|
|
175
|
+
}
|
|
176
|
+
if (conversation.length > 0) {
|
|
177
|
+
state.conversation = conversation;
|
|
178
|
+
}
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Map one provider answer. Only an explicit `refusal` becomes a `Refusal`; a
|
|
183
|
+
* missing or mistyped answer comes back `undefined` so validation raises
|
|
184
|
+
* `model_parse_error` instead of passing a provider fault off as the model
|
|
185
|
+
* declining.
|
|
186
|
+
*/
|
|
187
|
+
function fromNativeAnswer(question, answer) {
|
|
188
|
+
if (isRefusal(answer)) {
|
|
189
|
+
return {
|
|
190
|
+
type: 'refusal',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (question.type === 'boolean' && answer?.type === 'noul') {
|
|
194
|
+
return booleanAnswer(question, answer.noul);
|
|
195
|
+
}
|
|
196
|
+
if (question.type === 'choice' && answer?.type === 'choice') {
|
|
197
|
+
const probabilities = everyOutcome(choiceValues(question.choices), answer.probabilities ?? {
|
|
198
|
+
[answer.choice]: 1,
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
type: 'choice',
|
|
202
|
+
value: answer.choice,
|
|
203
|
+
confidence: answer.confidence ?? top(probabilities),
|
|
204
|
+
probabilities,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (question.type === 'score' && answer?.type === 'score') {
|
|
208
|
+
const labels = scoreLevels(question.levels).map((l) => l.label);
|
|
209
|
+
const rounded = labels[Math.round(answer.score)] ?? '';
|
|
210
|
+
// Score probabilities arrive keyed by level index ('0', '1', …). Without
|
|
211
|
+
// a distribution, all the weight goes on the level the score rounds to.
|
|
212
|
+
const byIndex = answer.probabilities ?? {
|
|
213
|
+
[String(Math.round(answer.score))]: 1,
|
|
214
|
+
};
|
|
215
|
+
const probabilities = everyOutcome(labels, Object.fromEntries(labels.map((label, i) => [
|
|
216
|
+
label,
|
|
217
|
+
byIndex[String(i)] ?? 0,
|
|
218
|
+
])));
|
|
219
|
+
return {
|
|
220
|
+
type: 'score',
|
|
221
|
+
value: answer.score,
|
|
222
|
+
level: answer.probabilities ? (argmax(probabilities) ?? rounded) : rounded,
|
|
223
|
+
confidence: answer.confidence ?? top(probabilities),
|
|
224
|
+
probabilities,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region Estimated path
|
|
231
|
+
/**
|
|
232
|
+
* Ask a chat model for a probability per outcome, `samples` times, and average.
|
|
233
|
+
* Self-reported probabilities are not calibrated — fit for routing thresholds,
|
|
234
|
+
* not for measurement.
|
|
235
|
+
*/
|
|
236
|
+
async function callEstimated(request, deps) {
|
|
237
|
+
const schema = estimateSchema(request.questions);
|
|
238
|
+
const samples = Math.max(1, request.samples ?? 1);
|
|
239
|
+
const settled = await Promise.allSettled(Array.from({
|
|
240
|
+
length: samples,
|
|
241
|
+
}, () => deps.callModel({
|
|
242
|
+
model: request.model,
|
|
243
|
+
instructions: estimateInstructions(request),
|
|
244
|
+
items: [
|
|
245
|
+
stateMessage(request.state),
|
|
246
|
+
],
|
|
247
|
+
// Averaging only helps when the runs can differ.
|
|
248
|
+
params: {
|
|
249
|
+
temperature: samples > 1 ? 0.7 : 0,
|
|
250
|
+
},
|
|
251
|
+
outputSchema: schema,
|
|
252
|
+
signal: request.signal,
|
|
253
|
+
parentSpan: request.parentSpan,
|
|
254
|
+
})));
|
|
255
|
+
const runs = settled.flatMap((r) => r.status === 'fulfilled'
|
|
256
|
+
? [
|
|
257
|
+
r.value,
|
|
258
|
+
]
|
|
259
|
+
: []);
|
|
260
|
+
const failed = settled.find((r) => r.status === 'rejected');
|
|
261
|
+
if (runs.length === 0 && failed) {
|
|
262
|
+
throw failed.reason;
|
|
263
|
+
}
|
|
264
|
+
const usage = {
|
|
265
|
+
inputTokens: sum(runs.map((r) => r.usage.inputTokens)),
|
|
266
|
+
outputTokens: sum(runs.map((r) => r.usage.outputTokens)),
|
|
267
|
+
};
|
|
268
|
+
const cost = runs.some((r) => r.cost !== undefined)
|
|
269
|
+
? sum(runs.map((r) => r.cost ?? 0))
|
|
270
|
+
: undefined;
|
|
271
|
+
// Samples run hot, so one bad reply should cost only itself; fail only when none parse.
|
|
272
|
+
const results = runs.map((run) => parseEstimate(schema, extractAssistantText(run.items)));
|
|
273
|
+
const parsed = results.filter((r) => !(r instanceof DecideOutputError));
|
|
274
|
+
const firstError = results.find((r) => r instanceof DecideOutputError);
|
|
275
|
+
if (parsed.length === 0 && firstError) {
|
|
276
|
+
throw new DecideOutputError({
|
|
277
|
+
raw: firstError.raw,
|
|
278
|
+
schema: firstError.schema,
|
|
279
|
+
zodError: firstError.zodError,
|
|
280
|
+
usage,
|
|
281
|
+
cost,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const answers = {};
|
|
285
|
+
for (const [id, question] of Object.entries(request.questions)) {
|
|
286
|
+
answers[id] = averageAnswer(question, parsed.map((p) => p[id]));
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
answers,
|
|
290
|
+
source: 'estimated',
|
|
291
|
+
modelId: request.model,
|
|
292
|
+
usage,
|
|
293
|
+
cost,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function parseEstimate(schema, raw) {
|
|
297
|
+
let json;
|
|
298
|
+
try {
|
|
299
|
+
json = JSON.parse(raw);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
json = undefined;
|
|
303
|
+
}
|
|
304
|
+
const result = schema.safeParse(json);
|
|
305
|
+
if (!result.success) {
|
|
306
|
+
return new DecideOutputError({
|
|
307
|
+
raw,
|
|
308
|
+
schema,
|
|
309
|
+
zodError: result.error,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return result.data;
|
|
313
|
+
}
|
|
314
|
+
function estimateSchema(questions) {
|
|
315
|
+
const shape = {};
|
|
316
|
+
for (const [id, question] of Object.entries(questions)) {
|
|
317
|
+
const keys = outcomeKeys(question);
|
|
318
|
+
shape[id] = z.object(Object.fromEntries(keys.map((k) => [
|
|
319
|
+
k,
|
|
320
|
+
z.number().min(0).max(1),
|
|
321
|
+
])));
|
|
322
|
+
}
|
|
323
|
+
return frameworkCast(z.object(shape));
|
|
324
|
+
}
|
|
325
|
+
/** Booleans estimate `{ yes }`; choices and scores estimate one probability per outcome. */
|
|
326
|
+
function outcomeKeys(question) {
|
|
327
|
+
switch (question.type) {
|
|
328
|
+
case 'boolean':
|
|
329
|
+
return [
|
|
330
|
+
'yes',
|
|
331
|
+
];
|
|
332
|
+
case 'choice':
|
|
333
|
+
return choiceValues(question.choices);
|
|
334
|
+
case 'score':
|
|
335
|
+
return scoreLevels(question.levels).map((l) => l.label);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function estimateInstructions(request) {
|
|
339
|
+
const lines = [
|
|
340
|
+
'You answer questions about the state you are given. For each question, return a probability for every outcome.',
|
|
341
|
+
'For a yes/no question, return the probability of yes. For other questions, the probabilities should sum to 1.',
|
|
342
|
+
];
|
|
343
|
+
if (request.instructions) {
|
|
344
|
+
lines.push('', request.instructions);
|
|
345
|
+
}
|
|
346
|
+
lines.push('', 'Questions:');
|
|
347
|
+
for (const [id, question] of Object.entries(request.questions)) {
|
|
348
|
+
lines.push(`- ${id}: ${describeQuestion(question)}`);
|
|
349
|
+
}
|
|
350
|
+
return lines.join('\n');
|
|
351
|
+
}
|
|
352
|
+
function describeQuestion(question) {
|
|
353
|
+
const ask = question.instructions ?? '';
|
|
354
|
+
switch (question.type) {
|
|
355
|
+
case 'boolean': {
|
|
356
|
+
const yes = question.criteria?.true ? ` Yes means: ${question.criteria.true}.` : '';
|
|
357
|
+
const no = question.criteria?.false ? ` No means: ${question.criteria.false}.` : '';
|
|
358
|
+
return `(yes/no) ${ask}${yes}${no}`.trim();
|
|
359
|
+
}
|
|
360
|
+
case 'choice': {
|
|
361
|
+
const options = choiceValues(question.choices).map((v) => {
|
|
362
|
+
const description = describeChoice(question, v);
|
|
363
|
+
return description ? `${v} (${description})` : v;
|
|
364
|
+
});
|
|
365
|
+
return `(one of: ${options.join('; ')}) ${ask}`.trim();
|
|
366
|
+
}
|
|
367
|
+
case 'score':
|
|
368
|
+
return `(scale, lowest first: ${describeLevels(question)}) ${ask}`.trim();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function describeLevels(question) {
|
|
372
|
+
return scoreLevels(question.levels)
|
|
373
|
+
.map((l) => (l.description ? `${l.label} (${l.description})` : l.label))
|
|
374
|
+
.join('; ');
|
|
375
|
+
}
|
|
376
|
+
function stateMessage(parts) {
|
|
377
|
+
const content = parts.map((part) => {
|
|
378
|
+
if (part.type === 'image') {
|
|
379
|
+
return {
|
|
380
|
+
type: 'input_image',
|
|
381
|
+
imageUrl: part.imageUrl,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (part.type === 'json') {
|
|
385
|
+
return {
|
|
386
|
+
type: 'input_text',
|
|
387
|
+
text: `${part.name}: ${JSON.stringify(part.value)}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
type: 'input_text',
|
|
392
|
+
text: part.name ? `${part.name}: ${part.text}` : part.text,
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
return {
|
|
396
|
+
...createMessage('', 'user'),
|
|
397
|
+
content,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function averageAnswer(question, samples) {
|
|
401
|
+
const present = samples.filter((s) => s !== undefined);
|
|
402
|
+
if (present.length === 0) {
|
|
403
|
+
return {
|
|
404
|
+
type: 'refusal',
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const keys = outcomeKeys(question);
|
|
408
|
+
const mean = {};
|
|
409
|
+
for (const key of keys) {
|
|
410
|
+
mean[key] = sum(present.map((s) => s[key] ?? 0)) / present.length;
|
|
411
|
+
}
|
|
412
|
+
if (question.type === 'boolean') {
|
|
413
|
+
return booleanAnswer(question, mean.yes ?? 0);
|
|
414
|
+
}
|
|
415
|
+
const probabilities = normalize(mean);
|
|
416
|
+
const best = argmax(probabilities) ?? keys[0] ?? '';
|
|
417
|
+
if (question.type === 'choice') {
|
|
418
|
+
return {
|
|
419
|
+
type: 'choice',
|
|
420
|
+
value: best,
|
|
421
|
+
confidence: top(probabilities),
|
|
422
|
+
probabilities,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
type: 'score',
|
|
427
|
+
value: sum(keys.map((k, i) => i * (probabilities[k] ?? 0))),
|
|
428
|
+
level: best,
|
|
429
|
+
confidence: top(probabilities),
|
|
430
|
+
probabilities,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region Helpers
|
|
435
|
+
function booleanAnswer(question, probability) {
|
|
436
|
+
return {
|
|
437
|
+
type: 'boolean',
|
|
438
|
+
value: probability >= (question.threshold ?? 0.5),
|
|
439
|
+
probability,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/** The SDK surfaces answer types it does not model as `UNKNOWN`, keeping the payload in `raw`. */
|
|
443
|
+
function isRefusal(answer) {
|
|
444
|
+
if (answer?.type !== 'UNKNOWN' || !('raw' in answer)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const raw = answer.raw;
|
|
448
|
+
return typeof raw === 'object' && raw !== null && 'type' in raw && raw.type === 'refusal';
|
|
449
|
+
}
|
|
450
|
+
/** One entry per outcome, so a caller can index any choice or level; missing ones read 0. */
|
|
451
|
+
function everyOutcome(outcomes, given) {
|
|
452
|
+
return Object.fromEntries(outcomes.map((o) => [
|
|
453
|
+
o,
|
|
454
|
+
Object.hasOwn(given, o) ? (given[o] ?? 0) : 0,
|
|
455
|
+
]));
|
|
456
|
+
}
|
|
457
|
+
function normalize(probabilities) {
|
|
458
|
+
const keys = Object.keys(probabilities);
|
|
459
|
+
const total = sum(Object.values(probabilities));
|
|
460
|
+
const out = {};
|
|
461
|
+
for (const key of keys) {
|
|
462
|
+
out[key] = total > 0 ? (probabilities[key] ?? 0) / total : 1 / keys.length;
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
function argmax(probabilities) {
|
|
467
|
+
let best;
|
|
468
|
+
let bestP = -1;
|
|
469
|
+
for (const [key, p] of Object.entries(probabilities)) {
|
|
470
|
+
if (p > bestP) {
|
|
471
|
+
best = key;
|
|
472
|
+
bestP = p;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return best;
|
|
476
|
+
}
|
|
477
|
+
function top(probabilities) {
|
|
478
|
+
return Math.max(0, ...Object.values(probabilities));
|
|
479
|
+
}
|
|
480
|
+
function sum(values) {
|
|
481
|
+
return values.reduce((a, b) => a + b, 0);
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { OpenRouter } from '@openrouter/agent';
|
|
2
|
+
export { OpenRouter as OpenRouterSdk } from '@openrouter/sdk';
|
|
2
3
|
export { createInMemoryFsAdapter } from '../../adapters/in-memory-fs-adapter.js';
|
|
3
4
|
export { createInMemoryShellAdapter } from '../../adapters/in-memory-shell-adapter.js';
|
|
4
5
|
export { createInMemorySubprocessAdapter } from '../../adapters/in-memory-subprocess-adapter.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { OpenRouter } from '@openrouter/agent';
|
|
2
|
+
export { OpenRouter as OpenRouterSdk } from '@openrouter/sdk';
|
|
2
3
|
export { createInMemoryFsAdapter } from '../../adapters/in-memory-fs-adapter.js';
|
|
3
4
|
export { createInMemoryShellAdapter } from '../../adapters/in-memory-shell-adapter.js';
|
|
4
5
|
export { createInMemorySubprocessAdapter } from '../../adapters/in-memory-subprocess-adapter.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { LayerStateStore, RecallCache } from '@noetic-tools/context';
|
|
2
|
-
export { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from '@noetic-tools/context';
|
|
2
|
+
export { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, notifyDecision, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from '@noetic-tools/context';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from '@noetic-tools/context';
|
|
1
|
+
export { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, notifyDecision, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from '@noetic-tools/context';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, ExecutionContext, ProjectionPolicy, StorageAdapter, } from '@noetic-tools/context';
|
|
2
|
-
export type { AcpKeepAlive, AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
|
|
2
|
+
export type { AcpKeepAlive, AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DecideRequest, DecideResponse, DecisionRecord, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, RecallOptions, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
|
|
3
3
|
export { SteeringAction } from '@noetic-tools/types';
|
|
4
4
|
export type { ZodType } from 'zod';
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export { context } from './builders/context-builder.js';
|
|
|
17
17
|
/** @public */
|
|
18
18
|
export { conditional, inParallel } from './builders/control-flow-builders.js';
|
|
19
19
|
/** @public */
|
|
20
|
+
export { decide, routeSwitch } from './builders/decide-builders.js';
|
|
21
|
+
/** @public */
|
|
20
22
|
export type { ScheduleOptions } from './builders/every.js';
|
|
21
23
|
/** @public */
|
|
22
24
|
export { schedule } from './builders/every.js';
|
|
@@ -65,7 +67,7 @@ export { execute } from './interpreter/execute.js';
|
|
|
65
67
|
/** @public */
|
|
66
68
|
/** @public */
|
|
67
69
|
/** @public */
|
|
68
|
-
export type { FactExtractor, FactSearcher, HistoryConfig, ObservationsConfig, ObservationsState, PlanConfig, PlanEnterSessionCallback, PlanExecutionEntry, PlanExitCallback, PlanState, ScratchpadConfig, ScratchpadState, TaskState, TemporalConfig, TemporalFact, TemporalSearchResult, } from '@noetic-tools/context';
|
|
70
|
+
export type { DecisionsHistoryConfig, DecisionsHistoryLayer, DecisionsHistoryState, FactExtractor, FactSearcher, HistoryConfig, ObservationsConfig, ObservationsState, PlanConfig, PlanEnterSessionCallback, PlanExecutionEntry, PlanExitCallback, PlanState, ScratchpadConfig, ScratchpadState, TaskState, TemporalConfig, TemporalFact, TemporalSearchResult, } from '@noetic-tools/context';
|
|
69
71
|
/** @public */
|
|
70
72
|
/** @public */
|
|
71
73
|
/** @public */
|
|
@@ -78,7 +80,7 @@ export type { FactExtractor, FactSearcher, HistoryConfig, ObservationsConfig, Ob
|
|
|
78
80
|
/** @public */
|
|
79
81
|
/** @public */
|
|
80
82
|
/** @public */
|
|
81
|
-
export { filesystem, findFunctionCall, history, instructions, observations, PlanPhase, PlanStyle, plan, scratchpad, steering, storageGetMany, stripUnresolvedToolCalls, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
83
|
+
export { DECISION_PROJECTION, decisionsHistoryLayer, filesystem, findFunctionCall, history, instructions, observations, PlanPhase, PlanStyle, plan, scratchpad, steering, storageGetMany, stripUnresolvedToolCalls, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
82
84
|
/** @public */
|
|
83
85
|
export { createMessage, estimateTokens } from '@noetic-tools/types';
|
|
84
86
|
/** @public */
|
|
@@ -104,7 +106,7 @@ export { getRegistry, lookupStep, registerStep } from './runtime/step-registry.j
|
|
|
104
106
|
/** @public */
|
|
105
107
|
export { defaultItemSchemaRegistry, ItemSchema, ItemSchemaRegistry } from '@noetic-tools/types';
|
|
106
108
|
/** @public */
|
|
107
|
-
export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, MergeStrategy, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowNode, } from './schemas/workflow.js';
|
|
109
|
+
export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, DecideWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, MergeStrategy, RouteSwitchWorkflowNode, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowNode, } from './schemas/workflow.js';
|
|
108
110
|
/** @public */
|
|
109
111
|
export { MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, } from './schemas/workflow.js';
|
|
110
112
|
/** @public */
|
|
@@ -145,7 +147,7 @@ export type { LayerTraceSpan, Span, TraceExporter } from '@noetic-tools/types';
|
|
|
145
147
|
/** @public */
|
|
146
148
|
export type { FrameworkStreamEvent, HarnessResponse, SdkStreamEvent, StreamEvent, StreamingItem, } from '@noetic-tools/types';
|
|
147
149
|
/** @public */
|
|
148
|
-
export type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, ContextCacheConfig, ContextCacheStore, ContextEpoch, DeliveryMode, ExecuteOptions, HarnessStatus, ItemSchemaConfig, ReanchorReason, RecallLayerOutput, SessionScope, SessionUsage, } from '@noetic-tools/types';
|
|
150
|
+
export type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, ContextCacheConfig, ContextCacheStore, ContextEpoch, DeliveryMode, ExecuteOptions, HarnessJudgmentContract, HarnessStatus, ItemSchemaConfig, ReanchorReason, RecallLayerOutput, SessionScope, SessionUsage, } from '@noetic-tools/types';
|
|
149
151
|
/** @public */
|
|
150
152
|
export type { AfterModelCallParams, AfterModelCallResult, BeforeToolCallParams, BeforeToolCallResult, LedgerEntry, SteeringConfig, SteeringDecision, SteeringRule, SteeringState, } from '@noetic-tools/types';
|
|
151
153
|
/** @public */
|
|
@@ -153,6 +155,12 @@ export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
|
|
|
153
155
|
/** @public */
|
|
154
156
|
export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepAcpAgent, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepRunCode, StepSpawn, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
|
|
155
157
|
/** @public */
|
|
158
|
+
export type { AnsweredAll, AnswerFor, BooleanAnswer, BooleanQuestion, CaseKeyOf, ChoiceAnswer, ChoiceQuestion, ChoiceValues, DecideAnswer, DecideAnswers, DecideQuestion, DecideQuestions, DecideRequest, DecideResponse, DecideStatePart, DecideStep, DecideView, DecisionMeta, DecisionRecord, DecisionSource, LevelLabels, OnDecisionParams, RecallOptions, ScoreAnswer, ScoreLevel, ScoreQuestion, StepDecide, } from '@noetic-tools/types';
|
|
159
|
+
/** @public */
|
|
160
|
+
export { BooleanQuestionSchema, ChoiceQuestionSchema, choiceValues, DECIDE_LIMITS, DecideOutputError, DecideQuestionSchema, DecideQuestionsSchema, routeOutcomes, ScoreQuestionSchema, scoreLevels, } from '@noetic-tools/types';
|
|
161
|
+
/** @public */
|
|
162
|
+
export type { RouteSwitchByFunctionOpts, RouteSwitchByKeyOpts, StepDecideOpts, StepInput, StepOutput, } from './builders/decide-builders.js';
|
|
163
|
+
/** @public */
|
|
156
164
|
export type { AcpAgent, AcpAgentCapabilities, AcpAgentConnection, AcpAuthMethod, AcpAvailableCommand, AcpBoundPermissionHandler, AcpClientCapabilityConfig, AcpClientHost, AcpConnectOptions, AcpContentBlock, AcpKeepAlive, AcpLiveSession, AcpLoadSessionOptions, AcpMcpServer, AcpNewSessionOptions, AcpPermissionHandler, AcpPermissionOption, AcpPermissionOutcome, AcpPermissionPolicy, AcpPermissionRequestInfo, AcpPermissionRule, AcpPermissionSteerer, AcpPlanEntry, AcpPromptCapabilities, AcpPromptOptions, AcpRequestPermissionRequest, AcpSession, AcpSessionDisposer, AcpSessionInfo, AcpSessionMode, AcpSessionModeState, AcpSessionNotification, AcpSessionPolicy, AcpStopReason, AcpToolCallContent, AcpToolCallStatus, AcpToolKind, AcpTransport, AcpTransportFactory, AcpTransportOptions, AcpTurnResult, } from '@noetic-tools/types';
|
|
157
165
|
/** @public */
|
|
158
166
|
export { ACP_AGENT_STEP_KIND, AcpCapabilityError, AcpConnectError, AcpPermissionDecision, isAcpCapabilityError, isAcpConnectError, } from '@noetic-tools/types';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,8 @@ export { context } from './builders/context-builder.js';
|
|
|
16
16
|
/** @public */
|
|
17
17
|
export { conditional, inParallel } from './builders/control-flow-builders.js';
|
|
18
18
|
/** @public */
|
|
19
|
+
export { decide, routeSwitch } from './builders/decide-builders.js';
|
|
20
|
+
/** @public */
|
|
19
21
|
export { schedule } from './builders/every.js';
|
|
20
22
|
/** @public */
|
|
21
23
|
export { layerData, layerFunction } from './builders/layer-provides-builders.js';
|
|
@@ -60,7 +62,7 @@ export { execute } from './interpreter/execute.js';
|
|
|
60
62
|
/** @public */
|
|
61
63
|
/** @public */
|
|
62
64
|
/** @public */
|
|
63
|
-
export { filesystem, findFunctionCall, history, instructions, observations, PlanPhase, PlanStyle, plan, scratchpad, steering, storageGetMany, stripUnresolvedToolCalls, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
65
|
+
export { DECISION_PROJECTION, decisionsHistoryLayer, filesystem, findFunctionCall, history, instructions, observations, PlanPhase, PlanStyle, plan, scratchpad, steering, storageGetMany, stripUnresolvedToolCalls, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
64
66
|
//#endregion
|
|
65
67
|
//#region Message Utilities
|
|
66
68
|
/** @public */
|
|
@@ -100,6 +102,8 @@ export { Slot } from '@noetic-tools/context';
|
|
|
100
102
|
/** @public */
|
|
101
103
|
export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
|
|
102
104
|
/** @public */
|
|
105
|
+
export { BooleanQuestionSchema, ChoiceQuestionSchema, choiceValues, DECIDE_LIMITS, DecideOutputError, DecideQuestionSchema, DecideQuestionsSchema, routeOutcomes, ScoreQuestionSchema, scoreLevels, } from '@noetic-tools/types';
|
|
106
|
+
/** @public */
|
|
103
107
|
export { ACP_AGENT_STEP_KIND, AcpCapabilityError, AcpConnectError, AcpPermissionDecision, isAcpCapabilityError, isAcpConnectError, } from '@noetic-tools/types';
|
|
104
108
|
/** @public */
|
|
105
109
|
export { TIMEOUT_ERROR_PREFIX } from '@noetic-tools/types';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { LayerServeInfo, LayerStateStore } from '@noetic-tools/context';
|
|
2
|
-
export { allocateBudgets, assembleView, churnFor, churnRate, commitLayerUsage, computeLayerUsage, contextToExecCtx, createContextCacheStore, DEFAULT_PROJECTION, lineageKey, noteCacheOutcome, openEpoch, pin, pinKey, reanchorReason, resolveCacheConfig, resolveLayerTools, returnLayers, spawnLayers, } from '@noetic-tools/context';
|
|
2
|
+
export { allocateBudgets, assembleView, churnFor, churnRate, commitLayerUsage, computeLayerUsage, contextToExecCtx, createContextCacheStore, DECISION_PROJECTION, DEFAULT_PROJECTION, lineageKey, noteCacheOutcome, openEpoch, pin, pinKey, reanchorReason, resolveCacheConfig, resolveLayerTools, returnLayers, spawnLayers, } from '@noetic-tools/context';
|
|
3
3
|
export type { ItemSchemaRegistry } from '@noetic-tools/types';
|
|
4
4
|
export { defaultItemSchemaRegistry } from '@noetic-tools/types';
|
|
5
5
|
export { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { allocateBudgets, assembleView, churnFor, churnRate, commitLayerUsage, computeLayerUsage, contextToExecCtx, createContextCacheStore, DEFAULT_PROJECTION, lineageKey, noteCacheOutcome, openEpoch, pin, pinKey, reanchorReason, resolveCacheConfig, resolveLayerTools, returnLayers, spawnLayers, } from '@noetic-tools/context';
|
|
1
|
+
export { allocateBudgets, assembleView, churnFor, churnRate, commitLayerUsage, computeLayerUsage, contextToExecCtx, createContextCacheStore, DECISION_PROJECTION, DEFAULT_PROJECTION, lineageKey, noteCacheOutcome, openEpoch, pin, pinKey, reanchorReason, resolveCacheConfig, resolveLayerTools, returnLayers, spawnLayers, } from '@noetic-tools/context';
|
|
2
2
|
export { defaultItemSchemaRegistry } from '@noetic-tools/types';
|
|
3
3
|
export { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
4
4
|
export { ContextImpl } from '../runtime/context-impl.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { ContextConfig, ContextData, ContextLayer, ExecutionContext, } from '@noetic-tools/context';
|
|
2
|
-
export type { AgentHarnessContract, CallModelRequest, Context, ContextRerenderRequest, ExecuteStepFn, FunctionCallItem, InputMessageItem, Item, LLMResponse, ProjectionPolicy, RecallLayerOutput, RetryPolicy, ServerToolSpec, StepCallModel, StepInvokeTool, StepMeta, StepRunCode, StepSpawn, StepWithContext, Tool, } from '@noetic-tools/types';
|
|
2
|
+
export type { AgentHarnessContract, CallModelRequest, Context, ContextRerenderRequest, ExecuteStepFn, FunctionCallItem, InputMessageItem, Item, LLMResponse, ProjectionPolicy, RecallLayerOutput, RetryPolicy, ServerToolSpec, StepCallModel, StepDecide, StepInvokeTool, StepMeta, StepRunCode, StepSpawn, StepWithContext, Tool, } from '@noetic-tools/types';
|
|
3
3
|
export { isServerToolSpec, SteeringAction } from '@noetic-tools/types';
|
|
@@ -43,6 +43,8 @@ function walkStep(step, out) {
|
|
|
43
43
|
return;
|
|
44
44
|
case 'runCode':
|
|
45
45
|
case 'invokeTool':
|
|
46
|
+
// Decide steps ask questions; they never call tools.
|
|
47
|
+
case 'decide':
|
|
46
48
|
// An ACP agent runs its own tools inside the external agent process; it
|
|
47
49
|
// contributes nothing to the host unified tool set.
|
|
48
50
|
case 'acp-agent':
|
|
@@ -9,7 +9,7 @@ import { cloneWithGuard } from './clone-guard.js';
|
|
|
9
9
|
import { getContextChannelStore, isContextImpl, isMutableContext } from './typeguards.js';
|
|
10
10
|
//#region conditional
|
|
11
11
|
export async function executeConditional(step, input, ctx, executeStep) {
|
|
12
|
-
const selected = await step.route(input, ctx);
|
|
12
|
+
const selected = await step.route(input, ctx, executeStep);
|
|
13
13
|
if (selected === null) {
|
|
14
14
|
// Requires I assignable to O for null route — when no route is selected,
|
|
15
15
|
// the input passes through. Callers must ensure I is compatible with O.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context, ContextLayer, StepDecide } from './action-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run a `decide` step: build state from the input, the context view, and the
|
|
4
|
+
* step's context selector; ask the harness; validate; record. The conversation
|
|
5
|
+
* is read, never written — no item-log appends, no steering, no layer `store()`.
|
|
6
|
+
* Layers hear about the decision through `onDecision`.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare function executeDecide<TContext, I, O>(step: StepDecide<TContext, I, O>, input: I, ctx: Context<TContext>, layers?: ContextLayer[]): Promise<O>;
|