@noetic-tools/core 5.4.2 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/adapters/openrouter.js +1 -1
  2. package/dist/builders/control-flow-builders.d.ts +6 -4
  3. package/dist/builders/control-flow-builders.js +3 -2
  4. package/dist/builders/decide-builders.d.ts +86 -0
  5. package/dist/builders/decide-builders.js +230 -0
  6. package/dist/builders/dynamic-workflow.js +1 -0
  7. package/dist/builders/tool-builder.d.ts +5 -1
  8. package/dist/builders/tool-builder.js +2 -0
  9. package/dist/builders/workflow-hydrator.js +81 -2
  10. package/dist/conditions/conditions.d.ts +9 -3
  11. package/dist/conditions/conditions.js +84 -25
  12. package/dist/harness/agent-harness.d.ts +19 -4
  13. package/dist/harness/agent-harness.js +74 -9
  14. package/dist/harness/decide-call.d.ts +19 -0
  15. package/dist/harness/decide-call.js +483 -0
  16. package/dist/harness/deps/adapters.d.ts +1 -0
  17. package/dist/harness/deps/adapters.js +1 -0
  18. package/dist/harness/deps/context.d.ts +1 -1
  19. package/dist/harness/deps/context.js +1 -1
  20. package/dist/harness/deps/types.d.ts +1 -1
  21. package/dist/harness/model-call.js +3 -1
  22. package/dist/index.d.ts +12 -4
  23. package/dist/index.js +5 -1
  24. package/dist/interpreter/action-deps.d.ts +1 -1
  25. package/dist/interpreter/action-deps.js +1 -1
  26. package/dist/interpreter/action-types.d.ts +1 -1
  27. package/dist/interpreter/collect-tools.js +2 -0
  28. package/dist/interpreter/execute-acp-agent.js +1 -1
  29. package/dist/interpreter/execute-action.js +46 -21
  30. package/dist/interpreter/execute-control.js +1 -1
  31. package/dist/interpreter/execute-decide.d.ts +10 -0
  32. package/dist/interpreter/execute-decide.js +417 -0
  33. package/dist/interpreter/execute.js +8 -3
  34. package/dist/observability/genai-attributes.d.ts +6 -0
  35. package/dist/observability/genai-attributes.js +6 -0
  36. package/dist/portable/context-layers.d.ts +1 -1
  37. package/dist/portable/context-layers.js +1 -1
  38. package/dist/runtime/session-runner.js +4 -0
  39. package/dist/schemas/workflow.d.ts +1 -1
  40. package/dist/util/status-code.d.ts +2 -0
  41. package/dist/util/status-code.js +7 -0
  42. package/package.json +7 -6
  43. package/schema/noetic-workflow.schema.json +396 -0
@@ -1,5 +1,5 @@
1
1
  import { storageGetMany } from '@noetic-tools/context';
2
- import { createMessage, extractAssistantText, trackUsage } from '@noetic-tools/types';
2
+ import { DecideOutputError, isNoeticError, NoeticErrorImpl, trackUsage } from '@noetic-tools/types';
3
3
  import { z } from 'zod';
4
4
  import { cosineSimilarity } from './cosine-similarity.js';
5
5
  //#endregion
@@ -234,41 +234,100 @@ export function allCondition(...conditions) {
234
234
  }
235
235
  //#endregion
236
236
  //#region aiCondition
237
- const AiConditionResponseSchema = z.object({
238
- answer: z.boolean(),
239
- });
240
237
  /**
241
- * Creates a condition that uses an LLM to classify the input as true or false.
238
+ * Creates a condition that asks a model a yes/no question about the input — a
239
+ * one-question `decide`. Works with decision models natively and with chat
240
+ * models through estimated probabilities.
242
241
  *
243
242
  * @public
244
- * @param opts - Configuration with model identifier and classification prompt.
245
- * @returns A `Condition` that delegates boolean classification to the model.
243
+ * @param opts.model - Decision or chat model id.
244
+ * @param opts.prompt - The yes/no question.
245
+ * @param opts.threshold - Probability of yes at or above which the condition holds. Default 0.5.
246
+ * @returns A `Condition` that is true when the model answers yes. A refusal counts as no.
247
+ * @throws `NoeticError` `model_parse_error` when the response is not a boolean answer.
246
248
  */
