@noetic-tools/types 3.3.0 → 3.4.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/index.d.ts CHANGED
@@ -16,9 +16,11 @@ export * from './types/context-layer.js';
16
16
  export * from './types/context-parts/item-log.js';
17
17
  export * from './types/context-parts/layer-usage.js';
18
18
  export * from './types/context-scope.js';
19
+ export * from './types/decide.js';
19
20
  export * from './types/detached.js';
20
21
  export * from './types/error.js';
21
22
  export * from './types/fs-adapter.js';
23
+ export * from './types/harness-judgment.js';
22
24
  export * from './types/harness-result.js';
23
25
  export * from './types/items.js';
24
26
  export * from './types/observability.js';
package/dist/index.js CHANGED
@@ -16,9 +16,11 @@ export * from './types/context-layer.js';
16
16
  export * from './types/context-parts/item-log.js';
17
17
  export * from './types/context-parts/layer-usage.js';
18
18
  export * from './types/context-scope.js';
19
+ export * from './types/decide.js';
19
20
  export * from './types/detached.js';
20
21
  export * from './types/error.js';
21
22
  export * from './types/fs-adapter.js';
23
+ export * from './types/harness-judgment.js';
22
24
  export * from './types/harness-result.js';
23
25
  export * from './types/items.js';
24
26
  export * from './types/observability.js';
@@ -9,6 +9,8 @@
9
9
  * document back into live `Step` objects via the existing builders.
10
10
  */
11
11
  import { z } from 'zod';
