@noetic-tools/core 5.5.0 → 5.7.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.
Files changed (39) hide show
  1. package/dist/builders/control-flow-builders.d.ts +6 -4
  2. package/dist/builders/control-flow-builders.js +3 -2
  3. package/dist/builders/decide-builders.d.ts +86 -0
  4. package/dist/builders/decide-builders.js +230 -0
  5. package/dist/builders/dynamic-workflow.js +1 -0
  6. package/dist/builders/workflow-hydrator.js +81 -2
  7. package/dist/conditions/conditions.d.ts +9 -3
  8. package/dist/conditions/conditions.js +84 -25
  9. package/dist/harness/agent-harness.d.ts +18 -3
  10. package/dist/harness/agent-harness.js +63 -6
  11. package/dist/harness/decide-call.d.ts +19 -0
  12. package/dist/harness/decide-call.js +483 -0
  13. package/dist/harness/deps/adapters.d.ts +1 -0
  14. package/dist/harness/deps/adapters.js +1 -0
  15. package/dist/harness/deps/context.d.ts +1 -1
  16. package/dist/harness/deps/context.js +1 -1
  17. package/dist/harness/deps/types.d.ts +1 -1
  18. package/dist/index.d.ts +14 -6
  19. package/dist/index.js +6 -2
  20. package/dist/interpreter/action-deps.d.ts +1 -1
  21. package/dist/interpreter/action-deps.js +1 -1
  22. package/dist/interpreter/action-types.d.ts +1 -1
  23. package/dist/interpreter/collect-tools.js +2 -0
  24. package/dist/interpreter/execute-action.js +3 -20
  25. package/dist/interpreter/execute-control.js +1 -1
  26. package/dist/interpreter/execute-decide.d.ts +10 -0
  27. package/dist/interpreter/execute-decide.js +417 -0
  28. package/dist/interpreter/execute.js +8 -3
  29. package/dist/observability/genai-attributes.d.ts +6 -0
  30. package/dist/observability/genai-attributes.js +6 -0
  31. package/dist/portable/context-layers.d.ts +1 -1
  32. package/dist/portable/context-layers.js +1 -1
  33. package/dist/runtime/context-impl.d.ts +3 -0
  34. package/dist/runtime/context-impl.js +2 -0
  35. package/dist/schemas/workflow.d.ts +1 -1
  36. package/dist/util/status-code.d.ts +2 -0
  37. package/dist/util/status-code.js +7 -0
  38. package/package.json +7 -6
  39. package/schema/noetic-workflow.schema.json +396 -0
@@ -656,19 +656,6 @@ async function collectSpawnItems({ layers, parentExecutionCtx, childExecutionCtx
656
656
  });
657
657
  return spawnResults.flatMap((r) => r.items);
658
658
  }
659
- /** Adds a spawned child's accumulated token/cost usage to its parent. */
660
- function rollUpSpawnUsage(parent, child) {
661
- if (!isMutableContext(parent)) {
662
- return;
663
- }
664
- parent.tokens.input += child.tokens.input;
665
- parent.tokens.output += child.tokens.output;
666
- parent.tokens.total += child.tokens.total;
667
- if (child.tokens.cached !== undefined) {
668
- parent.tokens.cached = (parent.tokens.cached ?? 0) + child.tokens.cached;
669
- }
670
- parent.cost += child.cost;
671
- }
672
659
  export async function executeSpawn(step, input, ctx, executeStep, opts) {
673
660
  const baseCtx = frameworkCast(ctx);
674
661
  const layers = resolveLayersForSpawn(step, opts?.parentLayers);
@@ -736,14 +723,10 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
736
723
  ledger: baseCtx instanceof ContextImpl ? baseCtx.ledger : undefined,
737
724
  });