247
249
  export function aiCondition(opts) {
248
250
  return async (input, ctx) => {
249
- const text = serializeInput(input);
250
- const systemPrompt = `You are a boolean classifier. Given the user's input, answer the following question with JSON: {"answer": true} or {"answer": false}.\n\nQuestion: ${opts.prompt}`;
251
- const items = [
252
- createMessage(systemPrompt, 'developer'),
253
- createMessage(text, 'user'),
254
- ];
255
- const response = await ctx.harness.callModel({
251
+ const response = await askDecide(ctx, {
256
252
  model: opts.model,
257
- items,
258
- params: {
259
- temperature: 0,
253
+ state: [
254
+ {
255
+ type: 'text',
256
+ name: 'input',
257
+ text: serializeInput(input),
258
+ },
259
+ ],
260
+ questions: {
261
+ answer: {
262
+ type: 'boolean',
263
+ instructions: opts.prompt,
264
+ threshold: opts.threshold,
265
+ },
260
266
  },
261
267
  });
262
- trackUsage(ctx, response);
263
- const responseText = extractAssistantText(response.items);
264
- try {
265
- const parsed = JSON.parse(responseText);
266
- const validated = AiConditionResponseSchema.parse(parsed);
267
- return validated.answer;
268
- }
269
- catch {
268
+ trackUsage(ctx, {
269
+ items: [],
270
+ usage: response.usage,
271
+ cost: response.cost,
272
+ });
273
+ const answer = response.answers.answer;
274
+ if (answer?.type === 'refusal') {
270
275
  return false;
271
276
  }
277
+ const parsed = AiConditionAnswerSchema.safeParse(answer);
278
+ if (!parsed.success) {
279
+ throw new NoeticErrorImpl({
280
+ kind: 'model_parse_error',
281
+ stepId: 'aiCondition',
282
+ raw: JSON.stringify(answer),
283
+ schema: AiConditionAnswerSchema,
284
+ zodError: parsed.error,
285
+ });
286
+ }
287
+ return parsed.data.value;
272
288
  };
273
289
  }
290
+ async function askDecide(ctx, request) {
291
+ try {
292
+ return await ctx.harness.decide(request);
293
+ }
294
+ catch (e) {
295
+ if (isNoeticError(e)) {
296
+ throw e;
297
+ }
298
+ if (ctx.aborted) {
299
+ throw new NoeticErrorImpl({
300
+ kind: 'cancelled',
301
+ reason: ctx.abortReason ?? 'context aborted',
302
+ });
303
+ }
304
+ if (e instanceof DecideOutputError) {
305
+ if (e.usage) {
306
+ trackUsage(ctx, {
307
+ items: [],
308
+ usage: e.usage,
309
+ cost: e.cost,
310
+ });
311
+ }
312
+ throw new NoeticErrorImpl({
313
+ kind: 'model_parse_error',
314
+ stepId: 'aiCondition',
315
+ raw: e.raw,
316
+ schema: e.schema,
317
+ zodError: e.zodError,
318
+ });
319
+ }
320
+ throw new NoeticErrorImpl({
321
+ kind: 'step_failed',
322
+ stepId: 'aiCondition',
323
+ cause: e instanceof Error ? e : new Error(String(e)),
324
+ retriesExhausted: false,
325
+ });
326
+ }
327
+ }
328
+ const AiConditionAnswerSchema = z.object({
329
+ type: z.literal('boolean'),
330
+ value: z.boolean(),
331
+ probability: z.number().min(0).max(1),
332
+ });
274
333
  //#endregion
@@ -3,7 +3,7 @@ import { AcpSessionStore } from '../runtime/acp-session-store.js';
3
3
  import type { LayerStateStore, RecallCache } from './deps/context.js';
4
4
  import type { DetachedSpawnOverrides } from './deps/interpreter.js';
5
5
  import type { CheckpointStore, EventBroadcaster, RestoreCheckpointOptions, StepLedgerRetention, StepLedgerStore } from './deps/runtime.js';
6
- import type { AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
6
+ import type { AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DecideRequest, DecideResponse, DecisionRecord, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, RecallOptions, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
7
7
  export { createStreamIdleWatchdog } from './model-call.js';
8
8
  /**
9
9
  * Storage-scoped environment configuration for `AgentHarness`.
@@ -87,8 +87,12 @@ interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<stri
87
87
  * those produced by spawn/fork. Defaults to `process.cwd()`.
88
88
  */
89
89
  initialCwd?: string;
90
+ /** Default model for `decide` steps. */
91
+ decisionModel?: string;
90
92
  /** @internal Test-only escape hatch to inject a mock callModel implementation. */
91
93
  _testCallModel?: (request: CallModelRequest) => Promise<LLMResponse>;
94
+ /** @internal Test-only escape hatch to inject a mock decide implementation. */
95
+ _testDecide?: (request: DecideRequest) => Promise<DecideResponse>;
92
96
  }
93
97
  /** Env vars consulted when resolving the LLM client (read defensively so the
94
98
  * harness also works in runtimes without a `process` global, e.g. the browser). */
@@ -154,6 +158,11 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
154
158
  * without friend-class gymnastics. Do not access from outside core. */
155
159
  readonly _contextLayers?: ContextLayer[];
156
160
  private readonly client?;
161
+ /** SDK 1.x client for the decisions endpoint; the agent's client predates it. */
162
+ private readonly decisionsClient?;
163
+ private readonly decideOverride?;
164
+ /** Model id → whether it answers natively as a decision model. */
165
+ private readonly decisionModels;
157
166
  private readonly channelStore;
158
167
  /** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
159
168
  private readonly rootRunDepth;
@@ -207,6 +216,8 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
207
216
  * `ctx.id` (the layer-state store's executionId).
208
217
  */
209
218
  private readonly initializedExecutions;
219
+ /** Executions restored from a checkpoint: their first layer init keeps the restored state. */
220
+ private readonly restoredExecutions;
210
221
  readonly traceExporter: TraceExporter;
211
222
  /**
212
223
  * Long-lived shared cwd state. The same reference is seeded into every
@@ -242,6 +253,8 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
242
253
  private getOrCreateSession;
243
254
  private requireSession;
244
255
  callModel(request: CallModelRequest): Promise<LLMResponse>;
256
+ decide(request: DecideRequest): Promise<DecideResponse>;
257
+ notifyDecision(layers: ContextLayer[], record: DecisionRecord, ctx: Context): Promise<void>;
245
258
  run<I, O>(s: Step<ContextData, I, O>, input: I, ctx: Context): Promise<O>;
246
259
  /** Run all layer `init` hooks before the first step executes. Per spec 11,
247
260
  * init MUST complete before any recall fires so layer state is populated. */
@@ -271,6 +284,8 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
271
284
  threadId?: string;
272
285
  resourceId?: string;
273
286
  contextLayers?: ContextLayer[];
287
+ /** Additional layers appended AFTER the effective set — never replaces it. */
288
+ extraContextLayers?: ContextLayer[];
274
289
  /**
275
290
  * Initial cwd for the new context. When set, takes precedence over both
276
291
  * the parent snapshot and the harness root cwd — used by worktree
@@ -293,8 +308,8 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
293
308
  recallLayers(layers: ContextLayer[], input: string, ctx: Context): Promise<RecallLayerOutput[]>;
294
309
  /** Allocate per-layer recall budgets from the harness projection policy. */
295
310
  private layerBudgets;
296
- recallLayersAtomic(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<RecallLayerOutput[]>;
297
- recallLayersEventual(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>): Promise<RecallLayerOutput[]>;
311
+ recallLayersAtomic(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>, opts?: RecallOptions): Promise<RecallLayerOutput[]>;
312
+ recallLayersEventual(layers: ContextLayer[], input: string, ctx: Context, budgets: Map<string, number>, opts?: RecallOptions): Promise<RecallLayerOutput[]>;
298
313
  /**
299
314
  * Compute the items array that would be sent to the model on the next turn —
300
315
  * the same arrangement `executeLLM` builds: harness-level context layers'
@@ -393,7 +408,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
393
408
  createSpan(name: string, parent: Span | null): Span;
394
409
  getLayerState<T>(executionId: string, layerId: string): T | undefined;
395
410
  setLayerState<T>(executionId: string, layerId: string, state: T): void;
396
- beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context): Promise<SteeringDecision>;
411
+ beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context, callId?: string): Promise<SteeringDecision>;
397
412
  afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<SteeringDecision>;
398
413
  projectHistory(layers: ContextLayer[], items: ReadonlyArray<Item>, ctx: Context): Promise<ReadonlyArray<Item>>;
399
414
  runAppendPipeline(layers: ContextLayer[], items: Item[], ctx: Context): Promise<{
@@ -2,8 +2,9 @@ import { frameworkCast, ItemSchemaRegistry, NoeticConfigError } from '@noetic-to
2
2
  import { SpanImpl } from '../observability/span-impl.js';
3
3
  import { NoopExporter } from '../observability/trace-exporter.js';
4
4
  import { AcpSessionStore } from '../runtime/acp-session-store.js';
5
- import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, } from './deps/adapters.js';
6
- import { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from './deps/context.js';
5
+ import { callDecide } from './decide-call.js';
6
+ import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, OpenRouterSdk, } from './deps/adapters.js';
7
+ import { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, notifyDecision, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from './deps/context.js';
7
8
  import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, } from './deps/interpreter.js';
8
9
  import { buildItemStream, ChannelStore, ContextImpl, captureCheckpoint, clearCheckpoint, collectContextTree, createInMemoryStorage, createStepLedgerStore, filterReasoningStream, filterTextStream, resolveStepLedgerRetention, restoreFromCheckpoint, SessionRunner, StepLedger, snapshotCwdState, } from './deps/runtime.js';
9
10
  import { SteeringAction } from './deps/types.js';
@@ -144,6 +145,16 @@ function createClient(config, contextCache) {
144
145
  },
145
146
  });
146
147
  }
148
+ function createDecisionsClient(config) {
149
+ const resolved = resolveLlmClient(config, readLlmEnv());
150
+ if (!resolved) {
151
+ return undefined;
152
+ }
153
+ return new OpenRouterSdk({
154
+ apiKey: resolved.apiKey,
155
+ serverURL: resolved.serverURL,
156
+ });
157
+ }
147
158
  //#region AgentHarness
148
159
  /**
149
160
  * Default agent harness for executing agent steps with built-in channel, context, and trace support.
@@ -186,6 +197,11 @@ export class AgentHarness {
186
197
  * without friend-class gymnastics. Do not access from outside core. */
187
198
  _contextLayers;
188
199
  client;
200
+ /** SDK 1.x client for the decisions endpoint; the agent's client predates it. */
201
+ decisionsClient;
202
+ decideOverride;
203
+ /** Model id → whether it answers natively as a decision model. */
204
+ decisionModels = new Map();
189
205
  channelStore;
190
206
  /** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
191
207
  rootRunDepth = new Map();
@@ -252,6 +268,8 @@ export class AgentHarness {
252
268
  * `ctx.id` (the layer-state store's executionId).
253
269
  */
254
270
  initializedExecutions = new Set();
271
+ /** Executions restored from a checkpoint: their first layer init keeps the restored state. */
272
+ restoredExecutions = new Set();
255
273
  traceExporter;
256
274
  /**
257
275
  * Long-lived shared cwd state. The same reference is seeded into every
@@ -276,6 +294,7 @@ export class AgentHarness {
276
294
  projection: opts.projection,
277
295
  forceAtomicRecall: opts.forceAtomicRecall,
278
296
  contextCache: opts.contextCache,
297
+ decisionModel: opts.decisionModel,
279
298
  };
280
299
  this.fs = environment?.fs ?? createInMemoryFsAdapter();
281
300
  this.shell = environment?.shell ?? createInMemoryShellAdapter();
@@ -294,6 +313,10 @@ export class AgentHarness {
294
313
  this.client = opts._testCallModel
295
314
  ? undefined
296
315
  : createClient(opts.callModelDefaults, opts.contextCache);
316
+ this.decideOverride = opts._testDecide;
317
+ this.decisionsClient = opts._testCallModel
318
+ ? undefined
319
+ : createDecisionsClient(opts.callModelDefaults);
297
320
  this.channelStore = new ChannelStore();
298
321
  this.traceExporter = opts.traceExporter ?? new NoopExporter();
299
322
  this.layerStateStore =
@@ -439,6 +462,7 @@ export class AgentHarness {
439
462
  resourceId: perTurnOptions.resourceId,
440
463
  state: perTurnOptions.state,
441
464
  contextLayers: perTurnOptions.contextLayers,
465
+ extraContextLayers: perTurnOptions.extraContextLayers,
442
466
  _broadcaster: session.runner.broadcaster,
443
467
  });
444
468
  const ext = frameworkCast(ctx);
@@ -509,6 +533,27 @@ export class AgentHarness {
509
533
  }).callModel(request);
510
534
  }
511
535
  //#endregion
536
+ //#region decide
537
+ async decide(request) {
538
+ if (this.decideOverride) {
539
+ return this.decideOverride(request);
540
+ }
541
+ return callDecide(request, {
542
+ client: this.decisionsClient,
543
+ callModel: (r) => this.callModel(r),
544
+ nativeModels: this.decisionModels,
545
+ });
546
+ }
547
+ async notifyDecision(layers, record, ctx) {
548
+ await notifyDecision({
549
+ layers,
550
+ record,
551
+ ctx: this.toExecCtx(ctx),
552
+ store: this.layerStateStore,
553
+ recallCache: this.recallCache,
554
+ });
555
+ }
556
+ //#endregion
512
557
  async run(s, input, ctx) {
513
558
  // Populate the unified tool pool when an embedder drives a step directly on
514
559
  // a bare `createContext()` context (the turn pipeline does this per turn).
@@ -583,7 +628,14 @@ export class AgentHarness {
583
628
  return;
584
629
  }
585
630
  this.initializedExecutions.add(ctx.id);
586
- await this.initLayers(layers, ctx, storage);
631
+ const keep = this.restoredExecutions.delete(ctx.id);
632
+ await initLayers({
633
+ layers,
634
+ ctx: this.toExecCtx(ctx),
635
+ storage,
636
+ store: this.layerStateStore,
637
+ keep,
638
+ });
587
639
  }
588
640
  detachedSpawn(s, input, parentCtx, overrides) {
589
641
  /* NOT a checkpoint boundary, despite spec 23 listing one here: a DetachedHandle's
@@ -596,8 +648,14 @@ export class AgentHarness {
596
648
  const resolved = opts ?? {};
597
649
  // `ContextImpl` takes the layers as `layers`, so the option spelling is
598
650
  // dropped from `rest` rather than spread through under the wrong name.
599
- const { contextLayers, cwdInit, ...rest } = resolved;
600
- const effectiveLayers = contextLayers ?? this._contextLayers;
651
+ const { contextLayers, extraContextLayers, cwdInit, ...rest } = resolved;
652
+ const baseLayers = contextLayers ?? this._contextLayers;
653
+ const effectiveLayers = extraContextLayers && extraContextLayers.length > 0
654
+ ? [
655
+ ...(baseLayers ?? []),
656
+ ...extraContextLayers,
657
+ ]
658
+ : baseLayers;
601
659
  const itemSchemas = buildItemSchemaRegistry({
602
660
  base: this.itemSchemas,
603
661
  layers: effectiveLayers,
@@ -700,7 +758,7 @@ export class AgentHarness {
700
758
  a.allocated,
701
759
  ]));
702
760
  }
703
- async recallLayersAtomic(layers, input, ctx, budgets) {
761
+ async recallLayersAtomic(layers, input, ctx, budgets, opts) {
704
762
  return recallLayersAtomic({
705
763
  layers,
706
764
  query: input,
@@ -710,9 +768,10 @@ export class AgentHarness {
710
768
  store: this.layerStateStore,
711
769
  itemSchemas: this.itemSchemas,
712
770
  forceAtomic: this.config.forceAtomicRecall,
771
+ peek: opts?.peek,
713
772
  });
714
773
  }
715
- async recallLayersEventual(layers, input, ctx, budgets) {
774
+ async recallLayersEventual(layers, input, ctx, budgets, opts) {
716
775
  return recallLayersEventual({
717
776
  layers,
718
777
  query: input,
@@ -723,6 +782,7 @@ export class AgentHarness {
723
782
  itemSchemas: this.itemSchemas,
724
783
  forceAtomic: this.config.forceAtomicRecall,
725
784
  cache: this.recallCache,
785
+ peek: opts?.peek,
726
786
  });
727
787
  }
728
788
  /**
@@ -861,7 +921,11 @@ export class AgentHarness {
861
921
  * whose `id` is already the original `executionId`.
862
922
  */
863
923
  async restore(executionId, opts) {
864
- return restoreFromCheckpoint(this, executionId, opts);
924
+ const ctx = await restoreFromCheckpoint(this, executionId, opts);
925
+ if (ctx) {
926
+ this.restoredExecutions.add(ctx.id);
927
+ }
928
+ return ctx;
865
929
  }
866
930
  /**
867
931
  * Discard everything `restore(executionId)` would have used — the snapshot *and* the
@@ -948,7 +1012,7 @@ export class AgentHarness {
948
1012
  setLayerState(executionId, layerId, state) {
949
1013
  this.layerStateStore.set(executionId, layerId, state);
950
1014
  }
951
- async beforeToolCall(layers, toolName, toolArgs, ctx) {
1015
+ async beforeToolCall(layers, toolName, toolArgs, ctx, callId) {
952
1016
  const hasHook = layers.some((l) => l.hooks.beforeToolCall);
953
1017
  if (!hasHook) {
954
1018
  return {
@@ -959,6 +1023,7 @@ export class AgentHarness {
959
1023
  layers,
960
1024
  toolName,
961
1025
  toolArgs,
1026
+ callId,
962
1027
  ctx: this.toExecCtx(ctx),
963
1028
  store: this.layerStateStore,
964
1029
  });
@@ -0,0 +1,19 @@
1
+ import type { CallModelRequest, DecideRequest, DecideResponse, LLMResponse } from '@noetic-tools/types';
2
+ import type { OpenRouter } from '@openrouter/sdk';
3
+ /** @internal The slice of the SDK client the decide caller uses. */
4
+ export type DecisionsClient = Pick<OpenRouter, 'alpha' | 'models'>;
5
+ /** @internal */
6
+ export interface DecideCallerDeps {
7
+ client?: DecisionsClient;
8
+ callModel: (request: CallModelRequest) => Promise<LLMResponse>;
9
+ /** Per-harness cache: model id → whether it is a decision model (`undefined` while unknown). */
10
+ nativeModels: Map<string, Promise<boolean | undefined>>;
11
+ }
12
+ /**
13
+ * Answer a decide request. Decision models (those whose OpenRouter output
14
+ * modality is `decisions`) answer natively with their own distributions; any
15
+ * other model answers through a structured-output call that estimates them.
16
+ *
17
+ * @internal
18
+ */
19
+ export declare function callDecide(request: DecideRequest, deps: DecideCallerDeps): Promise<DecideResponse>;