@nifrajs/testing 4.0.2 → 5.0.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/src/trajectory.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  runAgent,
10
10
  } from "@nifrajs/agent"
11
11
  import type { StandardSchemaV1 } from "@nifrajs/core/schema"
12
- import { createToolBudget, type ToolBudget, type ToolContract } from "@nifrajs/core/tool-contract"
12
+ import type { ToolBudget } from "@nifrajs/core/tool-contract"
13
13
  import { createFailureLab, type FailureEvidence } from "./failure-lab.ts"
14
14
  import type { FaultProfile, FaultProfileFault, FaultProfileFaultKind } from "./fault-profile.ts"
15
15
 
@@ -169,7 +169,32 @@ export async function replayTrajectory<
169
169
  return lab.provider("trajectory.model", () => response)
170
170
  },
171
171
  }
172
- const tools = definition.tools.map((tool) => wrapTool(tool, lab, () => takeFault("tool")))
172
+ // Tools run dry, so a tool's own execute never runs: its faults fire at the budget check every
173
+ // tool call still makes. Each replayed tool carries its own cost object, which is how that check
174
+ // tells a tool's charge from the model's.
175
+ const toolCosts = new Map<object, string>()
176
+ const tools = definition.tools.map((tool) => {
177
+ const cost = { ...tool.cost }
178
+ toolCosts.set(cost, tool.name)
179
+ return Object.freeze({ ...tool, cost })
180
+ })
181
+ const callerBudget = ports.budget
182
+ const budget: ToolBudget = {
183
+ consume: (cost) => {
184
+ const tool = cost === undefined ? undefined : toolCosts.get(cost)
185
+ if (tool !== undefined) {
186
+ lab.checkpoint(`trajectory.tool.${tool}`)
187
+ const fault = takeFault("tool")
188
+ if (fault?.kind === "budget-pressure") return false
189
+ if (fault?.kind === "tool-error") throw new Error("trajectory tool fault")
190
+ // The call then stops as cancelled, and so does the run, as a cancelled model call does.
191
+ if (fault?.kind === "cancellation") controller.abort()
192
+ else lab.checkpoint(`trajectory.effect.${tool}`)
193
+ }
194
+ return callerBudget?.consume(cost) ?? true
195
+ },
196
+ snapshot: () => callerBudget?.snapshot() ?? {},
197
+ }
173
198
  const replayDefinition: AgentDefinition<InputSchema, OutputSchema> = { ...definition, tools }
174
199
  let effectiveApproval = ports.approval
175
200
  if (effectiveApproval !== undefined) {
@@ -182,17 +207,14 @@ export async function replayTrajectory<
182
207
  },
183
208
  }
184
209
  }
185
- const pressuredBudget: ToolBudget = createToolBudget({ limits: { calls: 0 } })
186
210
  const effectivePorts: AgentPorts = {
187
211
  ...ports,
188
212
  model,
189
213
  dryRun: true,
190
214
  signal: controller.signal,
215
+ budget,
191
216
  idFactory: () => `trajectory-${seed}-${++idSequence}`,
192
217
  ...(effectiveApproval === undefined ? {} : { approval: effectiveApproval }),
193
- ...(faults.some((fault) => fault.kind === "budget-pressure")
194
- ? { budget: pressuredBudget }
195
- : {}),
196
218
  }
197
219
  const result = await runAgent(replayDefinition, input, effectivePorts, {
198
220
  state: options.state,
@@ -284,28 +306,6 @@ export async function trajectoryRegressionId(
284
306
  return digest({ transcript: transcript.digest, faultProfile, invariant })
285
307
  }
286
308
 
287
- function wrapTool<Input, Output>(
288
- tool: ToolContract<Input, Output>,
289
- lab: ReturnType<typeof createFailureLab>,
290
- nextFault: () => FaultProfileFault | undefined,
291
- ): ToolContract<Input, Output> {
292
- return Object.freeze({
293
- ...tool,
294
- execute: async (
295
- input: Input,
296
- context: Parameters<ToolContract<Input, Output>["execute"]>[1],
297
- ) => {
298
- lab.checkpoint(`trajectory.tool.${tool.name}`)
299
- const fault = nextFault()
300
- if (fault?.kind === "tool-error" || fault?.kind === "budget-pressure")
301
- throw new Error("trajectory tool fault")
302
- if (fault?.kind === "cancellation") throw new Error("trajectory cancellation")
303
- lab.checkpoint(`trajectory.effect.${tool.name}`)
304
- return tool.execute(input, context)
305
- },
306
- })
307
- }
308
-
309
309
  function validateAgentTranscript(value: AgentTranscript): void {
310
310
  if (
311
311
  value === null ||
package/src/zod.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * carries no `.jsonSchema`, so without a bridge the laboratory reports NO_WITNESS for every zod route and
5
5
  * emits only generic type-confusion mutations, and the mock server returns `{}` for zod responses.
6
6
  *
7
- * Since the vendor-sniffing default (`@nifrajs/mock`'s `autoReflectJsonSchema`), the laboratory and the
8
- * mock server apply this bridge AUTOMATICALLY when zod is installed - no wiring. This module remains the
7
+ * The laboratory and the mock server apply the same conversion AUTOMATICALLY through `@nifrajs/mock`'s
8
+ * `autoReflectJsonSchema`, which reads a zod schema's own converter - no wiring. This module remains the
9
9
  * EXPLICIT form: pass it when you want the conversion pinned regardless of the default's behavior, or as
10
10
  * the base of a custom hook that handles more vendors:
11
11
  *