jev-recipes 0.8.4 → 0.9.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.
@@ -4,7 +4,13 @@ import type { EvaluationRow } from './engine.js';
4
4
  import { buildReport } from './report.js';
5
5
  import type { EvaluationOptions, EvaluationPolicy } from './schema.js';
6
6
  declare const archiveSchema: z.ZodObject<{
7
- format: z.ZodLiteral<1>;
7
+ format: z.ZodLiteral<2>;
8
+ scoringRevision: z.ZodLiteral<2>;
9
+ sourceMode: z.ZodEnum<{
10
+ fixture: "fixture";
11
+ live: "live";
12
+ }>;
13
+ evaluatedAt: z.ZodISODateTime;
8
14
  runId: z.ZodString;
9
15
  createdAt: z.ZodISODateTime;
10
16
  mode: z.ZodEnum<{
@@ -125,6 +131,7 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
125
131
  } | undefined;
126
132
  };
127
133
  mode: "fixture" | "live" | "replay";
134
+ scoringRevision: 2;
128
135
  report: {
129
136
  contestedAccuracy: number | null;
130
137
  adversarialAccuracy: number | null;
@@ -310,6 +317,7 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
310
317
  } | undefined;
311
318
  };
312
319
  mode: "fixture" | "live" | "replay";
320
+ scoringRevision: 2;
313
321
  report: {
314
322
  contestedAccuracy: number | null;
315
323
  adversarialAccuracy: number | null;
@@ -7,8 +7,13 @@ import { datasetFingerprints, fingerprint, loadEvaluationRecipe, recipeFingerpri
7
7
  import { confidenceThresholds, evaluateCases } from './engine.js';
8
8
  import { buildReport } from './report.js';
9
9
  import { evaluationCaseSchema, evaluationOptionsSchema, evaluationPolicySchema, priceSchema, } from './schema.js';
10
- const archiveSchema = z.object({
11
- format: z.literal(1),
10
+ import { scoringRevision } from './comparison.js';
11
+ const archiveSchema = z
12
+ .object({
13
+ format: z.literal(2),
14
+ scoringRevision: z.literal(scoringRevision),
15
+ sourceMode: z.enum(['live', 'fixture']),
16
+ evaluatedAt: z.iso.datetime(),
12
17
  runId: z.string(),
13
18
  createdAt: z.iso.datetime(),
14
19
  mode: z.enum(['live', 'fixture', 'replay']),
@@ -53,6 +58,21 @@ const archiveSchema = z.object({
53
58
  thresholds: z.array(z.number()),
54
59
  sourceRun: z.string().optional(),
55
60
  report: z.record(z.string(), z.unknown()),
61
+ })
62
+ .superRefine((run, context) => {
63
+ if (run.mode !== 'replay' && (run.sourceMode !== run.mode || run.evaluatedAt !== run.createdAt))
64
+ context.addIssue({
65
+ code: 'custom',
66
+ message: 'Direct runs must retain their own response mode and date.',
67
+ });
68
+ for (const [rowIndex, row] of run.rows.entries())
69
+ for (const [exchangeIndex, exchange] of row.exchanges.entries())
70
+ if (Object.hasOwn(exchange, 'response') === Object.hasOwn(exchange, 'error'))
71
+ context.addIssue({
72
+ code: 'custom',
73
+ path: ['rows', rowIndex, 'exchanges', exchangeIndex],
74
+ message: 'A completed exchange must contain exactly one response or error.',
75
+ });
56
76
  });
57
77
  export async function evaluate(name, values, options = {}) {
58
78
  const settings = evaluationOptionsSchema.parse(options);
@@ -73,10 +93,14 @@ export async function evaluate(name, values, options = {}) {
73
93
  throw new Error('The frozen policy belongs to a different recipe version or requested model.');
74
94
  }
75
95
  const manifest = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
96
+ const createdAt = new Date().toISOString();
76
97
  const metadata = {
77
- format: 1,
98
+ format: 2,
99
+ scoringRevision,
100
+ sourceMode: options.mode ?? 'live',
101
+ evaluatedAt: createdAt,
78
102
  runId: randomUUID(),
79
- createdAt: new Date().toISOString(),
103
+ createdAt,
80
104
  mode: options.mode ?? 'live',
81
105
  packageVersion: manifest.version,
82
106
  recipe: recipe.id,
@@ -130,6 +154,8 @@ export async function replay(run, policy = run.policy, out) {
130
154
  const cases = validateCases(recipe, run.cases);
131
155
  const metadata = {
132
156
  ...run,
157
+ format: 2,
158
+ scoringRevision,
133
159
  runId: randomUUID(),
134
160
  createdAt: new Date().toISOString(),
135
161
  mode: 'replay',
@@ -158,8 +184,10 @@ export async function readRun(directory) {
158
184
  throw error;
159
185
  return gunzipSync(await readFile(join(directory, 'run.json.gz'))).toString('utf8');
160
186
  });
161
- const parsed = archiveSchema.parse(JSON.parse(content));
162
- const run = parsed;
187
+ const raw = JSON.parse(content);
188
+ if (raw.format !== 2)
189
+ throw new Error('Unsupported archive format. This evaluator reads format 2; historical files remain available for direct inspection.');
190
+ const run = archiveSchema.parse(raw);
163
191
  const hashes = datasetFingerprints(run.cases);
164
192
  if (Object.entries(hashes).some(([key, hash]) => run[key] !== hash))
165
193
  throw new Error('Archive dataset fingerprints do not match its cases.');
@@ -196,7 +224,10 @@ function reportEvidence(run) {
196
224
  }
197
225
  return {
198
226
  runId: run.runId,
199
- evaluatedAt: run.createdAt,
227
+ sourceMode: run.sourceMode,
228
+ evaluatedAt: run.evaluatedAt,
229
+ createdAt: run.createdAt,
230
+ scoringRevision: run.scoringRevision,
200
231
  packageVersion: run.packageVersion,
201
232
  recipeFingerprint: run.recipeFingerprint,
202
233
  datasetFingerprint: run.datasetFingerprint,
@@ -207,6 +238,8 @@ function reportEvidence(run) {
207
238
  };
208
239
  }
209
240
  export function compare(baseline, candidate) {
241
+ if (baseline.scoringRevision !== candidate.scoringRevision)
242
+ throw new Error('Comparison requires the same scoring revision. Replay compatible archives before comparing.');
210
243
  if (baseline.recipe !== candidate.recipe ||
211
244
  baseline.inputFingerprint !== candidate.inputFingerprint ||
212
245
  baseline.answerKeyFingerprint !== candidate.answerKeyFingerprint)
@@ -220,6 +253,7 @@ export function compare(baseline, candidate) {
220
253
  models: run.report.models,
221
254
  policy: run.policy,
222
255
  mode: run.mode,
256
+ scoringRevision: run.scoringRevision,
223
257
  report: run.report,
224
258
  });
225
259
  return {
@@ -0,0 +1,8 @@
1
+ export declare const scoringRevision: 2;
2
+ type Schema = Record<string, unknown>;
3
+ export declare function resultSchemaFor(recipe: string): Schema;
4
+ export declare function comparableDecision(value: unknown, recipe: string): unknown;
5
+ export declare function matchesExpected(actual: unknown, expected: unknown, recipe: string): boolean;
6
+ export declare function isMetadataPath(schema: Schema, segments: string[]): boolean;
7
+ export declare function schemaAtPath(schema: Schema, segments: string[]): Schema | undefined;
8
+ export {};
@@ -0,0 +1,83 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { describeRecipe } from '../catalog/index.js';
3
+ import { recipeNameSchema } from '../catalog/schema.js';
4
+ import { isRecord } from './decisions.js';
5
+ export const scoringRevision = 2;
6
+ const metadataFields = new Set([
7
+ 'model',
8
+ 'usage',
9
+ 'confidence',
10
+ 'probability',
11
+ 'probabilities',
12
+ 'score',
13
+ 'status',
14
+ ]);
15
+ const resultSchemas = new Map();
16
+ export function resultSchemaFor(recipe) {
17
+ const cached = resultSchemas.get(recipe);
18
+ if (cached)
19
+ return cached;
20
+ const schema = describeRecipe(recipeNameSchema.parse(recipe)).resultSchema;
21
+ resultSchemas.set(recipe, schema);
22
+ return schema;
23
+ }
24
+ // Expected keys are result paths. Below an unconstrained payload, every key is data.
25
+ export function comparableDecision(value, recipe) {
26
+ if (!isRecord(value))
27
+ return value;
28
+ const schema = resultSchemaFor(recipe);
29
+ return Object.fromEntries(Object.entries(value)
30
+ .filter(([path]) => !isMetadataPath(schema, path.split('.')))
31
+ .map(([path, field]) => [path, normalize(field, schemaAtPath(schema, path.split('.')))]));
32
+ }
33
+ export function matchesExpected(actual, expected, recipe) {
34
+ return isDeepStrictEqual(comparableDecision(actual, recipe), comparableDecision(expected, recipe));
35
+ }
36
+ function normalize(value, schema) {
37
+ if (!schema)
38
+ return value;
39
+ if (Array.isArray(value))
40
+ return value.map((item, index) => normalize(item, schemaAtPath(schema, [String(index)])));
41
+ if (!isRecord(value))
42
+ return value;
43
+ return Object.fromEntries(Object.entries(value)
44
+ .filter(([name]) => !isMetadataPath(schema, [name]))
45
+ .map(([name, field]) => [name, normalize(field, schemaAtPath(schema, [name]))]));
46
+ }
47
+ export function isMetadataPath(schema, segments) {
48
+ if (!segments.length)
49
+ return false;
50
+ if (Array.isArray(schema.anyOf))
51
+ return schema.anyOf.some((branch) => isRecord(branch) && isMetadataPath(branch, segments));
52
+ const [first, ...rest] = segments;
53
+ if (isRecord(schema.properties) &&
54
+ Object.hasOwn(schema.properties, first) &&
55
+ metadataFields.has(first))
56
+ return true;
57
+ const child = schemaAtPath(schema, [first]);
58
+ return child !== undefined && isMetadataPath(child, rest);
59
+ }
60
+ export function schemaAtPath(schema, segments) {
61
+ if (!segments.length)
62
+ return schema;
63
+ if (Array.isArray(schema.anyOf)) {
64
+ const candidates = schema.anyOf.flatMap((branch) => {
65
+ const field = isRecord(branch) ? schemaAtPath(branch, segments) : undefined;
66
+ return field ? [field] : [];
67
+ });
68
+ return candidates.length ? { anyOf: candidates } : undefined;
69
+ }
70
+ const [first, ...rest] = segments;
71
+ if (isRecord(schema.properties) &&
72
+ Object.hasOwn(schema.properties, first) &&
73
+ isRecord(schema.properties[first]))
74
+ return schemaAtPath(schema.properties[first], rest);
75
+ if (schema.type === 'array' && /^(0|[1-9]\d*)$/.test(first) && isRecord(schema.items))
76
+ return schemaAtPath(schema.items, rest);
77
+ if (isRecord(schema.additionalProperties))
78
+ return schemaAtPath(schema.additionalProperties, rest);
79
+ if (schema.additionalProperties === true ||
80
+ (schema.type === undefined && schema.enum === undefined && schema.const === undefined))
81
+ return {};
82
+ return undefined;
83
+ }
@@ -1,9 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { z } from 'zod';
4
- import { describeRecipe } from '../catalog/index.js';
5
4
  import { recipeNameSchema } from '../catalog/schema.js';
6
- import { isRecord, comparableDecision } from './decisions.js';
5
+ import { isRecord } from './decisions.js';
6
+ import { comparableDecision, isMetadataPath, schemaAtPath, resultSchemaFor } from './comparison.js';
7
7
  import { evaluationCaseSchema } from './schema.js';
8
8
  export async function loadEvaluationRecipe(name) {
9
9
  const id = recipeNameSchema.parse(name);
@@ -30,7 +30,7 @@ export async function readEvaluationCases(path) {
30
30
  export function validateCases(recipe, values) {
31
31
  if (!values.length)
32
32
  throw new Error('The case file is empty.');
33
- const resultSchema = describeRecipe(recipeNameSchema.parse(recipe.id)).resultSchema;
33
+ const resultSchema = resultSchemaFor(recipe.id);
34
34
  const cases = values.map((value) => evaluationCaseSchema.parse(value));
35
35
  const ids = new Set();
36
36
  const familySplits = new Map();
@@ -45,17 +45,7 @@ export function validateCases(recipe, values) {
45
45
  }
46
46
  recipe.inputSchema.parse(entry.input);
47
47
  for (const [path, expected] of Object.entries(entry.expected)) {
48
- if (path
49
- .split('.')
50
- .some((segment) => [
51
- 'status',
52
- 'confidence',
53
- 'probabilities',
54
- 'probability',
55
- 'model',
56
- 'usage',
57
- 'score',
58
- ].includes(segment)))
48
+ if (isMetadataPath(resultSchema, path.split('.')))
59
49
  throw new Error(`${entry.id}: expected must name an ungated decision, not ${path}.`);
60
50
  const field = schemaAtPath(resultSchema, path.split('.'));
61
51
  if (!field)
@@ -63,30 +53,11 @@ export function validateCases(recipe, values) {
63
53
  if (!acceptsValue(field, expected))
64
54
  throw new Error(`${entry.id}: expected ${path} does not match the result schema.`);
65
55
  }
66
- if (isEmptyDecision(comparableDecision(entry.expected)))
56
+ if (isEmptyDecision(comparableDecision(entry.expected, recipe.id)))
67
57
  throw new Error(`${entry.id}: expected must contain a decision to compare.`);
68
58
  }
69
59
  return cases;
70
60
  }
71
- function schemaAtPath(schema, segments) {
72
- if (!segments.length)
73
- return schema;
74
- if (Array.isArray(schema.anyOf)) {
75
- const candidates = schema.anyOf.flatMap((branch) => {
76
- const field = isRecord(branch) ? schemaAtPath(branch, segments) : undefined;
77
- return field ? [field] : [];
78
- });
79
- return candidates.length ? { anyOf: candidates } : undefined;
80
- }
81
- const [first, ...rest] = segments;
82
- if (isRecord(schema.properties) && isRecord(schema.properties[first]))
83
- return schemaAtPath(schema.properties[first], rest);
84
- if (schema.type === 'array' && /^(0|[1-9]\d*)$/.test(first) && isRecord(schema.items))
85
- return schemaAtPath(schema.items, rest);
86
- if (isRecord(schema.additionalProperties))
87
- return schemaAtPath(schema.additionalProperties, rest);
88
- return undefined;
89
- }
90
61
  function acceptsValue(schema, value) {
91
62
  if (Array.isArray(schema.anyOf))
92
63
  return schema.anyOf.some((candidate) => isRecord(candidate) && acceptsValue(candidate, value));
@@ -1,5 +1,3 @@
1
- export declare function comparableDecision(value: unknown): unknown;
2
- export declare function matchesExpected(actual: unknown, expected: unknown): boolean;
3
1
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
4
2
  export declare function valueAtPath(record: unknown, path: string): unknown;
5
3
  export declare function pickPaths(record: unknown, paths: string[]): Record<string, unknown>;
@@ -1,25 +1,3 @@
1
- import { isDeepStrictEqual } from 'node:util';
2
- const metadataFields = new Set([
3
- 'model',
4
- 'usage',
5
- 'confidence',
6
- 'probability',
7
- 'probabilities',
8
- 'score',
9
- 'status',
10
- ]);
11
- export function comparableDecision(value) {
12
- if (Array.isArray(value))
13
- return value.map(comparableDecision);
14
- if (isRecord(value))
15
- return Object.fromEntries(Object.entries(value)
16
- .filter(([name]) => !metadataFields.has(name))
17
- .map(([name, field]) => [name, comparableDecision(field)]));
18
- return value;
19
- }
20
- export function matchesExpected(actual, expected) {
21
- return isDeepStrictEqual(comparableDecision(actual), comparableDecision(expected));
22
- }
23
1
  export function isRecord(value) {
24
2
  return value !== null && typeof value === 'object' && !Array.isArray(value);
25
3
  }
@@ -1,6 +1,7 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  import { createClient } from '../src/client.js';
3
- import { caseConfidence, isReady, mapWithConcurrencyLimit, matchesExpected, pickPaths, } from './decisions.js';
3
+ import { caseConfidence, isReady, mapWithConcurrencyLimit, pickPaths } from './decisions.js';
4
+ import { matchesExpected } from './comparison.js';
4
5
  export const confidenceThresholds = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95];
5
6
  export async function evaluateCases(recipe, cases, options = {}) {
6
7
  let requests = 0;
@@ -50,7 +51,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
50
51
  try {
51
52
  row.result = await recipe.run(goldenCase.input, recipeOptions);
52
53
  row.actual = pickPaths(row.result, Object.keys(goldenCase.expected));
53
- row.correct = matchesExpected(row.actual, row.expected);
54
+ row.correct = matchesExpected(row.actual, row.expected, recipe.id);
54
55
  row.ready = isReady(row.result);
55
56
  row.confidence = caseConfidence(row.result, Object.keys(goldenCase.expected));
56
57
  const model = pickPaths(row.result, ['model']).model;
@@ -72,7 +73,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
72
73
  row.decisions.push({
73
74
  minConfidence,
74
75
  ready: isReady(result),
75
- correct: matchesExpected(pickPaths(result, Object.keys(row.expected)), row.expected),
76
+ correct: matchesExpected(pickPaths(result, Object.keys(row.expected)), row.expected, recipe.id),
76
77
  });
77
78
  }
78
79
  return row;
@@ -4,6 +4,9 @@ import type { EvaluationCase, EvaluationPolicy } from './schema.js';
4
4
  export interface ReportEvidence {
5
5
  runId: string;
6
6
  evaluatedAt: string;
7
+ createdAt: string;
8
+ sourceMode: 'live' | 'fixture';
9
+ scoringRevision: number;
7
10
  packageVersion: string;
8
11
  recipeFingerprint: string;
9
12
  datasetFingerprint: string;
@@ -1,4 +1,5 @@
1
- import { isRecord, matchesExpected, roundedTo } from './decisions.js';
1
+ import { isRecord, roundedTo } from './decisions.js';
2
+ import { matchesExpected } from './comparison.js';
2
3
  export function buildReport(id, rows, confidenceThresholds, price, evidence) {
3
4
  const completed = rows.filter((row) => row.error === undefined);
4
5
  const ready = completed.filter((row) => row.ready);
@@ -41,7 +42,7 @@ export function buildReport(id, rows, confidenceThresholds, price, evidence) {
41
42
  reviewRate: completed.length
42
43
  ? roundedTo(3, (completed.length - ready.length) / completed.length)
43
44
  : null,
44
- ...itemAccuracyOf(rows),
45
+ ...itemAccuracyOf(id, rows),
45
46
  contestedAccuracy: accuracyOf(rows.filter((row) => row.contested)),
46
47
  adversarialAccuracy: accuracyOf(rows.filter((row) => row.adversarial)),
47
48
  calibration: [0, 0.5, 0.6, 0.7, 0.8, 0.9].map((lower, index, edges) => {
@@ -90,14 +91,15 @@ export function buildReport(id, rows, confidenceThresholds, price, evidence) {
90
91
  function accuracyOf(rows) {
91
92
  return rows.length ? roundedTo(3, rows.filter((row) => row.correct).length / rows.length) : null;
92
93
  }
93
- function itemAccuracyOf(rows) {
94
+ function itemAccuracyOf(recipe, rows) {
94
95
  const paths = rows.flatMap((row) => Object.entries(row.expected).map(([path, expected]) => ({
96
+ path,
95
97
  expected,
96
98
  actual: row.error === undefined ? row.actual?.[path] : undefined,
97
99
  })));
98
100
  if (paths.length <= rows.length)
99
101
  return {};
100
- const correct = paths.filter((entry) => matchesExpected(entry.actual, entry.expected)).length;
102
+ const correct = paths.filter(({ path, actual, expected }) => matchesExpected({ [path]: actual }, { [path]: expected }, recipe)).length;
101
103
  return {
102
104
  items: paths.length,
103
105
  correctItems: correct,
package/dist/src/batch.js CHANGED
@@ -11,7 +11,20 @@ export async function evaluateInBatches(items, batchSize, evaluate) {
11
11
  const chunks = [];
12
12
  for (let offset = 0; offset < items.length; offset += batchSize)
13
13
  chunks.push({ chunk: items.slice(offset, offset + batchSize), offset });
14
- const evaluations = await Promise.all(chunks.map(({ chunk, offset }) => evaluate(chunk, offset)));
14
+ let firstFailure;
15
+ const settled = await Promise.allSettled(chunks.map(async ({ chunk, offset }) => {
16
+ try {
17
+ return await evaluate(chunk, offset);
18
+ }
19
+ catch (error) {
20
+ firstFailure ??= { error };
21
+ throw error;
22
+ }
23
+ }));
24
+ // Let request recorders finish before a caller archives this failed batch.
25
+ if (firstFailure)
26
+ throw firstFailure.error;
27
+ const evaluations = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
15
28
  return {
16
29
  items: evaluations.flatMap((evaluation) => evaluation.items),
17
30
  model: evaluations[0]?.model ?? 'none',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-recipes",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "Small, composable Jev decisions for retrieval, conversations, and agent workflows.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,14 @@
27
27
  "types": "./dist/evaluation/index.d.ts",
28
28
  "import": "./dist/evaluation/index.js"
29
29
  },
30
+ "./ai-sdk": {
31
+ "types": "./dist/adapters/ai-sdk/index.d.ts",
32
+ "import": "./dist/adapters/ai-sdk/index.js"
33
+ },
34
+ "./langchain": {
35
+ "types": "./dist/adapters/langchain/index.d.ts",
36
+ "import": "./dist/adapters/langchain/index.js"
37
+ },
30
38
  "./package.json": "./package.json",
31
39
  "./action-compare": {
32
40
  "types": "./dist/recipes/action-compare/index.d.ts",
@@ -1035,6 +1043,8 @@
1035
1043
  "dist/cli/*.js",
1036
1044
  "dist/evaluation/**/*.js",
1037
1045
  "dist/evaluation/**/*.d.ts",
1046
+ "dist/adapters/**/*.js",
1047
+ "dist/adapters/**/*.d.ts",
1038
1048
  "dist/cli/*.d.ts",
1039
1049
  "dist/recipes/*/demo.json",
1040
1050
  "README.md",
@@ -1054,6 +1064,7 @@
1054
1064
  "eval": "node --env-file-if-exists=.env evals/run.mjs",
1055
1065
  "eval:smoke": "node --env-file-if-exists=.env evals/smoke.mjs",
1056
1066
  "eval:validate": "node evals/validate.mjs",
1067
+ "eval:audit": "node scripts/audit-evidence.mjs",
1057
1068
  "jev-recipes": "node --env-file-if-exists=.env dist/cli/index.js",
1058
1069
  "ci": "node scripts/ci.mjs",
1059
1070
  "pack:check": "node scripts/pack-check.mjs",
@@ -1066,15 +1077,29 @@
1066
1077
  "test:coverage": "vitest run --coverage",
1067
1078
  "eval:featured": "node --env-file-if-exists=.env evals/featured.mjs",
1068
1079
  "site:build": "npm run generate:check && npm run build && node site/build.mjs",
1069
- "site:preview": "node site/preview.mjs"
1080
+ "site:preview": "node --env-file-if-exists=.env site/preview.mjs"
1070
1081
  },
1071
1082
  "dependencies": {
1072
1083
  "@typesafe-ai/sdk": "^0.6.0",
1073
1084
  "zod": "^4.6.5"
1074
1085
  },
1086
+ "peerDependencies": {
1087
+ "ai": ">=7.0.0",
1088
+ "@langchain/core": ">=1.0.0"
1089
+ },
1090
+ "peerDependenciesMeta": {
1091
+ "ai": {
1092
+ "optional": true
1093
+ },
1094
+ "@langchain/core": {
1095
+ "optional": true
1096
+ }
1097
+ },
1075
1098
  "devDependencies": {
1099
+ "@langchain/core": "^1.2.13",
1076
1100
  "@types/node": "^22.0.0",
1077
1101
  "@vitest/coverage-v8": "4.1.11",
1102
+ "ai": "^7.0.122",
1078
1103
  "prettier": "3.9.8",
1079
1104
  "rimraf": "6.1.3",
1080
1105
  "typescript": "~5.9.3",