@noetic-tools/types 3.0.0 → 3.2.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
@@ -10,6 +10,7 @@ export * from './schemas/workflow.js';
10
10
  export * from './types/channel.js';
11
11
  export * from './types/common.js';
12
12
  export * from './types/context.js';
13
+ export * from './types/context-cache.js';
13
14
  export * from './types/context-layer.js';
14
15
  export * from './types/context-parts/item-log.js';
15
16
  export * from './types/context-parts/layer-usage.js';
@@ -22,6 +23,7 @@ export * from './types/items.js';
22
23
  export * from './types/observability.js';
23
24
  export * from './types/output-codec.js';
24
25
  export * from './types/runtime.js';
26
+ export * from './types/schema.js';
25
27
  export * from './types/shell-adapter.js';
26
28
  export * from './types/steering.js';
27
29
  export * from './types/step.js';
@@ -29,5 +31,6 @@ export * from './types/sub-harness.js';
29
31
  export * from './types/subprocess-adapter.js';
30
32
  export * from './types/tool.js';
31
33
  export * from './types/tool-context.js';
34
+ export * from './util/content-hash.js';
32
35
  export * from './util/framework-cast.js';
33
36
  export * from './util/message-helpers.js';
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export * from './schemas/workflow.js';
10
10
  export * from './types/channel.js';
11
11
  export * from './types/common.js';
12
12
  export * from './types/context.js';
13
+ export * from './types/context-cache.js';
13
14
  export * from './types/context-layer.js';
14
15
  export * from './types/context-parts/item-log.js';
15
16
  export * from './types/context-parts/layer-usage.js';
@@ -22,6 +23,7 @@ export * from './types/items.js';
22
23
  export * from './types/observability.js';
23
24
  export * from './types/output-codec.js';
24
25
  export * from './types/runtime.js';
26
+ export * from './types/schema.js';
25
27
  export * from './types/shell-adapter.js';
26
28
  export * from './types/steering.js';
27
29
  export * from './types/step.js';
@@ -29,5 +31,6 @@ export * from './types/sub-harness.js';
29
31
  export * from './types/subprocess-adapter.js';
30
32
  export * from './types/tool.js';
31
33
  export * from './types/tool-context.js';
34
+ export * from './util/content-hash.js';
32
35
  export * from './util/framework-cast.js';
33
36
  export * from './util/message-helpers.js';
@@ -56,25 +56,41 @@ export interface TokenUsage {
56
56
  total: number;
57
57
  cached?: number;
58
58
  }
59
+ /**
60
+ * @public Token counts for a single model round.
61
+ *
62
+ * `cachedTokens` and `cacheWriteTokens` are `undefined` — not `0` — when the
63
+ * provider reports no prompt-cache figures at all. Callers that steer on cache
64
+ * behaviour must tell "nothing was cached" apart from "this provider doesn't
65
+ * say", so the distinction is preserved rather than defaulted away.
66
+ */
67
+ export interface RoundUsage {
68
+ inputTokens: number;
69
+ outputTokens: number;
70
+ /** Prompt tokens served from the provider's cache (a cache *read*). */
71
+ cachedTokens?: number;
72
+ /** Prompt tokens written into the provider's cache (a cache *write*). */
73
+ cacheWriteTokens?: number;
74
+ }
59
75
  /** @public Metadata captured from the most recent step execution. */
60
76
  export interface StepMeta {
61
77
  toolCalls?: FunctionCallItem[];
62
- usage?: {
63
- inputTokens: number;
64
- outputTokens: number;
65
- cachedTokens?: number;
66
- };
78
+ usage?: RoundUsage;
67
79
  cost?: number;
68
80
  responseItems?: ReadonlyArray<Item>;
69
81
  }
70
82
  /** @public Structured response returned by a model adapter after an LLM call. */
71
83
  export interface LLMResponse {
72
84
  items: Item[];
73
- usage: {
74
- inputTokens: number;
75
- outputTokens: number;
76
- cachedTokens?: number;
77
- };
85
+ /** Totals across every round of the call. */
86
+ usage: RoundUsage;
87
+ /**
88
+ * Per-round breakdown, oldest first. Rounds after the first replay the same
89
+ * assembled view plus appended tool traffic, so they hit the prompt cache
90
+ * regardless of whether the first round did — only `rounds[0]` answers
91
+ * "was my prefix cached?".
92
+ */
93
+ rounds?: ReadonlyArray<RoundUsage>;
78
94
  cost?: number;
79
95
  }
80
96
  /** @public Configuration for the LLM provider used by the agent harness. */
