@noetic-tools/core 5.4.2 → 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/adapters/openrouter.js +1 -1
- 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/tool-builder.d.ts +5 -1
- package/dist/builders/tool-builder.js +2 -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 +19 -4
- package/dist/harness/agent-harness.js +74 -9
- 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/harness/model-call.js +3 -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-acp-agent.js +1 -1
- package/dist/interpreter/execute-action.js +46 -21
- 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/runtime/session-runner.js +4 -0
- 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
|
@@ -386,26 +386,11 @@ function buildModelRequest(params) {
|
|
|
386
386
|
const { step, ctx, layers } = params;
|
|
387
387
|
const _serverTools = params.serverToolSpecs.length > 0 ? params.serverToolSpecs : undefined;
|
|
388
388
|
const signal = isContextImpl(ctx) ? ctx.abortSignal : undefined;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
tools: params.resolvedTools,
|
|
395
|
-
params: step.params,
|
|
396
|
-
outputSchema: params.outputSchema,
|
|
397
|
-
outputJsonSchema: params.step.outputJsonSchema,
|
|
398
|
-
emit: step.emit,
|
|
399
|
-
_serverTools,
|
|
400
|
-
ctx,
|
|
401
|
-
layers,
|
|
402
|
-
allowedToolNames: params.allowedToolNames,
|
|
403
|
-
nodeId: step.id,
|
|
404
|
-
parentSpan: ctx.span,
|
|
405
|
-
signal,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
return {
|
|
389
|
+
// One construction for both shapes: the tool fields vary, but `ctx` (which
|
|
390
|
+
// carries the broadcaster — without it a tool-less call streams NOTHING to
|
|
391
|
+
// getFullStream/getTextStream) and `layers` must be attached either way.
|
|
392
|
+
// The two hand-maintained branches this replaces had drifted exactly there.
|
|
393
|
+
const base = {
|
|
409
394
|
model: params.resolvedModel,
|
|
410
395
|
items: params.assembledItems,
|
|
411
396
|
instructions: params.resolvedInstructions,
|
|
@@ -414,10 +399,20 @@ function buildModelRequest(params) {
|
|
|
414
399
|
outputJsonSchema: params.step.outputJsonSchema,
|
|
415
400
|
emit: step.emit,
|
|
416
401
|
_serverTools,
|
|
402
|
+
ctx,
|
|
403
|
+
layers,
|
|
417
404
|
nodeId: step.id,
|
|
418
405
|
parentSpan: ctx.span,
|
|
419
406
|
signal,
|
|
420
407
|
};
|
|
408
|
+
if (params.resolvedTools) {
|
|
409
|
+
return {
|
|
410
|
+
...base,
|
|
411
|
+
tools: params.resolvedTools,
|
|
412
|
+
allowedToolNames: params.allowedToolNames,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return base;
|
|
421
416
|
}
|
|
422
417
|
/**
|
|
423
418
|
* Records a finished model response on the context: appends response items to
|
|
@@ -881,9 +876,37 @@ export async function executeInvokeTool(step, input, ctx, harness, layers) {
|
|
|
881
876
|
});
|
|
882
877
|
}
|
|
883
878
|
const parsedArgs = parseResult.value;
|
|
879
|
+
// The started event precedes the gate so a parked call (e.g. one awaiting an
|
|
880
|
+
// external permission decision) is visible as pending rather than invisible
|
|
881
|
+
// until approved — the same ordering the model-driven path guarantees.
|
|
882
|
+
const broadcaster = getBroadcaster(baseCtx);
|
|
883
|
+
const agentName = baseCtx.harness.config.name;
|
|
884
|
+
emitFrameworkEvent({
|
|
885
|
+
broadcaster,
|
|
886
|
+
agentName,
|
|
887
|
+
eventType: 'tool_call_started',
|
|
888
|
+
data: {
|
|
889
|
+
name: step.tool.name,
|
|
890
|
+
callId: step.id,
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
const emitCompleted = (error, output) => {
|
|
894
|
+
emitFrameworkEvent({
|
|
895
|
+
broadcaster,
|
|
896
|
+
agentName,
|
|
897
|
+
eventType: 'tool_call_completed',
|
|
898
|
+
data: {
|
|
899
|
+
name: step.tool.name,
|
|
900
|
+
callId: step.id,
|
|
901
|
+
error,
|
|
902
|
+
output,
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
};
|
|
884
906
|
if (layers && layers.length > 0) {
|
|
885
|
-
const decision = await harness.beforeToolCall(layers, step.tool.name, parsedArgs, baseCtx);
|
|
907
|
+
const decision = await harness.beforeToolCall(layers, step.tool.name, parsedArgs, baseCtx, step.id);
|
|
886
908
|
if (decision.action !== SteeringAction.Allow) {
|
|
909
|
+
emitCompleted(true, `Tool call denied: ${decision.guidance ?? 'steering rule violation'}`);
|
|
887
910
|
throw new NoeticErrorImpl({
|
|
888
911
|
kind: 'steering_denied',
|
|
889
912
|
guidance: decision.guidance,
|
|
@@ -917,6 +940,7 @@ export async function executeInvokeTool(step, input, ctx, harness, layers) {
|
|
|
917
940
|
args: parsedArgs,
|
|
918
941
|
output: result,
|
|
919
942
|
});
|
|
943
|
+
emitCompleted(false, typeof result === 'string' ? result : JSON.stringify(result));
|
|
920
944
|
return frameworkCast(result);
|
|
921
945
|
}
|
|
922
946
|
catch (e) {
|
|
@@ -928,6 +952,7 @@ export async function executeInvokeTool(step, input, ctx, harness, layers) {
|
|
|
928
952
|
args: parsedArgs,
|
|
929
953
|
error: e,
|
|
930
954
|
});
|
|
955
|
+
emitCompleted(true, e instanceof Error ? e.message : String(e));
|
|
931
956
|
if (e instanceof NoeticErrorImpl) {
|
|
932
957
|
throw e;
|
|
933
958
|
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { choiceValues, DecideOutputError, frameworkCast, hashString, isNoeticError, NoeticConfigError, NoeticErrorImpl, scoreLevels, } from '@noetic-tools/types';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { GenAI, NoeticAttr } from '../observability/genai-attributes.js';
|
|
4
|
+
import { statusOf } from '../util/status-code.js';
|
|
5
|
+
import { allocateBudgets, assembleView, DECISION_PROJECTION } from './action-deps.js';
|
|
6
|
+
import { resolveLazy } from './execute-action.js';
|
|
7
|
+
import { trackUsage } from './message-helpers.js';
|
|
8
|
+
import { isContextImpl, isMutableContext } from './typeguards.js';
|
|
9
|
+
//#region Public API
|
|
10
|
+
/**
|
|
11
|
+
* Run a `decide` step: build state from the input, the context view, and the
|
|
12
|
+
* step's context selector; ask the harness; validate; record. The conversation
|
|
13
|
+
* is read, never written — no item-log appends, no steering, no layer `store()`.
|
|
14
|
+
* Layers hear about the decision through `onDecision`.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export async function executeDecide(step, input, ctx, layers) {
|
|
19
|
+
const baseCtx = frameworkCast(ctx);
|
|
20
|
+
const key = memoKey(step.id, input, baseCtx.itemLog.items.length);
|
|
21
|
+
const seen = key === undefined ? undefined : answered(baseCtx).get(key);
|
|
22
|
+
if (seen) {
|
|
23
|
+
return frameworkCast(seen);
|
|
24
|
+
}
|
|
25
|
+
const model = await resolveModel(step, ctx);
|
|
26
|
+
const instructions = await resolveLazy(step.instructions, ctx);
|
|
27
|
+
const state = [
|
|
28
|
+
...inputState(input),
|
|
29
|
+
...(await viewState(step, input, baseCtx, layers ?? [])),
|
|
30
|
+
...contextState(step.context ? await step.context(ctx) : undefined),
|
|
31
|
+
];
|
|
32
|
+
const span = baseCtx.harness.createSpan('decide.call', baseCtx.span);
|
|
33
|
+
span.setAttribute(NoeticAttr.NODE_ID, step.id);
|
|
34
|
+
let answers;
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await askHarness(step, baseCtx, {
|
|
38
|
+
model,
|
|
39
|
+
instructions,
|
|
40
|
+
state,
|
|
41
|
+
questions: step.questions,
|
|
42
|
+
samples: step.samples,
|
|
43
|
+
signal: isContextImpl(baseCtx) ? baseCtx.abortSignal : undefined,
|
|
44
|
+
parentSpan: span,
|
|
45
|
+
});
|
|
46
|
+
trackUsage(baseCtx, {
|
|
47
|
+
items: [],
|
|
48
|
+
usage: response.usage,
|
|
49
|
+
cost: response.cost,
|
|
50
|
+
});
|
|
51
|
+
answers = validateAnswers(step.id, step.questions, response.answers);
|
|
52
|
+
stampSpan({
|
|
53
|
+
span,
|
|
54
|
+
response,
|
|
55
|
+
answers,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
span.setAttribute('error', true);
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
span.end();
|
|
64
|
+
await baseCtx.harness.traceExporter.export([
|
|
65
|
+
span,
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
if (baseCtx.aborted) {
|
|
69
|
+
throw new NoeticErrorImpl({
|
|
70
|
+
kind: 'cancelled',
|
|
71
|
+
reason: baseCtx.abortReason ?? 'context aborted',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (isMutableContext(baseCtx)) {
|
|
75
|
+
// Merged, not replaced: a loop's `until.noToolCalls()` still reads the tool
|
|
76
|
+
// calls of the llm step that ran before this decision.
|
|
77
|
+
baseCtx.lastStepMeta = {
|
|
78
|
+
...baseCtx.lastStepMeta,
|
|
79
|
+
usage: response.usage,
|
|
80
|
+
cost: response.cost,
|
|
81
|
+
decision: {
|
|
82
|
+
modelId: response.modelId,
|
|
83
|
+
source: response.source,
|
|
84
|
+
refused: refusedIds(answers),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (key !== undefined) {
|
|
89
|
+
answered(baseCtx).set(key, answers);
|
|
90
|
+
}
|
|
91
|
+
if (layers && layers.length > 0) {
|
|
92
|
+
await baseCtx.harness.notifyDecision(layers, {
|
|
93
|
+
stepId: step.id,
|
|
94
|
+
at: Date.now(),
|
|
95
|
+
modelId: response.modelId,
|
|
96
|
+
source: response.source,
|
|
97
|
+
answers,
|
|
98
|
+
}, baseCtx);
|
|
99
|
+
}
|
|
100
|
+
return frameworkCast(answers);
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region Reuse
|
|
104
|
+
/**
|
|
105
|
+
* Answers already given in each execution, keyed by step id, input, and how
|
|
106
|
+
* long the conversation was. A decide step asked the same thing twice with
|
|
107
|
+
* nothing said in between — say, by two nested routeSwitches — answers from
|
|
108
|
+
* here, so the model is paid once and the two switches cannot disagree. A loop
|
|
109
|
+
* that asks again after new items land gets a fresh decision.
|
|
110
|
+
*/
|
|
111
|
+
const ANSWERED = new WeakMap();
|
|
112
|
+
function answered(ctx) {
|
|
113
|
+
let map = ANSWERED.get(ctx);
|
|
114
|
+
if (!map) {
|
|
115
|
+
map = new Map();
|
|
116
|
+
ANSWERED.set(ctx, map);
|
|
117
|
+
}
|
|
118
|
+
return map;
|
|
119
|
+
}
|
|
120
|
+
/** `undefined` when the input cannot be keyed (it does not survive JSON), so it is never reused. */
|
|
121
|
+
function memoKey(stepId, input, logLength) {
|
|
122
|
+
try {
|
|
123
|
+
return `${stepId}\u0000${logLength}\u0000${hashString(JSON.stringify(input) ?? 'undefined')}`;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function refusedIds(answers) {
|
|
130
|
+
return Object.entries(answers)
|
|
131
|
+
.filter(([, a]) => a.type === 'refusal')
|
|
132
|
+
.map(([id]) => id);
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region Model + harness call
|
|
136
|
+
async function resolveModel(step, ctx) {
|
|
137
|
+
const model = (await resolveLazy(step.model, ctx)) ?? ctx.harness.config.decisionModel;
|
|
138
|
+
if (!model || model.trim() === '') {
|
|
139
|
+
throw new NoeticConfigError({
|
|
140
|
+
code: 'MISSING_MODEL',
|
|
141
|
+
message: `decide(${JSON.stringify(step.id)}) has no model.`,
|
|
142
|
+
hint: "Set `model` on the step, or `decisionModel` on the harness, e.g. 'typesafe/jev-1.14'.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return model;
|
|
146
|
+
}
|
|
147
|
+
/** Provider errors arrive raw; give them the step's id and a Noetic kind. */
|
|
148
|
+
async function askHarness(step, ctx, request) {
|
|
149
|
+
try {
|
|
150
|
+
return await ctx.harness.decide(request);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
if (isNoeticError(e) || e instanceof NoeticConfigError) {
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
if (ctx.aborted) {
|
|
157
|
+
throw new NoeticErrorImpl({
|
|
158
|
+
kind: 'cancelled',
|
|
159
|
+
reason: ctx.abortReason ?? 'context aborted',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (e instanceof DecideOutputError) {
|
|
163
|
+
if (e.usage) {
|
|
164
|
+
trackUsage(ctx, {
|
|
165
|
+
items: [],
|
|
166
|
+
usage: e.usage,
|
|
167
|
+
cost: e.cost,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
throw new NoeticErrorImpl({
|
|
171
|
+
kind: 'model_parse_error',
|
|
172
|
+
stepId: step.id,
|
|
173
|
+
raw: e.raw,
|
|
174
|
+
schema: e.schema,
|
|
175
|
+
zodError: e.zodError,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (statusOf(e) === 429) {
|
|
179
|
+
throw new NoeticErrorImpl({
|
|
180
|
+
kind: 'model_rate_limit',
|
|
181
|
+
stepId: step.id,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
throw new NoeticErrorImpl({
|
|
185
|
+
kind: 'step_failed',
|
|
186
|
+
stepId: step.id,
|
|
187
|
+
cause: e instanceof Error ? e : new Error(String(e)),
|
|
188
|
+
retriesExhausted: false,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region State
|
|
194
|
+
function inputState(input) {
|
|
195
|
+
if (input === undefined || input === null || input === '') {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
if (typeof input === 'string') {
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
type: 'text',
|
|
202
|
+
name: 'input',
|
|
203
|
+
text: input,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
type: 'json',
|
|
210
|
+
name: 'input',
|
|
211
|
+
value: input,
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
/** Selector values ride under one `context` entry, so no key can overwrite `input` or the view. */
|
|
216
|
+
function contextState(values) {
|
|
217
|
+
if (!values || Object.keys(values).length === 0) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
type: 'json',
|
|
223
|
+
name: 'context',
|
|
224
|
+
value: values,
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The same recall + history projection `callModel` sees, within the decision's
|
|
230
|
+
* own budget. Recall peeks, so a decision never drains layer state (such as
|
|
231
|
+
* queued steering guidance) meant for the next model call.
|
|
232
|
+
*/
|
|
233
|
+
async function viewState(step, input, ctx, layers) {
|
|
234
|
+
if (step.view === 'none') {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const policy = step.projection ?? DECISION_PROJECTION;
|
|
238
|
+
const recalled = await peekRecall({
|
|
239
|
+
layers,
|
|
240
|
+
query: typeof input === 'string' ? input : '',
|
|
241
|
+
ctx,
|
|
242
|
+
policy,
|
|
243
|
+
});
|
|
244
|
+
const history = step.view === 'full' ? await ctx.harness.projectHistory(layers, ctx.itemLog.items, ctx) : [];
|
|
245
|
+
const view = assembleView({
|
|
246
|
+
systemPromptItems: [],
|
|
247
|
+
layerOutputItems: recalled,
|
|
248
|
+
historyItems: history.filter((item) => !(item.type === 'message' && item.role === 'system')),
|
|
249
|
+
policy,
|
|
250
|
+
});
|
|
251
|
+
return view.flatMap(itemState);
|
|
252
|
+
}
|
|
253
|
+
async function peekRecall({ layers, query, ctx, policy, }) {
|
|
254
|
+
if (layers.length === 0) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
const { allocations } = allocateBudgets({
|
|
258
|
+
layers,
|
|
259
|
+
totalBudget: policy.tokenBudget,
|
|
260
|
+
systemPromptTokens: 0,
|
|
261
|
+
responseReserve: policy.responseReserve,
|
|
262
|
+
});
|
|
263
|
+
const budgets = new Map(allocations.map((a) => [
|
|
264
|
+
a.layerId,
|
|
265
|
+
a.allocated,
|
|
266
|
+
]));
|
|
267
|
+
const peek = {
|
|
268
|
+
peek: true,
|
|
269
|
+
};
|
|
270
|
+
const results = [
|
|
271
|
+
...(await ctx.harness.recallLayersAtomic(layers, query, ctx, budgets, peek)),
|
|
272
|
+
...(await ctx.harness.recallLayersEventual(layers, query, ctx, budgets, peek)),
|
|
273
|
+
];
|
|
274
|
+
const slotOf = new Map(layers.map((l) => [
|
|
275
|
+
l.id,
|
|
276
|
+
l.slot,
|
|
277
|
+
]));
|
|
278
|
+
return results
|
|
279
|
+
.sort((a, b) => (slotOf.get(a.layerId) ?? 0) - (slotOf.get(b.layerId) ?? 0))
|
|
280
|
+
.flatMap((r) => r.items);
|
|
281
|
+
}
|
|
282
|
+
/** Render one view item as state: role-tagged text, plus any images it carries. */
|
|
283
|
+
function itemState(item) {
|
|
284
|
+
if (item.type === 'function_call') {
|
|
285
|
+
return [
|
|
286
|
+
{
|
|
287
|
+
type: 'text',
|
|
288
|
+
text: `tool call ${item.name}(${item.arguments})`,
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
if (item.type === 'function_call_output') {
|
|
293
|
+
return [
|
|
294
|
+
{
|
|
295
|
+
type: 'text',
|
|
296
|
+
text: `tool result: ${item.output}`,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
if (item.type !== 'message') {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
const parts = [];
|
|
304
|
+
const texts = [];
|
|
305
|
+
for (const part of item.content) {
|
|
306
|
+
if (part.type === 'input_image') {
|
|
307
|
+
parts.push({
|
|
308
|
+
type: 'image',
|
|
309
|
+
imageUrl: part.imageUrl,
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if ('text' in part && typeof part.text === 'string') {
|
|
314
|
+
texts.push(part.text);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (texts.length > 0) {
|
|
318
|
+
parts.unshift({
|
|
319
|
+
type: 'text',
|
|
320
|
+
text: `${item.role}: ${texts.join('')}`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return parts;
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region Validation
|
|
327
|
+
function validateAnswers(stepId, questions, answers) {
|
|
328
|
+
const schema = answersSchema(questions);
|
|
329
|
+
const result = schema.safeParse(answers);
|
|
330
|
+
if (!result.success) {
|
|
331
|
+
throw new NoeticErrorImpl({
|
|
332
|
+
kind: 'model_parse_error',
|
|
333
|
+
stepId,
|
|
334
|
+
raw: JSON.stringify(answers),
|
|
335
|
+
schema,
|
|
336
|
+
zodError: result.error,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return result.data;
|
|
340
|
+
}
|
|
341
|
+
const Probability = z.number().min(0).max(1);
|
|
342
|
+
const RefusalSchema = z.object({
|
|
343
|
+
type: z.literal('refusal'),
|
|
344
|
+
});
|
|
345
|
+
/** Every question answered or refused, every choice and level known, every probability in range. */
|
|
346
|
+
function answersSchema(questions) {
|
|
347
|
+
const shape = {};
|
|
348
|
+
for (const [id, question] of Object.entries(questions)) {
|
|
349
|
+
shape[id] = z.union([
|
|
350
|
+
RefusalSchema,
|
|
351
|
+
answerSchema(question),
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
return frameworkCast(z.object(shape));
|
|
355
|
+
}
|
|
356
|
+
function answerSchema(question) {
|
|
357
|
+
switch (question.type) {
|
|
358
|
+
case 'boolean':
|
|
359
|
+
return z.object({
|
|
360
|
+
type: z.literal('boolean'),
|
|
361
|
+
value: z.boolean(),
|
|
362
|
+
probability: Probability,
|
|
363
|
+
});
|
|
364
|
+
case 'choice': {
|
|
365
|
+
const values = choiceValues(question.choices);
|
|
366
|
+
return z.object({
|
|
367
|
+
type: z.literal('choice'),
|
|
368
|
+
value: z.enum(values),
|
|
369
|
+
confidence: Probability,
|
|
370
|
+
probabilities: knownKeys(values),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
case 'score': {
|
|
374
|
+
const labels = scoreLevels(question.levels).map((l) => l.label);
|
|
375
|
+
return z.object({
|
|
376
|
+
type: z.literal('score'),
|
|
377
|
+
value: z
|
|
378
|
+
.number()
|
|
379
|
+
.min(0)
|
|
380
|
+
.max(labels.length - 1),
|
|
381
|
+
level: z.enum(labels),
|
|
382
|
+
confidence: Probability,
|
|
383
|
+
probabilities: knownKeys(labels),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/** A probability for every outcome and for nothing else. */
|
|
389
|
+
function knownKeys(keys) {
|
|
390
|
+
return z
|
|
391
|
+
.object(Object.fromEntries(keys.map((k) => [
|
|
392
|
+
k,
|
|
393
|
+
Probability,
|
|
394
|
+
])))
|
|
395
|
+
.strict();
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region Span
|
|
399
|
+
function stampSpan({ span, response, answers, }) {
|
|
400
|
+
span.setAttribute(GenAI.REQUEST_MODEL, response.modelId);
|
|
401
|
+
span.setAttribute(GenAI.USAGE_INPUT_TOKENS, response.usage.inputTokens);
|
|
402
|
+
span.setAttribute(GenAI.USAGE_OUTPUT_TOKENS, response.usage.outputTokens);
|
|
403
|
+
if (response.cost !== undefined) {
|
|
404
|
+
span.setAttribute(GenAI.COST, response.cost);
|
|
405
|
+
}
|
|
406
|
+
span.setAttribute(NoeticAttr.DECISION_SOURCE, response.source);
|
|
407
|
+
span.setAttribute(NoeticAttr.DECISION_REFUSED, refusedIds(answers).join(','));
|
|
408
|
+
for (const [id, answer] of Object.entries(answers)) {
|
|
409
|
+
const prefix = `${NoeticAttr.DECISION_PREFIX}${id}`;
|
|
410
|
+
if (answer.type === 'refusal') {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
span.setAttribute(`${prefix}.value`, answer.value);
|
|
414
|
+
span.setAttribute(answer.type === 'boolean' ? `${prefix}.probability` : `${prefix}.confidence`, answer.type === 'boolean' ? answer.probability : answer.confidence);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
@@ -5,6 +5,7 @@ import { DetachedHandleImpl } from '../util/detached-handle.js';
|
|
|
5
5
|
import { executeAcpAgent } from './execute-acp-agent.js';
|
|
6
6
|
import { executeCallModel, executeInvokeTool, executeRunCode, executeSpawn, executeWithContext, } from './execute-action.js';
|
|
7
7
|
import { executeConditional, executeInParallel, executeLoop, executeSchedule, } from './execute-control.js';
|
|
8
|
+
import { executeDecide } from './execute-decide.js';
|
|
8
9
|
import { isMutableContext } from './typeguards.js';
|
|
9
10
|
//#region Constants
|
|
10
11
|
const MAX_DEPTH = 64;
|
|
@@ -31,11 +32,12 @@ function buildSpawnOpts(ctx) {
|
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
* Resolve the per-step framework-event emit option.
|
|
35
|
-
* step kinds carry an `emit` field; every other kind defaults to
|
|
35
|
+
* Resolve the per-step framework-event emit option. `callModel`, `decide`, and
|
|
36
|
+
* the harness step kinds carry an `emit` field; every other kind defaults to
|
|
37
|
+
* enabled.
|
|
36
38
|
*/
|
|
37
39
|
function resolveStepEmit(step) {
|
|
38
|
-
if (step.kind === 'callModel' || step.kind === 'acp-agent') {
|
|
40
|
+
if (step.kind === 'callModel' || step.kind === 'decide' || step.kind === 'acp-agent') {
|
|
39
41
|
return step.emit;
|
|
40
42
|
}
|
|
41
43
|
return undefined;
|
|
@@ -260,6 +262,9 @@ export async function execute(step, input, ctx) {
|
|
|
260
262
|
case 'invokeTool':
|
|
261
263
|
result = await executeInvokeTool(step, input, ctx, baseCtx.harness);
|
|
262
264
|
break;
|
|
265
|
+
case 'decide':
|
|
266
|
+
result = await executeDecide(step, input, ctx, baseCtx.layers);
|
|
267
|
+
break;
|
|
263
268
|
case 'conditional':
|
|
264
269
|
result = await executeConditional(step, input, ctx, (s, i, c) => execute(s, i, c));
|
|
265
270
|
break;
|
|
@@ -50,4 +50,10 @@ export declare const NoeticAttr: {
|
|
|
50
50
|
readonly CONTEXT_LAYER_PLACEMENTS: 'noetic.context.layer_placements';
|
|
51
51
|
/** JSON array of `{ id, rate, rebillTokens }` per contributing layer. */
|
|
52
52
|
readonly CONTEXT_LAYER_CHURN: 'noetic.context.layer_churn';
|
|
53
|
+
/** `native` or `estimated` — where a decision's probabilities came from. */
|
|
54
|
+
readonly DECISION_SOURCE: 'noetic.decision.source';
|
|
55
|
+
/** Comma-separated ids of questions the model refused. */
|
|
56
|
+
readonly DECISION_REFUSED: 'noetic.decision.refused';
|
|
57
|
+
/** Prefix for per-question attributes: `noetic.decision.<id>.value` etc. */
|
|
58
|
+
readonly DECISION_PREFIX: 'noetic.decision.';
|
|
53
59
|
};
|
|
@@ -53,4 +53,10 @@ export const NoeticAttr = {
|
|
|
53
53
|
CONTEXT_LAYER_PLACEMENTS: 'noetic.context.layer_placements',
|
|
54
54
|
/** JSON array of `{ id, rate, rebillTokens }` per contributing layer. */
|
|
55
55
|
CONTEXT_LAYER_CHURN: 'noetic.context.layer_churn',
|
|
56
|
+
/** `native` or `estimated` — where a decision's probabilities came from. */
|
|
57
|
+
DECISION_SOURCE: 'noetic.decision.source',
|
|
58
|
+
/** Comma-separated ids of questions the model refused. */
|
|
59
|
+
DECISION_REFUSED: 'noetic.decision.refused',
|
|
60
|
+
/** Prefix for per-question attributes: `noetic.decision.<id>.value` etc. */
|
|
61
|
+
DECISION_PREFIX: 'noetic.decision.',
|
|
56
62
|
};
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
/** @public */
|
|
6
6
|
/** @public */
|
|
7
7
|
/** @public */
|
|
8
|
-
export { history, observations, plan, scratchpad, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
8
|
+
export { decisionsHistoryLayer, history, observations, plan, scratchpad, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
/** @public */
|
|
6
6
|
/** @public */
|
|
7
7
|
/** @public */
|
|
8
|
-
export { history, observations, plan, scratchpad, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
8
|
+
export { decisionsHistoryLayer, history, observations, plan, scratchpad, taskState, temporal, toolCalls, } from '@noetic-tools/context';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isNoeticError } from '@noetic-tools/types';
|
|
1
2
|
import { emitFrameworkEvent } from './broadcaster-utils.js';
|
|
2
3
|
import { EventBroadcaster } from './event-broadcaster.js';
|
|
3
4
|
import { MessageQueue } from './message-queue.js';
|
|
@@ -240,6 +241,9 @@ export class SessionRunner {
|
|
|
240
241
|
data: {
|
|
241
242
|
turnId,
|
|
242
243
|
reason: error.message,
|
|
244
|
+
// The typed kind ('cancelled', 'model_refused', …) so consumers can
|
|
245
|
+
// classify the abort without parsing the human-readable message.
|
|
246
|
+
errorKind: isNoeticError(error) ? error.noeticError.kind : undefined,
|
|
243
247
|
},
|
|
244
248
|
});
|
|
245
249
|
this.rejectWaiters(error);
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* consumers can validate workflow documents without importing core.
|
|
4
4
|
* This barrel keeps core's public and intra-core import paths unchanged.
|
|
5
5
|
*/
|
|
6
|
-
export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, OutputCodecRef, RunCodeWorkflowNode, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowGraph, WorkflowNode, } from '@noetic-tools/types';
|
|
6
|
+
export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, DecideWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, OutputCodecRef, RouteSwitchWorkflowNode, RunCodeWorkflowNode, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowGraph, WorkflowNode, } from '@noetic-tools/types';
|
|
7
7
|
export { MergeStrategy, MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, workflowGraph, } from '@noetic-tools/types';
|