738
725
  try {
739
- // Execute the child step
726
+ // Execute the child step. Its token/cost usage needs no merge here: every
727
+ // charge recorded on `childCtx` is also charged up its `usageParent`
728
+ // chain as it is spent, so a child that throws still bills the parent.
740
729
  const childOutput = await executeStep(step.child, input, frameworkCast(childCtx));
741
- // Roll the child's token/cost usage up into the parent. A sub-agent's
742
- // spend is the parent's spend; without this it would be stranded on the
743
- // child context and invisible to `ctx.tokens` / `ctx.cost` / until.maxCost.
744
- // Because every spawn boundary rolls up, nested sub-agents propagate to the
745
- // root recursively.
746
- rollUpSpawnUsage(baseCtx, childCtx);
747
730
  // Pipeline result through layer onReturn hooks
748
731
  if (!hasLayers || !parentExecutionCtx) {
749
732
  return childOutput;
@@ -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. Both `callModel` and the harness
35
- * step kinds carry an `emit` field; every other kind defaults to enabled.
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';
@@ -14,6 +14,7 @@ export declare class ContextImpl implements Context<ContextData> {
14
14
  cost: number;
15
15
  state: unknown;
16
16
  readonly parent: Context<ContextData> | null;
17
+ readonly usageParent: Context<ContextData> | null;
17
18
  readonly depth: number;
18
19
  readonly span: Span;
19
20
  readonly threadId: string;
@@ -98,6 +99,8 @@ export declare class ContextImpl implements Context<ContextData> {
98
99
  constructor(opts: {
99
100
  harness: AgentHarnessContract;
100
101
  parent?: Context;
102
+ /** Charge this context's usage to `parent` as it is spent. Default `true`. */
103
+ rollUpUsage?: boolean;
101
104
  items?: Item[];
102
105
  state?: unknown;
103
106
  threadId?: string;
@@ -38,6 +38,7 @@ export class ContextImpl {
38
38
  cost = 0;
39
39
  state;
40
40
  parent;
41
+ usageParent;
41
42
  depth;
42
43
  span;
43
44
  threadId;
@@ -125,6 +126,7 @@ export class ContextImpl {
125
126
  this.harness = opts.harness;
126
127
  this.state = opts.state ?? {};
127
128
  this.parent = opts.parent ?? null;
129
+ this.usageParent = opts.rollUpUsage === false ? null : this.parent;
128
130
  this.depth = this.parent ? this.parent.depth + 1 : 0;
129
131
  this.span = opts.span ?? new NoopSpan();
130
132
  this.threadId = opts.threadId ?? crypto.randomUUID();
@@ -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';
@@ -0,0 +1,2 @@
1
+ /** The HTTP status an SDK error carries, when it carries one. */
2
+ export declare function statusOf(e: unknown): number | undefined;
@@ -0,0 +1,7 @@
1
+ /** The HTTP status an SDK error carries, when it carries one. */
2
+ export function statusOf(e) {
3
+ if (typeof e !== 'object' || e === null || !('statusCode' in e)) {
4
+ return undefined;
5
+ }
6
+ return typeof e.statusCode === 'number' ? e.statusCode : undefined;
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/core",
3
- "version": "5.5.0",
3
+ "version": "5.7.0",
4
4
  "type": "module",
5
5
  "description": "Step primitives, interpreter, runtime, and context layer contract for Noetic agents.",
6
6
  "license": "Apache-2.0",
@@ -53,13 +53,13 @@
53
53
  "access": "public"
54
54
  },
55
55
  "devDependencies": {
56
- "@openrouter/agent": "^0.6.0",
56
+ "@openrouter/agent": "^0.11.0",
57
57
  "@valibot/to-json-schema": "1.7.1",
58
58
  "bun-types": "latest",
59
59
  "valibot": "1.4.2"
60
60
  },
61
61
  "peerDependencies": {
62
- "@openrouter/agent": "^0.6.0",
62
+ "@openrouter/agent": "^0.11.0",
63
63
  "zod": "^4.0.0"
64
64
  },
65
65
  "peerDependenciesMeta": {
@@ -68,9 +68,10 @@
68
68
  }
69
69
  },
70
70
  "dependencies": {
71
- "@noetic-tools/context": "^1.3.0",
72
- "@noetic-tools/types": "^3.3.0",
73
- "@openrouter/agent": "^0.6.0",
71
+ "@noetic-tools/context": "^1.4.0",
72
+ "@noetic-tools/types": "^3.5.0",
73
+ "@openrouter/agent": "^0.11.0",
74
+ "@openrouter/sdk": "1.4.25",
74
75
  "zod": "^4.0.0"
75
76
  },
76
77
  "scripts": {