12
+ import type { DecideQuestion } from '../types/decide.js';
13
+ import type { DecideView } from '../types/step.js';
12
14
  interface UntilAnyPredicate {
13
15
  kind: 'any';
14
16
  predicates: UntilPredicate[];
@@ -234,6 +236,27 @@ export interface ConditionalWorkflowNode extends WorkflowNodeBase {
234
236
  routes: ConditionalRoute[];
235
237
  default?: WorkflowNode;
236
238
  }
239
+ /** A node that asks typed questions of a decision model. See spec 32. */
240
+ export interface DecideWorkflowNode extends WorkflowNodeBase {
241
+ kind: 'decide';
242
+ model?: string;
243
+ instructions?: string;
244
+ questions: Record<string, DecideQuestion>;
245
+ view?: DecideView;
246
+ samples?: number;
247
+ }
248
+ /** A node that routes to a case by one question of a decision. See spec 32. */
249
+ export interface RouteSwitchWorkflowNode extends WorkflowNodeBase {
250
+ kind: 'routeSwitch';
251
+ decide: DecideWorkflowNode;
252
+ /** Question id to route by. */
253
+ on: string;
254
+ /** One case per outcome of the `on` question. */
255
+ cases: Record<string, WorkflowNode>;
256
+ minConfidence?: number;
257
+ /** Taken on refusal, or when confidence is below `minConfidence`. */
258
+ uncertain: WorkflowNode;
259
+ }
237
260
  export interface InParallelWorkflowNode extends WorkflowNodeBase {
238
261
  kind: 'inParallel';
239
262
  mode: 'race' | 'all' | 'settle';
@@ -326,7 +349,7 @@ export interface SubflowWorkflowNode extends WorkflowNodeBase {
326
349
  input?: string;
327
350
  }
328
351
  /** @public Discriminated union of all JSON-serialisable workflow node kinds. */
329
- export type WorkflowNode = CallModelWorkflowNode | InvokeToolWorkflowNode | RunCodeWorkflowNode | ConditionalWorkflowNode | InParallelWorkflowNode | SpawnWorkflowNode | WithContextWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | ScheduleWorkflowNode | SubflowWorkflowNode | AcpAgentWorkflowNode;
352
+ export type WorkflowNode = CallModelWorkflowNode | InvokeToolWorkflowNode | RunCodeWorkflowNode | ConditionalWorkflowNode | DecideWorkflowNode | RouteSwitchWorkflowNode | InParallelWorkflowNode | SpawnWorkflowNode | WithContextWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | ScheduleWorkflowNode | SubflowWorkflowNode | AcpAgentWorkflowNode;
330
353
  /** @public Zod schema validating a single `WorkflowNode` (any JSON-safe kind). */
331
354
  export declare const WorkflowNodeSchema: z.ZodType<WorkflowNode>;
332
355
  /** @public Top-level envelope for a JSON workflow definition. */
@@ -9,6 +9,7 @@
9
9
  * document back into live `Step` objects via the existing builders.
10
10
  */
11
11
  import { z } from 'zod';
12
+ import { DecideQuestionsSchema, routeOutcomes } from '../types/decide.js';
12
13
  //#endregion
13
14
  //#region Until Predicate Schemas
14
15
  const MaxStepsPredicateSchema = z.object({
@@ -245,6 +246,56 @@ const ConditionalNodeSchema = z.object({
245
246
  routes: z.array(ConditionalRouteSchema).min(1),
246
247
  default: WorkflowNodeRef.optional(),
247
248
  });
249
+ const DecideNodeSchema = z.object({
250
+ kind: z.literal('decide'),
251
+ ...SHARED_FIELDS,
252
+ model: z.string().min(1).optional(),
253
+ instructions: z.string().min(1).optional(),
254
+ questions: DecideQuestionsSchema,
255
+ view: z
256
+ .enum([
257
+ 'full',
258
+ 'layers',
259
+ 'none',
260
+ ])
261
+ .optional(),
262
+ samples: z.number().int().positive().optional(),
263
+ });
264
+ const RouteSwitchNodeSchema = z
265
+ .object({
266
+ kind: z.literal('routeSwitch'),
267
+ ...SHARED_FIELDS,
268
+ decide: DecideNodeSchema,
269
+ on: z.string().min(1),
270
+ cases: z.record(z.string().min(1), WorkflowNodeRef),
271
+ minConfidence: z.number().min(0).max(1).optional(),
272
+ uncertain: WorkflowNodeRef,
273
+ })
274
+ .superRefine((node, issue) => {
275
+ const question = Object.hasOwn(node.decide.questions, node.on)
276
+ ? node.decide.questions[node.on]
277
+ : undefined;
278
+ if (!question) {
279
+ issue.addIssue({
280
+ code: 'custom',
281
+ path: [
282
+ 'on',
283
+ ],
284
+ message: `routeSwitch 'on' names no question of its decide node: '${node.on}'.`,
285
+ });
286
+ return;
287
+ }
288
+ const missing = routeOutcomes(question).filter((o) => !Object.hasOwn(node.cases, o));
289
+ if (missing.length > 0) {
290
+ issue.addIssue({
291
+ code: 'custom',
292
+ path: [
293
+ 'cases',
294
+ ],
295
+ message: `routeSwitch cases miss outcomes of '${node.on}': ${missing.join(', ')}.`,
296
+ });
297
+ }
298
+ });
248
299
  const InParallelNodeSchema = z
249
300
  .object({
250
301
  kind: z.literal('inParallel'),
@@ -332,6 +383,8 @@ export const WorkflowNodeSchema = z
332
383
  InvokeToolNodeSchema,
333
384
  RunCodeNodeSchema,
334
385
  ConditionalNodeSchema,
386
+ DecideNodeSchema,
387
+ RouteSwitchNodeSchema,
335
388
  InParallelNodeSchema,
336
389
  SpawnNodeSchema,
337
390
  WithContextNodeSchema,
@@ -390,6 +443,12 @@ function childNodes(node) {
390
443
  }
391
444
  return children;
392
445
  }
446
+ case 'routeSwitch':
447
+ return [
448
+ node.decide,
449
+ ...Object.values(node.cases),
450
+ node.uncertain,
451
+ ];
393
452
  case 'subflow':
394
453
  // A named ref is a static leaf: the target document is only known at
395
454
  // hydration time, so walk/graph/depth cannot see through it.
@@ -1,3 +1,4 @@
1
+ import type { DecisionMeta } from './decide.js';
1
2
  import type { FunctionCallItem, Item } from './items.js';
2
3
  /**
3
4
  * Policy controlling automatic retry behavior on step failure.
@@ -90,6 +91,8 @@ export interface StepMeta {
90
91
  * `max_turn_requests`) from a complete one — both return normally.
91
92
  */
92
93
  acpStopReason?: string;
94
+ /** Set by `decide` steps. */
95
+ decision?: DecisionMeta;
93
96
  }
94
97
  /** @public Structured response returned by a model adapter after an LLM call. */
95
98
  export interface LLMResponse {
@@ -3,7 +3,8 @@ import type { Channel, ChannelHandle, ExternalChannel } from './channel.js';
3
3
  import type { LLMResponse, ModelParams } from './common.js';
4
4
  import type { Context, CwdState, RestoreContextOptions } from './context.js';
5
5
  import type { ContextCacheConfig, ContextCacheStore } from './context-cache.js';
6
- import type { ContextData, ContextLayer, ProjectionPolicy, StorageAdapter } from './context-layer.js';
6
+ import type { ContextData, ContextLayer, ProjectionPolicy, RecallOptions, StorageAdapter } from './context-layer.js';
7
+ import type { DecideRequest, DecideResponse, DecisionRecord } from './decide.js';
7
8
  import type { DetachedHandle } from './detached.js';
8
9
  import type { FsAdapter } from './fs-adapter.js';
9
10
  import type { HarnessResponse, StreamEvent, StreamingItem } from './harness-result.js';
@@ -76,6 +77,10 @@ export type ContextStep<TContext = ContextData, I = unknown, O = unknown> = {
76
77
  readonly kind: 'callModel';
77
78
  readonly id: string;
78
79
  readonly output?: StandardSchemaV1<unknown, O>;
80
+ } | {
81
+ readonly kind: 'decide';
82
+ readonly id: string;
83
+ readonly _answers?: O;
79
84
  } | {
80
85
  readonly kind: 'loop';
81
86
  readonly id: string;
@@ -94,12 +99,17 @@ export interface ContextHarness {
94
99
  readonly projection?: ProjectionPolicy;
95
100
  /** Tuning for prompt-cache anchoring. */
96
101
  readonly contextCache?: ContextCacheConfig;
102
+ /** Default model for `decide` steps. */
103
+ readonly decisionModel?: string;
97
104
  };
98
105
  readonly fs: FsAdapter;
99
106
  readonly shell: ShellAdapter;
100
107
  readonly subprocess: SubprocessAdapter;
101
108
  readonly rootCwdState: CwdState;
102
109
  callModel(request: ContextCallModelRequest): Promise<LLMResponse>;
110
+ decide(request: DecideRequest): Promise<DecideResponse>;
111
+ /** Run every layer's `onDecision` hook, in slot order. */
112
+ notifyDecision(layers: ContextLayer[], record: DecisionRecord, ctx: Context): Promise<void>;
103
113
  execute(input: ExecuteInput, options?: unknown): Promise<void>;
104
114
  getAgentResponse(scope?: unknown): Promise<HarnessResponse>;
105
115
  getItemStream(scope?: unknown): AsyncIterable<StreamingItem>;
@@ -126,8 +136,8 @@ export interface ContextHarness {
126
136
  /** Pinned anchor output and epoch bookkeeping for prompt-cache anchoring. Absent on a harness that predates it. */
127
137
  readonly contextCache?: ContextCacheStore;
128
138
  recallLayers(layers: ContextLayer[], input: string, ctx: Context): Promise<ContextRecallLayerOutput[]>;
129
- recallLayersAtomic(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<ContextRecallLayerOutput[]>;
130
- recallLayersEventual(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<ContextRecallLayerOutput[]>;
139
+ recallLayersAtomic(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>, opts?: RecallOptions): Promise<ContextRecallLayerOutput[]>;
140
+ recallLayersEventual(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>, opts?: RecallOptions): Promise<ContextRecallLayerOutput[]>;
131
141
  projectHistory(layers: ContextLayer[], items: ReadonlyArray<Item>, ctx: Context): Promise<ReadonlyArray<Item>>;
132
142
  storeLayers(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<void>;
133
143
  previewRequestItems(scope?: unknown): Promise<ReadonlyArray<Item>>;
@@ -2,6 +2,7 @@ import type { ZodType } from 'zod';
2
2
  import type { LLMResponse } from './common.js';
3
3
  import type { ItemLog } from './context-parts/item-log.js';
4
4
  import type { ContextScope, ExecutionContext, ExecutionOutcome } from './context-scope.js';
5
+ import type { DecisionRecord } from './decide.js';
5
6
  import type { Item, ItemSchemaExtensions } from './items.js';
6
7
  import type { AfterModelCallParams, AfterModelCallResult, BeforeToolCallParams, BeforeToolCallResult } from './steering.js';
7
8
  /** @public Isolation scope controlling how a context layer's state is keyed and shared. */
@@ -22,6 +23,7 @@ export interface LayerTimeouts {
22
23
  afterModelCall?: number;
23
24
  onItemAppend?: number;
24
25
  projectHistory?: number;
26
+ onDecision?: number;
25
27
  }
26
28
  /** @public A read-only data projection from layer state, accessible via `ctx.context['layerId'].prop`. */
27
29
  export interface LayerDataDecl<T = unknown, TState = unknown> {
@@ -301,6 +303,21 @@ export interface OnItemAppendResult<TState> {
301
303
  /** Which layers to re-recall (default: 'slot-after'). */
302
304
  scope?: RerenderScope;
303
305
  }
306
+ /** @public Options for a recall pass. */
307
+ export interface RecallOptions {
308
+ /**
309
+ * Render without leaving a trace: returned state is not committed and
310
+ * eventual-layer output is not cached. A `decide` step peeks so that it never
311
+ * drains state (such as queued steering guidance) meant for the next model call.
312
+ */
313
+ peek?: boolean;
314
+ }
315
+ /** @public Parameters for the `onDecision` hook. */
316
+ export interface OnDecisionParams<TState> {
317
+ state: TState;
318
+ record: DecisionRecord;
319
+ ctx: ExecutionContext;
320
+ }
304
321
  /** @public Lifecycle hook implementations for a context layer. Method syntax enables bivariant assignability for typed layers. */
305
322
  export interface ContextLayerHooks<TState = unknown> {
306
323
  init?(params: InitParams): Promise<InitResult<TState>>;
@@ -343,6 +360,13 @@ export interface ContextLayerHooks<TState = unknown> {
343
360
  * a row. Never called for layers in the `'live'` band, which re-render anyway.
344
361
  */
345
362
  renderDelta?(params: RenderDeltaParams<TState>): Promise<string | null>;
363
+ /**
364
+ * Called after every `decide` step completes, in slot order. Return new state
365
+ * to record the decision.
366
+ */
367
+ onDecision?(params: OnDecisionParams<TState>): Promise<undefined | {
368
+ state: TState;
369
+ }>;
346
370
  }
347
371
  /**
348
372
  * A composable context layer that participates in the recall/store lifecycle.
@@ -0,0 +1,270 @@
1
+ import type { ZodError, ZodType } from 'zod';
2
+ import { z } from 'zod';
3
+ import type { Span } from './observability.js';
4
+ /** @public A yes/no question. The answer's `value` is `probability >= threshold`. */
5
+ export interface BooleanQuestion {
6
+ type: 'boolean';
7
+ instructions?: string;
8
+ /** What yes and no mean. Required when `instructions` is absent. */
9
+ criteria?: {
10
+ true?: string;
11
+ false?: string;
12
+ };
13
+ /** Probability at or above which `value` is `true`. Default 0.5. */
14
+ threshold?: number;
15
+ }
16
+ /** @public Pick one of a set of unordered options. */
17
+ export interface ChoiceQuestion<K extends string = string> {
18
+ type: 'choice';
19
+ instructions?: string;
20
+ /** 1–255 options: bare values, or values mapped to descriptions. */
21
+ choices: readonly K[] | Readonly<Record<K, string>>;
22
+ }
23
+ /** @public A level on an ordered scale. */
24
+ export interface ScoreLevel<L extends string = string> {
25
+ label: L;
26
+ description?: string;
27
+ }
28
+ /**
29
+ * @public Place the state on an ordered scale. Levels run lowest first, and are
30
+ * an array because a record cannot carry order: JavaScript sorts integer-like
31
+ * keys ahead of all others.
32
+ */
33
+ export interface ScoreQuestion<L extends string = string> {
34
+ type: 'score';
35
+ instructions?: string;
36
+ /** 2–10 levels, lowest first. */
37
+ levels: readonly L[] | ReadonlyArray<ScoreLevel<L>>;
38
+ }
39
+ /** @public Any decide question. */
40
+ export type DecideQuestion = BooleanQuestion | ChoiceQuestion | ScoreQuestion;
41
+ /** @public Question map keyed by question id. */
42
+ export type DecideQuestions = Readonly<Record<string, DecideQuestion>>;
43
+ /** @public Answer to a boolean question. */
44
+ export interface BooleanAnswer {
45
+ type: 'boolean';
46
+ /** `probability >= threshold`. */
47
+ value: boolean;
48
+ /** Probability of yes, in `[0, 1]`. */
49
+ probability: number;
50
+ }
51
+ /** @public Answer to a choice question. */
52
+ export interface ChoiceAnswer<K extends string = string> {
53
+ type: 'choice';
54
+ /** The most probable choice. */
55
+ value: K;
56
+ /** The provider's own certainty measure — not the top probability. */
57
+ confidence: number;
58
+ probabilities: Readonly<Record<K, number>>;
59
+ }
60
+ /** @public Answer to a score question. */
61
+ export interface ScoreAnswer<L extends string = string> {
62
+ type: 'score';
63
+ /** Probability-weighted level index, 0 = lowest. */
64
+ value: number;
65
+ /** The most probable level. Not `value` rounded: a split distribution can differ. */
66
+ level: L;
67
+ /** The provider's own certainty measure — not the top probability. */
68
+ confidence: number;
69
+ probabilities: Readonly<Record<L, number>>;
70
+ }
71
+ /** @public The model declined to answer this question. */
72
+ export interface Refusal {
73
+ type: 'refusal';
74
+ }
75
+ /** @public Any answer, untyped by question. */
76
+ export type DecideAnswer = BooleanAnswer | ChoiceAnswer | ScoreAnswer | Refusal;
77
+ /** @public Choice values declared by a choice question's `choices`. */
78
+ export type ChoiceValues<C> = C extends readonly (infer K extends string)[] ? K : C extends Readonly<Record<infer K extends string | number, string>> ? `${K}` : never;
79
+ /** @public Level labels declared by a score question's `levels`. */
80
+ export type LevelLabels<L> = L extends readonly (infer E)[] ? E extends string ? E : E extends {
81
+ label: infer K extends string;
82
+ } ? K : never : never;
83
+ /** @public The answer type a question produces, refusal excluded. */
84
+ export type AnswerFor<Q> = Q extends {
85
+ type: 'boolean';
86
+ } ? BooleanAnswer : Q extends {
87
+ type: 'choice';
88
+ choices: infer C;
89
+ } ? ChoiceAnswer<ChoiceValues<C>> : Q extends {
90
+ type: 'score';
91
+ levels: infer L;
92
+ } ? ScoreAnswer<LevelLabels<L>> : never;
93
+ /** @public Typed answers for a question map. Any question may be refused. */
94
+ export type DecideAnswers<Q extends DecideQuestions> = {
95
+ readonly [K in keyof Q]: AnswerFor<Q[K]> | Refusal;
96
+ };
97
+ /** @public Case keys a question routes to: its choices, `'true' | 'false'`, or its level labels. */
98
+ export type CaseKeyOf<Q> = Q extends {
99
+ type: 'boolean';
100
+ } ? 'true' | 'false' : Q extends {
101
+ type: 'choice';
102
+ choices: infer C;
103
+ } ? ChoiceValues<C> : Q extends {
104
+ type: 'score';
105
+ levels: infer L;
106
+ } ? LevelLabels<L> : never;
107
+ /** @public Typed answers with refusals removed. */
108
+ export type AnsweredAll<Q extends DecideQuestions> = {
109
+ readonly [K in keyof Q]: AnswerFor<Q[K]>;
110
+ };
111
+ /** @public One piece of the state a decision is made against. */
112
+ export type DecideStatePart = {
113
+ type: 'text';
114
+ text: string;
115
+ name?: string;
116
+ } | {
117
+ type: 'json';
118
+ name: string;
119
+ value: unknown;
120
+ } | {
121
+ type: 'image';
122
+ imageUrl: string;
123
+ };
124
+ /**
125
+ * @public `native`: a decision model's own distribution. `estimated`: a chat
126
+ * model's self-reported probabilities, which are not calibrated.
127
+ */
128
+ export type DecisionSource = 'native' | 'estimated';
129
+ /** @public Request for `harness.decide()`, in Noetic's vocabulary. Adapters translate it per provider. */
130
+ export interface DecideRequest {
131
+ model: string;
132
+ instructions?: string;
133
+ state: ReadonlyArray<DecideStatePart>;
134
+ questions: DecideQuestions;
135
+ /** Runs averaged on the estimated path. Ignored on the native path. Default 1. */
136
+ samples?: number;
137
+ signal?: AbortSignal;
138
+ /** @internal Parent span for the decide-call span. */
139
+ parentSpan?: Span;
140
+ }
141
+ /** @public Response from `harness.decide()`. */
142
+ export interface DecideResponse {
143
+ answers: Readonly<Record<string, DecideAnswer>>;
144
+ source: DecisionSource;
145
+ /** Model that answered, as reported by the provider. */
146
+ modelId: string;
147
+ usage: {
148
+ inputTokens: number;
149
+ outputTokens: number;
150
+ };
151
+ cost?: number;
152
+ }
153
+ /** @public Decision metadata recorded on `ctx.lastStepMeta.decision`. */
154
+ export interface DecisionMeta {
155
+ modelId: string;
156
+ source: DecisionSource;
157
+ /** Ids of refused questions. */
158
+ refused: readonly string[];
159
+ }
160
+ /** @public One completed decision, as layers see it through `onDecision`. */
161
+ export interface DecisionRecord {
162
+ stepId: string;
163
+ /** Epoch ms. */
164
+ at: number;
165
+ modelId: string;
166
+ source: DecisionSource;
167
+ answers: Readonly<Record<string, DecideAnswer>>;
168
+ }
169
+ /**
170
+ * A chat model's estimate did not match the requested shape. The interpreter
171
+ * turns it into `model_parse_error`, which needs the step id this layer lacks.
172
+ * @public
173
+ */
174
+ export declare class DecideOutputError extends Error {
175
+ readonly raw: string;
176
+ readonly schema: ZodType;
177
+ readonly zodError: ZodError;
178
+ /** Tokens the failed estimate still cost, so the step can count them. */
179
+ readonly usage?: DecideResponse['usage'];
180
+ readonly cost?: number;
181
+ constructor(opts: {
182
+ raw: string;
183
+ schema: ZodType;
184
+ zodError: ZodError;
185
+ usage?: DecideResponse['usage'];
186
+ cost?: number;
187
+ });
188
+ }
189
+ /** Choice and level limits shared by the OpenAI and TypeSafe decision APIs. */
190
+ export declare const DECIDE_LIMITS: {
191
+ readonly maxChoices: 255;
192
+ readonly minLevels: 2;
193
+ readonly maxLevels: 10;
194
+ };
195
+ /** @public Zod schema for a boolean question. */
196
+ export declare const BooleanQuestionSchema: z.ZodObject<{
197
+ type: z.ZodLiteral<"boolean">;
198
+ instructions: z.ZodOptional<z.ZodString>;
199
+ criteria: z.ZodOptional<z.ZodObject<{
200
+ true: z.ZodOptional<z.ZodString>;
201
+ false: z.ZodOptional<z.ZodString>;
202
+ }, z.core.$strip>>;
203
+ threshold: z.ZodOptional<z.ZodNumber>;
204
+ }, z.core.$strip>;
205
+ /** @public Zod schema for a choice question. */
206
+ export declare const ChoiceQuestionSchema: z.ZodObject<{
207
+ type: z.ZodLiteral<"choice">;
208
+ instructions: z.ZodOptional<z.ZodString>;
209
+ choices: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodString>]>;
210
+ }, z.core.$strip>;
211
+ /** @public Zod schema for a score question. */
212
+ export declare const ScoreQuestionSchema: z.ZodObject<{
213
+ type: z.ZodLiteral<"score">;
214
+ instructions: z.ZodOptional<z.ZodString>;
215
+ levels: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodArray<z.ZodObject<{
216
+ label: z.ZodString;
217
+ description: z.ZodOptional<z.ZodString>;
218
+ }, z.core.$strip>>]>;
219
+ }, z.core.$strip>;
220
+ /** @public Zod schema for any decide question. */
221
+ export declare const DecideQuestionSchema: z.ZodUnion<readonly [z.ZodObject<{
222
+ type: z.ZodLiteral<"boolean">;
223
+ instructions: z.ZodOptional<z.ZodString>;
224
+ criteria: z.ZodOptional<z.ZodObject<{
225
+ true: z.ZodOptional<z.ZodString>;
226
+ false: z.ZodOptional<z.ZodString>;
227
+ }, z.core.$strip>>;
228
+ threshold: z.ZodOptional<z.ZodNumber>;
229
+ }, z.core.$strip>, z.ZodObject<{
230
+ type: z.ZodLiteral<"choice">;
231
+ instructions: z.ZodOptional<z.ZodString>;
232
+ choices: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodString>]>;
233
+ }, z.core.$strip>, z.ZodObject<{
234
+ type: z.ZodLiteral<"score">;
235
+ instructions: z.ZodOptional<z.ZodString>;
236
+ levels: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodArray<z.ZodObject<{
237
+ label: z.ZodString;
238
+ description: z.ZodOptional<z.ZodString>;
239
+ }, z.core.$strip>>]>;
240
+ }, z.core.$strip>]>;
241
+ /** @public Zod schema for a question map: at least one question. */
242
+ export declare const DecideQuestionsSchema: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
243
+ type: z.ZodLiteral<"boolean">;
244
+ instructions: z.ZodOptional<z.ZodString>;
245
+ criteria: z.ZodOptional<z.ZodObject<{
246
+ true: z.ZodOptional<z.ZodString>;
247
+ false: z.ZodOptional<z.ZodString>;
248
+ }, z.core.$strip>>;
249
+ threshold: z.ZodOptional<z.ZodNumber>;
250
+ }, z.core.$strip>, z.ZodObject<{
251
+ type: z.ZodLiteral<"choice">;
252
+ instructions: z.ZodOptional<z.ZodString>;
253
+ choices: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodString>]>;
254
+ }, z.core.$strip>, z.ZodObject<{
255
+ type: z.ZodLiteral<"score">;
256
+ instructions: z.ZodOptional<z.ZodString>;
257
+ levels: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodArray<z.ZodObject<{
258
+ label: z.ZodString;
259
+ description: z.ZodOptional<z.ZodString>;
260
+ }, z.core.$strip>>]>;
261
+ }, z.core.$strip>]>>;
262
+ /** @public Choice values in declaration order. */
263
+ export declare function choiceValues(choices: ChoiceQuestion['choices']): string[];
264
+ /** @public Score levels, lowest first, in object form. */
265
+ export declare function scoreLevels(levels: ScoreQuestion['levels']): ScoreLevel[];
266
+ /**
267
+ * @public Case keys a question routes to: its choices, `'true'`/`'false'`, or
268
+ * its level labels.
269
+ */
270
+ export declare function routeOutcomes(question: DecideQuestion): string[];
@@ -0,0 +1,142 @@
1
+ import { z } from 'zod';
2
+ //#endregion
3
+ /**
4
+ * A chat model's estimate did not match the requested shape. The interpreter
5
+ * turns it into `model_parse_error`, which needs the step id this layer lacks.
6
+ * @public
7
+ */
8
+ export class DecideOutputError extends Error {
9
+ raw;
10
+ schema;
11
+ zodError;
12
+ /** Tokens the failed estimate still cost, so the step can count them. */
13
+ usage;
14
+ cost;
15
+ constructor(opts) {
16
+ super(`decision estimate did not match its schema: ${opts.zodError.message}`);
17
+ this.name = 'DecideOutputError';
18
+ this.raw = opts.raw;
19
+ this.schema = opts.schema;
20
+ this.zodError = opts.zodError;
21
+ this.usage = opts.usage;
22
+ this.cost = opts.cost;
23
+ }
24
+ }
25
+ //#endregion
26
+ //#region Schemas
27
+ /** Choice and level limits shared by the OpenAI and TypeSafe decision APIs. */
28
+ export const DECIDE_LIMITS = {
29
+ maxChoices: 255,
30
+ minLevels: 2,
31
+ maxLevels: 10,
32
+ };
33
+ const InstructionsSchema = z.string().min(1).optional();
34
+ /** @public Zod schema for a boolean question. */
35
+ export const BooleanQuestionSchema = z
36
+ .object({
37
+ type: z.literal('boolean'),
38
+ instructions: InstructionsSchema,
39
+ criteria: z
40
+ .object({
41
+ true: z.string().min(1).optional(),
42
+ false: z.string().min(1).optional(),
43
+ })
44
+ .optional(),
45
+ threshold: z.number().min(0).max(1).optional(),
46
+ })
47
+ .refine((q) => q.instructions !== undefined || q.criteria?.true || q.criteria?.false, {
48
+ message: 'boolean question needs `instructions` or a `criteria` description.',
49
+ });
50
+ /** @public Zod schema for a choice question. */
51
+ export const ChoiceQuestionSchema = z
52
+ .object({
53
+ type: z.literal('choice'),
54
+ instructions: InstructionsSchema,
55
+ choices: z.union([
56
+ z.array(z.string().min(1)).min(1).max(DECIDE_LIMITS.maxChoices),
57
+ z.record(z.string().min(1), z.string()),
58
+ ]),
59
+ })
60
+ .refine((q) => choiceValues(q.choices).length >= 1, {
61
+ message: 'choice question needs at least one choice.',
62
+ })
63
+ .refine((q) => choiceValues(q.choices).length <= DECIDE_LIMITS.maxChoices, {
64
+ message: `choice question allows at most ${DECIDE_LIMITS.maxChoices} choices.`,
65
+ })
66
+ .refine((q) => unique(choiceValues(q.choices)), {
67
+ message: 'choice values must be unique.',
68
+ });
69
+ /** @public Zod schema for a score question. */
70
+ export const ScoreQuestionSchema = z
71
+ .object({
72
+ type: z.literal('score'),
73
+ instructions: InstructionsSchema,
74
+ levels: z
75
+ .union([
76
+ z.array(z.string().min(1)),
77
+ z.array(z.object({
78
+ label: z.string().min(1),
79
+ description: z.string().optional(),
80
+ })),
81
+ ])
82
+ .refine((l) => l.length >= DECIDE_LIMITS.minLevels && l.length <= DECIDE_LIMITS.maxLevels, {
83
+ message: `score question needs ${DECIDE_LIMITS.minLevels}–${DECIDE_LIMITS.maxLevels} levels.`,
84
+ }),
85
+ })
86
+ .refine((q) => unique(levelLabels(q.levels)), {
87
+ message: 'score level labels must be unique.',
88
+ });
89
+ /** @public Zod schema for any decide question. */
90
+ export const DecideQuestionSchema = z.union([
91
+ BooleanQuestionSchema,
92
+ ChoiceQuestionSchema,
93
+ ScoreQuestionSchema,
94
+ ]);
95
+ /** @public Zod schema for a question map: at least one question. */
96
+ export const DecideQuestionsSchema = z
97
+ .record(z.string().min(1), DecideQuestionSchema)
98
+ .refine((q) => Object.keys(q).length > 0, {
99
+ message: 'decide needs at least one question.',
100
+ });
101
+ //#endregion
102
+ //#region Helpers
103
+ /** @public Choice values in declaration order. */
104
+ export function choiceValues(choices) {
105
+ return Array.isArray(choices)
106
+ ? [
107
+ ...choices,
108
+ ]
109
+ : Object.keys(choices);
110
+ }
111
+ /** @public Score levels, lowest first, in object form. */
112
+ export function scoreLevels(levels) {
113
+ return levels.map((l) => typeof l === 'string'
114
+ ? {
115
+ label: l,
116
+ }
117
+ : l);
118
+ }
119
+ /**
120
+ * @public Case keys a question routes to: its choices, `'true'`/`'false'`, or
121
+ * its level labels.
122
+ */
123
+ export function routeOutcomes(question) {
124
+ switch (question.type) {
125
+ case 'boolean':
126
+ return [
127
+ 'true',
128
+ 'false',
129
+ ];
130
+ case 'choice':
131
+ return choiceValues(question.choices);
132
+ case 'score':
133
+ return levelLabels(question.levels);
134
+ }
135
+ }
136
+ function levelLabels(levels) {
137
+ return scoreLevels(levels).map((l) => l.label);
138
+ }
139
+ function unique(values) {
140
+ return new Set(values).size === values.length;
141
+ }
142
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import type { LLMResponse } from './common.js';
2
+ import type { Context } from './context.js';
3
+ import type { ContextLayer } from './context-layer.js';
4
+ import type { DecideRequest, DecideResponse, DecisionRecord } from './decide.js';
5
+ import type { SteeringDecision } from './steering.js';
6
+ /**
7
+ * @public The judgment calls an agent harness makes for a step: the steering
8
+ * hooks around tool and model calls, and typed decisions. Part of
9
+ * `AgentHarnessContract`.
10
+ */
11
+ export interface HarnessJudgmentContract {
12
+ /** Answer typed questions with a decision model, or estimate with a chat model. See spec 32. */
13
+ decide(request: DecideRequest): Promise<DecideResponse>;
14
+ /** Run every layer's `onDecision` hook, in slot order. */
15
+ notifyDecision(layers: ContextLayer[], record: DecisionRecord, ctx: Context): Promise<void>;
16
+ beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context, callId?: string): Promise<SteeringDecision>;
17
+ afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<SteeringDecision>;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -3,14 +3,14 @@ import type { Channel, ChannelHandle, ExternalChannel } from './channel.js';
3
3
  import type { LLMResponse, ModelParams, ServerToolSpec } from './common.js';
4
4
  import type { Context, CwdState, RestoreContextOptions } from './context.js';
5
5
  import type { ContextCacheConfig, ContextCacheStore } from './context-cache.js';
6
- import type { ContextData, ContextLayer, ProjectionPolicy, StorageAdapter } from './context-layer.js';
6
+ import type { ContextData, ContextLayer, ProjectionPolicy, RecallOptions, StorageAdapter } from './context-layer.js';
7
7
  import type { DetachedHandle } from './detached.js';
8
8
  import type { FsAdapter } from './fs-adapter.js';
9
+ import type { HarnessJudgmentContract } from './harness-judgment.js';
9
10
  import type { HarnessResponse, StreamEvent, StreamingItem } from './harness-result.js';
10
11
  import type { ExecuteInput, Item, ItemSchemaExtensions } from './items.js';
11
12
  import type { Span, TraceExporter } from './observability.js';
12
13
  import type { ShellAdapter } from './shell-adapter.js';
13
- import type { SteeringDecision } from './steering.js';
14
14
  import type { Step } from './step.js';
15
15
  import type { SubprocessAdapter } from './subprocess-adapter.js';
16
16
  import type { Tool } from './tool.js';
@@ -40,6 +40,8 @@ export interface AgentConfig<TParams extends Record<string, unknown> = Record<st
40
40
  forceAtomicRecall?: boolean;
41
41
  /** Tuning for prompt-cache anchoring. See `ContextCacheConfig` for the defaults. */
42
42
  contextCache?: ContextCacheConfig;
43
+ /** Default model for `decide` steps. */
44
+ decisionModel?: string;
43
45
  }
44
46
  /** @public How a message submitted during an active turn is delivered to the model.
45
47
  *
@@ -170,7 +172,7 @@ export interface SessionScope {
170
172
  threadId?: string;
171
173
  }
172
174
  /** @public Type-level contract for the agent runtime. Used for type annotations throughout the interpreter, context layers, and context. Implemented by `AgentHarness`. */
173
- export interface AgentHarnessContract<TParams extends Record<string, unknown> = Record<string, unknown>> {
175
+ export interface AgentHarnessContract<TParams extends Record<string, unknown> = Record<string, unknown>> extends HarnessJudgmentContract {
174
176
  readonly config: AgentConfig<TParams>;
175
177
  /** Filesystem adapter for virtual or real filesystem access. */
176
178
  readonly fs: FsAdapter;
@@ -298,9 +300,9 @@ export interface AgentHarnessContract<TParams extends Record<string, unknown> =
298
300
  initLayers(layers: ContextLayer[], ctx: Context, storage: StorageAdapter): Promise<void>;
299
301
  recallLayers(layers: ContextLayer[], input: string, ctx: Context): Promise<RecallLayerOutput[]>;
300
302
  /** Recall only atomic layers (or all layers when the harness forces atomic recall). Blocks until complete. */
301
- recallLayersAtomic(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<RecallLayerOutput[]>;
303
+ recallLayersAtomic(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>, opts?: RecallOptions): Promise<RecallLayerOutput[]>;
302
304
  /** Recall eventual layers from cache (non-blocking); returns nothing when the harness forces atomic recall. */
303
- recallLayersEventual(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<RecallLayerOutput[]>;
305
+ recallLayersEventual(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>, opts?: RecallOptions): Promise<RecallLayerOutput[]>;
304
306
  /**
305
307
  * Compute the items that would be sent to the model on the next turn for a
306
308
  * session — its accumulated history plus the harness's context-layer recall
@@ -332,8 +334,6 @@ export interface AgentHarnessContract<TParams extends Record<string, unknown> =
332
334
  createSpan(name: string, parent: Span | null): Span;
333
335
  getLayerState<T>(executionId: string, layerId: string): T | undefined;
334
336
  setLayerState<T>(executionId: string, layerId: string, state: T): void;
335
- beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context, callId?: string): Promise<SteeringDecision>;
336
- afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<SteeringDecision>;
337
337
  /**
338
338
  * Project the history items through every layer's `projectHistory` hook in
339
339
  * slot order. Pure read-side projection — `itemLog` is never mutated.
@@ -4,6 +4,7 @@ import type { Channel } from './channel.js';
4
4
  import type { ModelParams, RetryPolicy, ServerToolSpec, StepMeta } from './common.js';
5
5
  import type { Context } from './context.js';
6
6
  import type { ContextConfig, ContextData, ContextLayer, ProjectionPolicy } from './context-layer.js';
7
+ import type { DecideAnswers, DecideQuestions } from './decide.js';
7
8
  import type { NoeticError } from './error.js';
8
9
  import type { OutputCodec } from './output-codec.js';
9
10
  import type { SubprocessAdapter } from './subprocess-adapter.js';
@@ -73,7 +74,7 @@ export interface SettleResult<O> {
73
74
  error?: NoeticError;
74
75
  }
75
76
  /** @public Discriminated union of all step kinds that can be composed into an execution tree. */
76
- export type Step<TContext = ContextData, I = unknown, O = unknown> = StepRunCode<TContext, I, O> | StepCallModel<TContext, I, O> | StepAcpAgent<TContext, I, O> | StepInvokeTool<TContext, I, O> | StepConditional<TContext, I, O> | StepInParallel<TContext, I, O> | StepSpawn<TContext, I, O> | StepWithContext<TContext, I, O> | StepLoop<TContext, I, O> | StepSchedule<TContext, I, O>;
77
+ export type Step<TContext = ContextData, I = unknown, O = unknown> = StepRunCode<TContext, I, O> | StepCallModel<TContext, I, O> | StepAcpAgent<TContext, I, O> | StepInvokeTool<TContext, I, O> | StepDecide<TContext, I, O> | StepConditional<TContext, I, O> | StepInParallel<TContext, I, O> | StepSpawn<TContext, I, O> | StepWithContext<TContext, I, O> | StepLoop<TContext, I, O> | StepSchedule<TContext, I, O>;
77
78
  /** @public A step that executes arbitrary async logic via a user-supplied function. */
78
79
  export interface StepRunCode<TContext = ContextData, I = unknown, O = unknown> {
79
80
  kind: 'runCode';
@@ -173,12 +174,63 @@ export interface StepInvokeTool<_TContext = ContextData, I = unknown, O = unknow
173
174
  tool: Tool<StandardSchemaV1<unknown, I>, StandardSchemaV1<unknown, O>>;
174
175
  args?: Partial<I>;
175
176
  }
177
+ /**
178
+ * A step that asks a model typed questions about the current context and returns
179
+ * typed answers with probabilities. It reads the conversation but never appends
180
+ * to it, and runs no steering hooks. See spec 32.
181
+ * @public
182
+ */
183
+ export interface StepDecide<TContext = ContextData, _I = unknown, O = unknown> {
184
+ kind: 'decide';
185
+ /** Phantom field carrying the answers type for inference. Never set at runtime. */
186
+ readonly _answers?: O;
187
+ id: string;
188
+ /** Decision model id. Falls back to `harness.config.decisionModel`. */
189
+ model?: Lazy<string, TContext>;
190
+ /** Shared framing for every question. */
191
+ instructions?: Lazy<string | undefined, TContext>;
192
+ questions: DecideQuestions;
193
+ /** Which part of the assembled view the model sees. */
194
+ view: DecideView;
195
+ /** Typed state read from context layers, sent as named state entries. */
196
+ context?: (ctx: Context<TContext>) => Record<string, unknown> | Promise<Record<string, unknown>>;
197
+ /** Budget for the decision's view. Defaults to `DECISION_PROJECTION`. */
198
+ projection?: ProjectionPolicy;
199
+ /** Runs averaged on the estimated path. Ignored on the native path. */
200
+ samples: number;
201
+ /** Controls framework event emission for this step. Defaults to `true`. */
202
+ emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
203
+ }
204
+ /**
205
+ * A decide step that keeps its literal question map, so `routeSwitch` can infer
206
+ * the questions, choices, and levels it routes by.
207
+ * @public
208
+ */
209
+ export type DecideStep<TContext, I, Q extends DecideQuestions> = StepDecide<TContext, I, DecideAnswers<Q>> & {
210
+ readonly questions: Q;
211
+ };
212
+ /**
213
+ * What a decide step sees: `full` = layer recall + history, `layers` = layer
214
+ * recall only, `none` = only the input, instructions, and context selector.
215
+ * @public
216
+ */
217
+ export type DecideView = 'full' | 'layers' | 'none';
176
218
  /** @public A step that dynamically selects and executes one of several possible sub-steps. */
177
219
  export interface StepConditional<TContext = ContextData, I = unknown, O = unknown> {
178
220
  kind: 'conditional';
179
221
  id: string;
180
- route: (input: I, ctx: Context<TContext>) => Step<TContext, I, O> | null | Promise<Step<TContext, I, O> | null>;
222
+ /**
223
+ * Picks the step to run. `run` executes a step through the interpreter, for
224
+ * routes that must run a step (such as a decision) to choose.
225
+ */
226
+ route: (input: I, ctx: Context<TContext>, run: ExecuteStepFn) => Step<TContext, I, O> | null | Promise<Step<TContext, I, O> | null>;
181
227
  _optimizable?: Step<TContext>[];
228
+ /**
229
+ * @internal Rebuild this step around new `_optimizable` children, for
230
+ * conditionals whose route closes over them (`routeSwitch`). The optimizer calls
231
+ * it so a mutated child is the one that runs.
232
+ */
233
+ _rebuild?: (children: Step<TContext>[]) => StepConditional<TContext, I, O>;
182
234
  }
183
235
  /** @public Union of inParallel step variants (`race`, `all`, `settle`) for concurrent path execution. */
184
236
  export type StepInParallel<TContext = ContextData, I = unknown, O = unknown> = StepInParallelRace<TContext, I, O> | StepInParallelAll<TContext, I, O> | StepInParallelSettle<TContext, I, O>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/types",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "description": "Foundational type contracts and primitives shared across the Noetic framework (items, LLM config, adapters, steering, errors).",
6
6
  "license": "Apache-2.0",
@@ -41,11 +41,11 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@openrouter/agent": "^0.6.0",
44
+ "@openrouter/agent": "^0.11.0",
45
45
  "bun-types": "latest"
46
46
  },
47
47
  "peerDependencies": {
48
- "@openrouter/agent": "^0.6.0",
48
+ "@openrouter/agent": "^0.11.0",
49
49
  "zod": "^4.0.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@openrouter/agent": "^0.6.0",
57
+ "@openrouter/agent": "^0.11.0",
58
58
  "@standard-schema/spec": "1.1.0",
59
59
  "@zed-industries/agent-client-protocol": "^0.4.5",
60
60
  "zod": "^4.0.0"