@@ -0,0 +1,84 @@
1
+ import type { Item } from './items.js';
2
+ /**
3
+ * @public Tuning for the prompt-cache anchoring described on {@link LayerPlacement}.
4
+ *
5
+ * The defaults are chosen so that a layer set with no explicit placements still
6
+ * gets a stable prefix without any configuration.
7
+ */
8
+ export interface ContextCacheConfig {
9
+ /** Master switch. Default `true`. When off, every layer renders before history, as it did before bands existed. */
10
+ enabled?: boolean;
11
+ /** Re-anchor when the first round reports fewer cached tokens than this. Default 100. */
12
+ minCachedTokens?: number;
13
+ /**
14
+ * Assemblies an epoch must reach before its cache figures are judged. Default 2.
15
+ * The first assembly after a re-anchor writes the cache rather than reading it,
16
+ * so judging it immediately would re-anchor forever and never pin anything.
17
+ */
18
+ minEpochAssemblies?: number;
19
+ /** Assemblies after which an epoch re-anchors regardless of cache figures. Default 50. */
20
+ maxEpochAssemblies?: number;
21
+ /** Re-anchor once supersedes cost more than this fraction of the anchor band. Default 0.15. */
22
+ deltaBudgetFraction?: number;
23
+ /** An `'auto'` layer changing at least this often moves to the live band. Default 0.5. */
24
+ autoDemoteChurn?: number;
25
+ /** An `'auto'` layer changing at most this often moves back to the anchor band. Default 0.2. */
26
+ autoPromoteChurn?: number;
27
+ /** Assemblies a layer must be watched for before its placement moves. Default 3. */
28
+ minChurnSamples?: number;
29
+ /** Fraction of the churn counters carried across a re-anchor. Default 0.5. */
30
+ churnDecay?: number;
31
+ }
32
+ /** @public Why an epoch re-anchored. */
33
+ export type ReanchorReason = 'cold-start' | 'instructions-changed' | 'cache-miss' | 'delta-pressure' | 'delta-overflow' | 'max-age';
34
+ /** @public One layer's rendered output, held byte-stable for the life of an epoch. */
35
+ export interface AnchorPin {
36
+ layerId: string;
37
+ /** The exact items served every assembly this epoch. */
38
+ items: Item[];
39
+ tokenCount: number;
40
+ contentHash: string;
41
+ /** The layer state as of pinning. Held by reference — see `RenderDeltaParams.prevState`. */
42
+ state: unknown;
43
+ slot: number;
44
+ }
45
+ /** @public How often a layer's rendered output changes, and what that costs. */
46
+ export interface LayerChurn {
47
+ /** Assemblies in which the layer produced output. */
48
+ observed: number;
49
+ /** Assemblies in which its output differed from its pin. */
50
+ changed: number;
51
+ /** Tokens invalidated downstream by those changes, had they not been pinned. */
52
+ rebillTokens: number;
53
+ }
54
+ /** @public One run of assemblies sharing a cacheable prompt prefix. */
55
+ export interface ContextEpoch {
56
+ id: string;
57
+ /** Pinned output, keyed as `${scopeKey}:${layerId}`. */
58
+ pins: Map<string, AnchorPin>;
59
+ /** Band chosen for each `'auto'` layer at the last boundary, same key as `pins`. */
60
+ autoBand: Map<string, 'anchor' | 'live'>;
61
+ /** Hash of the resolved instructions this epoch was anchored against. */
62
+ instructionsHash: string;
63
+ assemblies: number;
64
+ anchorTokens: number;
65
+ deltaTokens: number;
66
+ /**
67
+ * Set once the provider has shown it reports no cache figures, or has missed
68
+ * so persistently that steering on its numbers is pointless. Suppresses the
69
+ * cache-miss trigger; age and delta pressure still apply.
70
+ */
71
+ cacheBlind: boolean;
72
+ /** Consecutive judged assemblies that came back a miss. */
73
+ misses: number;
74
+ lastReanchorReason: ReanchorReason;
75
+ }
76
+ /** @public Per-harness home for anchoring state, one epoch per cache lineage. */
77
+ export interface ContextCacheStore {
78
+ /** Keyed by cache lineage — a thread at top level, an execution below it. */
79
+ epochs: Map<string, ContextEpoch>;
80
+ /** Churn per layer, outliving epochs so placement is not relearnt each time. */
81
+ churn: Map<string, LayerChurn>;
82
+ /** Re-anchors decided from a response, applied at the next assembly. */
83
+ pendingReanchor: Map<string, ReanchorReason>;
84
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //#endregion
@@ -179,6 +179,42 @@ export interface StoreParams<TState> {
179
179
  export interface StoreResult<TState> {
180
180
  state: TState;
181
181
  }
182
+ /**
183
+ * @public Which band of the assembled view a layer's recall output sits in.
184
+ *
185
+ * Prompt caches match on a prefix, so a layer that re-renders every turn
186
+ * invalidates everything after it. The bands let stable content sit ahead of
187
+ * conversation history where it can be cached, and volatile content sit after
188
+ * it where re-rendering is nearly free.
189
+ *
190
+ * - `'anchor'` — before history. Pinned for the epoch: the bytes sent on the
191
+ * first assembly are re-sent unchanged until the next re-anchor, and any
192
+ * change is published as a supersede instead of rewriting the prefix.
193
+ * - `'live'` — after history. Re-rendered freely. Use for content that changes
194
+ * every turn, or whose `recall()` mutates state and so cannot be replayed.
195
+ * - `'auto'` (default) — starts anchored; the runtime may move it to `'live'`
196
+ * at an epoch boundary once it has watched how often the layer changes.
197
+ * An explicit `'anchor'` or `'live'` is never overridden.
198
+ */
199
+ export type LayerPlacement = 'anchor' | 'live' | 'auto';
200
+ /** @public Parameters passed to a context layer's `renderDelta` hook when its pinned output goes stale. */
201
+ export interface RenderDeltaParams<TState = unknown> {
202
+ /** The items still pinned in the anchor band — what the model can currently see. */
203
+ prev: ReadonlyArray<Item>;
204
+ /** The freshly recalled items that would have replaced them. */
205
+ next: ReadonlyArray<Item>;
206
+ /**
207
+ * The layer state captured when `prev` was pinned. Best-effort: it is held by
208
+ * reference, so a layer that mutates its state in place sees the current
209
+ * object here rather than a snapshot.
210
+ */
211
+ prevState: TState | undefined;
212
+ /** The layer's current state. */
213
+ state: TState | undefined;
214
+ ctx: ExecutionContext;
215
+ /** Soft token budget for the returned text. */
216
+ budget: number;
217
+ }
182
218
  /** @public Parameters passed to a context layer's `onSpawn` hook when a child execution starts. */
183
219
  export interface SpawnParams<TState> {
184
220
  parentState: TState;
@@ -297,6 +333,16 @@ export interface ContextLayerHooks<TState = unknown> {
297
333
  * Use for: capping history, summarising old turns, redacting items.
298
334
  */
299
335
  projectHistory?(params: ProjectHistoryParams<TState>): Promise<ProjectHistoryResult>;
336
+ /**
337
+ * Called for an anchored layer whose pinned output has gone stale, to describe
338
+ * the change compactly instead of re-sending the whole block.
339
+ *
340
+ * Return `null` to fall back to the default, which republishes the full new
341
+ * content. Worth implementing only when the layer's output is large and its
342
+ * changes are small — a file set where one file changed, a ledger that gained
343
+ * a row. Never called for layers in the `'live'` band, which re-render anyway.
344
+ */
345
+ renderDelta?(params: RenderDeltaParams<TState>): Promise<string | null>;
300
346
  }
301
347
  /**
302
348
  * A composable context layer that participates in the recall/store lifecycle.
@@ -343,6 +389,11 @@ export interface ContextLayer<TState = unknown> {
343
389
  itemSchemas?: Pick<ItemSchemaExtensions, 'developerMessages' | 'items'>;
344
390
  /** Default re-render timing when `onItemAppend` requests a re-render. */
345
391
  rerenderTiming?: 'immediate' | 'batched';
392
+ /**
393
+ * Which band of the assembled view this layer's recall output sits in, and so
394
+ * whether it is pinned for the prompt cache. Defaults to `'auto'`.
395
+ */
396
+ placement?: LayerPlacement;
346
397
  }
347
398
  /** @public Configuration for how the runtime projects conversation items into the model's context window. */
348
399
  export interface ProjectionPolicy {
@@ -1,3 +1,4 @@
1
+ import type { ReanchorReason } from '../context-cache.js';
1
2
  import type { Item } from '../items.js';
2
3
  /** @public Per-layer contribution to the context window on the most recent LLM call. */
3
4
  export interface LayerUsageEntry {
@@ -5,6 +6,27 @@ export interface LayerUsageEntry {
5
6
  readonly tokenCount: number;
6
7
  /** Items this layer contributed to the context view for the last LLM call. */
7
8
  readonly items: ReadonlyArray<Item>;
9
+ /** Which band the layer rendered into. */
10
+ readonly placement: 'anchor' | 'live';
11
+ /** `'pinned'` when the items are a replay of an earlier render, held for the prompt cache. */
12
+ readonly served: 'fresh' | 'pinned';
13
+ /** Whether the layer's fresh output differed from its pin, and so was superseded. */
14
+ readonly changed: boolean;
15
+ /** Share of watched assemblies in which this layer's output changed, 0–1. */
16
+ readonly churnRate: number;
17
+ /** Tokens this layer's changes would have re-billed had it not been pinned. */
18
+ readonly rebillTokens: number;
19
+ }
20
+ /** @public Prompt-cache anchoring state behind the most recent assembly. */
21
+ export interface EpochUsage {
22
+ readonly id: string;
23
+ /** Assemblies served by this epoch, including the one just made. */
24
+ readonly age: number;
25
+ readonly anchorTokens: number;
26
+ readonly liveTokens: number;
27
+ readonly deltaTokens: number;
28
+ /** Set only on an assembly that re-anchored. */
29
+ readonly reanchorReason?: ReanchorReason;
8
30
  }
9
31
  /** @public Breakdown of the context window as of the most recent LLM call in an execution. */
10
32
  export interface LastLayerUsage {
@@ -15,4 +37,6 @@ export interface LastLayerUsage {
15
37
  readonly toolsTokens: number;
16
38
  readonly historyTokens: number;
17
39
  readonly totalUsedTokens: number;
40
+ /** Absent when context caching is switched off. */
41
+ readonly epoch?: EpochUsage;
18
42
  }
@@ -1,7 +1,8 @@
1
- import type { ZodType } from 'zod';
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import type { ItemSchemaRegistry } from '../schemas/item.js';
3
3
  import type { Channel, ChannelHandle, ExternalChannel } from './channel.js';
4
4
  import type { LLMResponse, ModelParams, StepMeta, TokenUsage } from './common.js';
5
+ import type { ContextCacheConfig, ContextCacheStore } from './context-cache.js';
5
6
  import type { ContextData, ContextLayer, ProjectionPolicy, StorageAdapter } from './context-layer.js';
6
7
  import type { ItemLog } from './context-parts/item-log.js';
7
8
  import type { LastLayerUsage } from './context-parts/layer-usage.js';
@@ -40,7 +41,9 @@ interface ContextCallModelRequestBase {
40
41
  items: ReadonlyArray<import('./items.js').Item>;
41
42
  instructions?: string;
42
43
  params?: ModelParams;
43
- outputSchema?: ZodType;
44
+ outputSchema?: StandardSchemaV1;
45
+ /** Explicit non-Zod override/fallback after StandardJSONSchemaV1 conversion. */
46
+ outputJsonSchema?: Record<string, unknown>;
44
47
  emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
45
48
  signal?: AbortSignal;
46
49
  }
@@ -111,7 +114,7 @@ export type ContextStep<TContext = ContextData, I = unknown, O = unknown> = {
111
114
  } | {
112
115
  readonly kind: 'llm';
113
116
  readonly id: string;
114
- readonly output?: ZodType<O>;
117
+ readonly output?: StandardSchemaV1<unknown, O>;
115
118
  } | {
116
119
  readonly kind: 'loop';
117
120
  readonly id: string;
@@ -128,6 +131,8 @@ export interface ContextHarness {
128
131
  readonly params: Record<string, unknown>;
129
132
  /** Harness-wide default projection policy; a step's `projection` overrides it. */
130
133
  readonly projection?: ProjectionPolicy;
134
+ /** Tuning for prompt-cache anchoring. */
135
+ readonly contextCache?: ContextCacheConfig;
131
136
  };
132
137
  readonly fs: FsAdapter;
133
138
  readonly shell: ShellAdapter;
@@ -159,6 +164,8 @@ export interface ContextHarness {
159
164
  beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context): Promise<SteeringDecision>;
160
165
  afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<SteeringDecision>;
161
166
  runAppendPipeline(layers: ContextLayer[], items: import('./items.js').Item[], ctx: Context): Promise<ContextAppendPipelineResult>;
167
+ /** Pinned anchor output and epoch bookkeeping for prompt-cache anchoring. Absent on a harness that predates it. */
168
+ readonly contextCache?: ContextCacheStore;
162
169
  recallLayers(layers: ContextLayer[], input: string, ctx: Context): Promise<ContextRecallLayerOutput[]>;
163
170
  recallLayersAtomic(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<ContextRecallLayerOutput[]>;
164
171
  recallLayersEventual(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<ContextRecallLayerOutput[]>;
@@ -1,4 +1,5 @@
1
- import type { ZodError, ZodType } from 'zod';
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { ZodError } from 'zod';
2
3
  /** @public Discriminated union of all structured error kinds raised by the runtime. */
3
4
  export type NoeticError = {
4
5
  kind: 'step_failed';
@@ -13,7 +14,8 @@ export type NoeticError = {
13
14
  kind: 'llm_parse_error';
14
15
  stepId: string;
15
16
  raw: string;
16
- schema: ZodType;
17
+ schema: StandardSchemaV1;
18
+ /** Real `ZodError` for Zod schemas; synthetic `custom` issues adapted from the vendor's Standard Schema issues otherwise. */
17
19
  zodError: ZodError;
18
20
  } | {
19
21
  kind: 'llm_rate_limit';
@@ -1,7 +1,8 @@
1
- import type { ZodType } from 'zod';
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  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
+ import type { ContextCacheConfig, ContextCacheStore } from './context-cache.js';
5
6
  import type { ContextData, ContextLayer, ProjectionPolicy, StorageAdapter } from './context-layer.js';
6
7
  import type { DetachedHandle } from './detached.js';
7
8
  import type { FsAdapter } from './fs-adapter.js';
@@ -32,6 +33,8 @@ export interface AgentConfig<TParams extends Record<string, unknown> = Record<st
32
33
  projection?: ProjectionPolicy;
33
34
  /** When true, every layer is recalled atomically regardless of its `recallMode` (no eventual/cached recall). */
34
35
  forceAtomicRecall?: boolean;
36
+ /** Tuning for prompt-cache anchoring. See `ContextCacheConfig` for the defaults. */
37
+ contextCache?: ContextCacheConfig;
35
38
  }
36
39
  /** @public How a message submitted during an active turn is delivered to the model.
37
40
  *
@@ -51,7 +54,12 @@ interface CallModelRequestBase {
51
54
  instructions?: string;
52
55
  params?: ModelParams;
53
56
  /** When provided, the harness sends a JSON Schema constraint to the model so it returns structured JSON. */
54
- outputSchema?: ZodType;
57
+ outputSchema?: StandardSchemaV1;
58
+ /**
59
+ * Explicit raw JSON Schema override for the structured-output constraint.
60
+ * For non-Zod schemas, it takes precedence over StandardJSONSchemaV1 conversion.
61
+ */
62
+ outputJsonSchema?: Record<string, unknown>;
55
63
  /** Controls framework event emission. Defaults to `true`. Passed through from `StepLLM.emit`. */
56
64
  emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
57
65
  /**
@@ -245,6 +253,13 @@ export interface AgentHarnessContract<TParams extends Record<string, unknown> =
245
253
  * iterable owns a single iterator; subscribe again for a second consumer.
246
254
  */
247
255
  getChannelStream<T>(channel: ExternalChannel<T>, executionId: string): AsyncIterable<T>;
256
+ /**
257
+ * Pinned anchor output and epoch bookkeeping for prompt-cache anchoring,
258
+ * shared by every thread on this harness. Optional so a hand-written harness
259
+ * stays valid: without it nothing is pinned across turns and layers render
260
+ * fresh every time, as they did before anchoring existed.
261
+ */
262
+ readonly contextCache?: ContextCacheStore;
248
263
  initLayers(layers: ContextLayer[], ctx: Context, storage: StorageAdapter): Promise<void>;
249
264
  recallLayers(layers: ContextLayer[], input: string, ctx: Context): Promise<RecallLayerOutput[]>;
250
265
  /** Recall only atomic layers (or all layers when the harness forces atomic recall). Blocks until complete. */
@@ -307,6 +322,13 @@ export interface RecallLayerOutput {
307
322
  layerId: string;
308
323
  items: Item[];
309
324
  tokenCount: number;
325
+ /**
326
+ * Set when the layer's `recall` returned new state, meaning the call changed
327
+ * something and cannot be replayed. Such a layer is never pinned — serving an
328
+ * older render would drop the very thing the call committed (the steering
329
+ * layer, for one, drains its queue as it renders).
330
+ */
331
+ mutatedState?: boolean;
310
332
  }
311
333
  /** @public A request to re-render the context window, collected from onItemAppend hooks. */
312
334
  export interface RerenderRequest {
@@ -0,0 +1,62 @@
1
+ import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { ZodTypeAny, z } from 'zod';
3
+ import { ZodError } from 'zod';
4
+ export type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
5
+ /**
6
+ * Input type inferred from any Standard Schema. Zod schemas keep their exact
7
+ * `z.input` inference; other vendors use `StandardSchemaV1.InferInput`.
8
+ * @public
9
+ */
10
+ export type InferSchemaInput<S extends StandardSchemaV1> = S extends ZodTypeAny ? z.input<S> : StandardSchemaV1.InferInput<S>;
11
+ /**
12
+ * Output type inferred from any Standard Schema. Zod schemas keep their exact
13
+ * `z.output` inference; other vendors use `StandardSchemaV1.InferOutput`.
14
+ * @public
15
+ */
16
+ export type InferSchemaOutput<S extends StandardSchemaV1> = S extends ZodTypeAny ? z.output<S> : StandardSchemaV1.InferOutput<S>;
17
+ /**
18
+ * JSON Schema configuration for a tool input schema. Zod and Standard JSON
19
+ * Schema implementations can derive their wire schema; validation-only
20
+ * Standard Schemas require an explicit fallback.
21
+ * @public
22
+ */
23
+ export type InputSchemaConfig<S extends StandardSchemaV1> = StandardSchemaV1 extends S ? {
24
+ inputJsonSchema?: Record<string, unknown>;
25
+ } : S extends ZodTypeAny | StandardJSONSchemaV1 ? {
26
+ inputJsonSchema?: Record<string, unknown>;
27
+ } : {
28
+ inputJsonSchema: Record<string, unknown>;
29
+ };
30
+ /** @public Type guard for Zod schemas (detected via the `_zod` internals marker). */
31
+ export declare function isZodSchema(schema: StandardSchemaV1): schema is ZodTypeAny;
32
+ /** @public Type guard for schemas implementing the Standard JSON Schema v1 companion spec. */
33
+ export declare function isStandardJsonSchema(schema: StandardSchemaV1): schema is StandardSchemaV1 & StandardJSONSchemaV1;
34
+ /** @public Successful result of `validateSchema` — carries the parsed/transformed value. */
35
+ export interface SchemaValidationSuccess<T> {
36
+ success: true;
37
+ value: T;
38
+ }
39
+ /**
40
+ * Failed result of `validateSchema`. `issues` are the vendor's issues in
41
+ * Standard Schema form; `zodError` adapts them into a `ZodError` (the
42
+ * original error for Zod schemas, synthetic `custom` issues otherwise) so
43
+ * consumers like `llm_parse_error` keep a single error surface.
44
+ * @public
45
+ */
46
+ export interface SchemaValidationFailure {
47
+ success: false;
48
+ issues: readonly StandardSchemaV1.Issue[];
49
+ zodError: ZodError;
50
+ }
51
+ /** @public Result of `validateSchema`. */
52
+ export type SchemaValidationResult<T> = SchemaValidationSuccess<T> | SchemaValidationFailure;
53
+ /** @public Adapt Standard Schema issues into a synthetic `ZodError` of `custom` issues. */
54
+ export declare function standardIssuesToZodError(issues: readonly StandardSchemaV1.Issue[]): ZodError;
55
+ /**
56
+ * Validates a value against any Standard Schema. Zod schemas take the
57
+ * `safeParse` fast path; other vendors run `schema['~standard'].validate`,
58
+ * awaiting sync or Promise results. The returned success value is the
59
+ * parsed/transformed output.
60
+ * @public
61
+ */
62
+ export declare function validateSchema<S extends StandardSchemaV1>(schema: S, value: unknown): Promise<SchemaValidationResult<InferSchemaOutput<S>>>;
@@ -0,0 +1,63 @@
1
+ import { ZodError } from 'zod';
2
+ import { frameworkCast } from '../util/framework-cast.js';
3
+ /** @public Type guard for Zod schemas (detected via the `_zod` internals marker). */
4
+ export function isZodSchema(schema) {
5
+ return '_zod' in schema;
6
+ }
7
+ /** @public Type guard for schemas implementing the Standard JSON Schema v1 companion spec. */
8
+ export function isStandardJsonSchema(schema) {
9
+ const standard = frameworkCast(schema['~standard']);
10
+ return typeof standard.jsonSchema?.input === 'function';
11
+ }
12
+ function issuePathToPropertyKeys(path) {
13
+ if (!path) {
14
+ return [];
15
+ }
16
+ return path.map((segment) => typeof segment === 'object' && segment !== null && 'key' in segment ? segment.key : segment);
17
+ }
18
+ /** @public Adapt Standard Schema issues into a synthetic `ZodError` of `custom` issues. */
19
+ export function standardIssuesToZodError(issues) {
20
+ return new ZodError(issues.map((issue) => ({
21
+ code: 'custom',
22
+ message: issue.message,
23
+ path: issuePathToPropertyKeys(issue.path),
24
+ })));
25
+ }
26
+ /**
27
+ * Validates a value against any Standard Schema. Zod schemas take the
28
+ * `safeParse` fast path; other vendors run `schema['~standard'].validate`,
29
+ * awaiting sync or Promise results. The returned success value is the
30
+ * parsed/transformed output.
31
+ * @public
32
+ */
33
+ export async function validateSchema(schema, value) {
34
+ if (isZodSchema(schema)) {
35
+ const result = schema.safeParse(value);
36
+ if (result.success) {
37
+ return {
38
+ success: true,
39
+ value: frameworkCast(result.data),
40
+ };
41
+ }
42
+ return {
43
+ success: false,
44
+ issues: result.error.issues.map((issue) => ({
45
+ message: issue.message,
46
+ path: issue.path,
47
+ })),
48
+ zodError: result.error,
49
+ };
50
+ }
51
+ const result = await schema['~standard'].validate(value);
52
+ if (result.issues) {
53
+ return {
54
+ success: false,
55
+ issues: result.issues,
56
+ zodError: standardIssuesToZodError(result.issues),
57
+ };
58
+ }
59
+ return {
60
+ success: true,
61
+ value: frameworkCast(result.value),
62
+ };
63
+ }
@@ -1,4 +1,4 @@
1
- import type { ZodType } from 'zod';
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import type { Channel } from './channel.js';
3
3
  import type { ModelParams, RetryPolicy, ServerToolSpec, StepMeta } from './common.js';
4
4
  import type { Context } from './context.js';
@@ -104,11 +104,17 @@ export interface StepLLM<TContext = ContextData, _I = unknown, O = unknown> {
104
104
  */
105
105
  tools?: Lazy<(Tool | ServerToolSpec)[] | undefined, TContext>;
106
106
  /**
107
- * Structured output: a Zod schema (assistant text is JSON-parsed and
107
+ * Structured output: a Standard Schema (assistant text is JSON-parsed and
108
108
  * validated) or a streaming `OutputCodec` (fed each text delta, produces
109
- * the typed value at turn end).
109
+ * the typed value at turn end). Non-Zod schemas can derive the model
110
+ * constraint through StandardJSONSchemaV1 or provide `outputJsonSchema`.
110
111
  */
111
- output?: ZodType<O> | OutputCodec<O>;
112
+ output?: StandardSchemaV1<unknown, O> | OutputCodec<O>;
113
+ /**
114
+ * Explicit raw JSON Schema override sent to the model. For non-Zod schemas,
115
+ * it takes precedence over StandardJSONSchemaV1 conversion.
116
+ */
117
+ outputJsonSchema?: Record<string, unknown>;
112
118
  params?: ModelParams;
113
119
  /** Controls framework event emission for this step. Defaults to `true`. Set `false` to suppress all framework events. A filter function receives `(eventType, data)` and returns `boolean`. */
114
120
  emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
@@ -138,8 +144,8 @@ export interface StepSubHarness<TContext = ContextData, _I = unknown, O = unknow
138
144
  settings?: SubHarnessSettings;
139
145
  /** System instructions applied on the first message of a fresh session. */
140
146
  instructions?: Lazy<string | undefined, TContext>;
141
- /** When set, the assistant text is JSON-parsed and validated against this schema. */
142
- output?: ZodType<O>;
147
+ /** When set, the assistant text is JSON-parsed and validated against this Standard Schema. */
148
+ output?: StandardSchemaV1<unknown, O>;
143
149
  /** Session reuse + teardown policy across steps. */
144
150
  session?: SubHarnessSessionPolicy;
145
151
  /** Controls framework event emission for this step. Defaults to `true`. */
@@ -149,7 +155,7 @@ export interface StepSubHarness<TContext = ContextData, _I = unknown, O = unknow
149
155
  export interface StepTool<_TContext = ContextData, I = unknown, O = unknown> {
150
156
  kind: 'tool';
151
157
  id: string;
152
- tool: Tool<ZodType<I>, ZodType<O>>;
158
+ tool: Tool<StandardSchemaV1<unknown, I>, StandardSchemaV1<unknown, O>>;
153
159
  args?: Partial<I>;
154
160
  }
155
161
  /** @public A step that dynamically selects and executes one of several possible sub-steps. */
@@ -1,6 +1,7 @@
1
- import type { ZodTypeAny, z } from 'zod';
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import type { FunctionCallItem, FunctionCallOutputItem, Item, ItemSchemaExtensions, ToolResultExtensionItem } from './items.js';
3
- type ToolExecutionResult<O extends ZodTypeAny> = Promise<z.infer<O>> | AsyncGenerator<unknown, z.infer<O>>;
3
+ import type { InferSchemaOutput } from './schema.js';
4
+ type ToolExecutionResult<O extends StandardSchemaV1> = Promise<InferSchemaOutput<O>> | AsyncGenerator<unknown, InferSchemaOutput<O>>;
4
5
  /**
5
6
  * Declares tool-owned state that the runtime materializes into a ContextLayer.
6
7
  * @public
@@ -39,44 +40,55 @@ export interface UiFragment {
39
40
  * `tool()` builder and runtime consume.
40
41
  * @public
41
42
  */
42
- export interface ToolUiDeclaration<I extends ZodTypeAny = ZodTypeAny, O extends ZodTypeAny = ZodTypeAny, E = unknown> {
43
+ export interface ToolUiDeclaration<I extends StandardSchemaV1 = StandardSchemaV1, O extends StandardSchemaV1 = StandardSchemaV1, E = unknown> {
43
44
  /** Rendered as soon as the call streams in — args may be partial. */
44
- call?(args: Partial<z.infer<I>>): UiFragment | null;
45
+ call?(args: Partial<InferSchemaOutput<I>>): UiFragment | null;
45
46
  /** Re-rendered on each event an AsyncGenerator `execute` yields. Receives all events so far. */
46
47
  progress?(events: E[]): UiFragment | null;
47
48
  /** Replaces the tool's region on successful completion. */
48
- result?(output: z.infer<O>, args: z.infer<I>): UiFragment | null;
49
+ result?(output: InferSchemaOutput<O>, args: InferSchemaOutput<I>): UiFragment | null;
49
50
  /** Replaces the tool's region when execution throws. */
50
- error?(err: unknown, args: z.infer<I>): UiFragment | null;
51
+ error?(err: unknown, args: InferSchemaOutput<I>): UiFragment | null;
51
52
  }
52
53
  /**
53
54
  * A tool definition that an LLM can invoke during execution.
54
55
  *
56
+ * Input and output schemas accept any Standard Schema v1 implementation
57
+ * (Zod, Valibot, ArkType, …). Zod remains the fast path; other validators
58
+ * can provide JSON Schema through the Standard JSON Schema v1 companion
59
+ * trait or an explicit `inputJsonSchema` override.
60
+ *
55
61
  * The runtime passes a `ToolExecutionContext` (from `./tool-context`) as
56
62
  * the second argument to `execute`. Callers that need the concrete type
57
63
  * should import `Tool` from the package root (`@noetic-tools/core`), which
58
64
  * re-exports it with `ToolExecutionContext` substituted in.
59
65
  * @public
60
66
  */
61
- export interface Tool<I extends ZodTypeAny = ZodTypeAny, O extends ZodTypeAny = ZodTypeAny> {
67
+ export interface Tool<I extends StandardSchemaV1 = StandardSchemaV1, O extends StandardSchemaV1 = StandardSchemaV1> {
62
68
  /** Unique tool name used by the LLM for selection. */
63
69
  name: string;
64
70
  /** Human-readable description shown to the LLM. */
65
71
  description: string;
66
- /** Zod schema validating tool input arguments. */
72
+ /** Schema validating tool input arguments (any Standard Schema v1). */
67
73
  input: I;
68
- /** Zod schema validating tool return value. */
74
+ /** Schema validating tool return value (any Standard Schema v1). */
69
75
  output: O;
70
- /** Optional Zod schema validating streaming events yielded during execution. */
71
- event?: ZodTypeAny;
76
+ /**
77
+ * Explicit raw JSON Schema for the tool input, sent to the LLM wire format.
78
+ * For non-Zod schemas, overrides StandardJSONSchemaV1 conversion and serves
79
+ * as the required fallback for validation-only schemas.
80
+ */
81
+ inputJsonSchema?: Record<string, unknown>;
82
+ /** Optional schema validating streaming events yielded during execution. */
83
+ event?: StandardSchemaV1;
72
84
  /** Optional item schemas contributed by this tool for tool call/result extensions. */
73
85
  itemSchemas?: Pick<ItemSchemaExtensions, 'toolCalls' | 'toolResults' | 'items'>;
74
86
  /** Decorate the harness-created tool result item before it is appended/emitted. */
75
87
  decorateResultItem?(params: {
76
88
  baseItem: FunctionCallOutputItem;
77
89
  callItem: FunctionCallItem;
78
- args: z.infer<I>;
79
- result: z.infer<O> | undefined;
90
+ args: InferSchemaOutput<I>;
91
+ result: InferSchemaOutput<O> | undefined;
80
92
  output: string;
81
93
  error?: boolean;
82
94
  }): Item | ToolResultExtensionItem;
@@ -86,7 +98,7 @@ export interface Tool<I extends ZodTypeAny = ZodTypeAny, O extends ZodTypeAny =
86
98
  * this type a dependency leaf. Use `tool()` from `builders/` or cast at
87
99
  * the call site to get a typed handle.
88
100
  */
89
- execute(args: z.infer<I>, toolCtx: unknown): ToolExecutionResult<O>;
101
+ execute(args: InferSchemaOutput<I>, toolCtx: unknown): ToolExecutionResult<O>;
90
102
  /** When true, execution pauses for human approval before running. */
91
103
  needsApproval?: boolean;
92
104
  /** Optional context declaration — the runtime generates a ContextLayer from this. */
@@ -0,0 +1,25 @@
1
+ import type { Item } from '../types/items.js';
2
+ /**
3
+ * @public Hash a string to 16 hex chars (64 bits).
4
+ *
5
+ * Not cryptographic — it exists to answer "is this byte-identical to last
6
+ * time?" cheaply. 64 bits keeps accidental collisions far enough away that a
7
+ * stale-content false match is not a practical concern.
8
+ *
9
+ * Adapted from bryc's cyrb64 (public domain):
10
+ * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb1.js
11
+ */
12
+ export declare function hashString(input: string): string;
13
+ /**
14
+ * @public Hash rendered items by the content that actually reaches the model.
15
+ *
16
+ * `createMessage` mints a fresh `id` on every render, so hashing items verbatim
17
+ * would report a change every turn even when the text is identical. Message
18
+ * items are sent as `{role, content}` alone, so `id` and `status` are dropped
19
+ * before hashing; every other item type is hashed whole, since their ids can
20
+ * reach the wire.
21
+ *
22
+ * Object keys are sorted, so a layer that builds its items in a different key
23
+ * order between renders still hashes the same.
24
+ */
25
+ export declare function hashItems(items: ReadonlyArray<Item>): string;
@@ -0,0 +1,65 @@
1
+ //#region Public API
2
+ /**
3
+ * @public Hash a string to 16 hex chars (64 bits).
4
+ *
5
+ * Not cryptographic — it exists to answer "is this byte-identical to last
6
+ * time?" cheaply. 64 bits keeps accidental collisions far enough away that a
7
+ * stale-content false match is not a practical concern.
8
+ *
9
+ * Adapted from bryc's cyrb64 (public domain):
10
+ * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb1.js
11
+ */
12
+ export function hashString(input) {
13
+ let h1 = 0xdeadbeef;
14
+ let h2 = 0x41c6ce57;
15
+ for (let i = 0; i < input.length; i++) {
16
+ const ch = input.charCodeAt(i);
17
+ h1 = Math.imul(h1 ^ ch, 2654435761);
18
+ h2 = Math.imul(h2 ^ ch, 1597334677);
19
+ }
20
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
21
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
22
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
23
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
24
+ return `${(h2 >>> 0).toString(16).padStart(8, '0')}${(h1 >>> 0).toString(16).padStart(8, '0')}`;
25
+ }
26
+ /**
27
+ * @public Hash rendered items by the content that actually reaches the model.
28
+ *
29
+ * `createMessage` mints a fresh `id` on every render, so hashing items verbatim
30
+ * would report a change every turn even when the text is identical. Message
31
+ * items are sent as `{role, content}` alone, so `id` and `status` are dropped
32
+ * before hashing; every other item type is hashed whole, since their ids can
33
+ * reach the wire.
34
+ *
35
+ * Object keys are sorted, so a layer that builds its items in a different key
36
+ * order between renders still hashes the same.
37
+ */
38
+ export function hashItems(items) {
39
+ return hashString(stableStringify(items.map(wireShape)));
40
+ }
41
+ //#endregion
42
+ //#region Helpers
43
+ /** Drops the fields the provider adapter strips before sending a message item. */
44
+ function wireShape(item) {
45
+ if (item.type !== 'message') {
46
+ return item;
47
+ }
48
+ const { id: _id, status: _status, ...rest } = item;
49
+ return rest;
50
+ }
51
+ /** `JSON.stringify` with object keys sorted at every depth. */
52
+ function stableStringify(value) {
53
+ if (value === null || typeof value !== 'object') {
54
+ return JSON.stringify(value) ?? 'null';
55
+ }
56
+ if (Array.isArray(value)) {
57
+ return `[${value.map(stableStringify).join(',')}]`;
58
+ }
59
+ const entries = Object.entries(value)
60
+ .filter(([, v]) => v !== undefined)
61
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
62
+ .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
63
+ return `{${entries.join(',')}}`;
64
+ }
65
+ //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/types",
3
- "version": "3.0.0",
3
+ "version": "3.2.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",
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@openrouter/agent": "^0.6.0",
58
+ "@standard-schema/spec": "1.1.0",
58
59
  "zod": "^4.0.0"
59
60
  },
60
61
  "scripts": {