@absolutejs/rag 0.18.1 → 0.19.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/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.19.0 — 2026-09-24
10
+
11
+ ### Added
12
+
13
+ - **Allow a separately configured review provider and model through the existing host accounting path.** (`ResearchConfig`)
14
+
15
+ ### Fixed
16
+
17
+ - **Require relevance, entity, time and scope checks before supporting research fields; carry task criteria into review, bound default findings and share evidence space with follow-up sources. Resolve model-selected passage IDs to exact source text instead of asking models to transcribe quotes.** (`bindResearchReview`, `ResearchField`, `createResearch`)
18
+
9
19
  ## 0.18.1 — 2026-09-24
10
20
 
11
21
  ### Fixed
package/RESEARCH.md CHANGED
@@ -266,3 +266,42 @@ const mcpTools = toMcpToolRegistry(researchManifest, {
266
266
  Use `evaluateResearch` to run representative cases with optional independent human review and total measured cost. Unreviewed acceptance, unknown cost, and cost per accepted finding without accepted findings remain `null`. Publication freshness measures source publication dates; it is not event freshness.
267
267
 
268
268
  Each result includes `groundingCase`, accepted by the existing `evaluateRAGAnswerGrounding({cases})` function from `@absolutejs/rag/quality`. Citation resolution metrics do not establish factual correctness or discovery recall. Keep approved ground truth and reviewer identity with comparisons, and hold task/model/budget policy constant when switching search providers.
269
+
270
+ ### Research quality and reviewer configuration
271
+
272
+ Default research returns at most six concise findings. Every supported field must
273
+ have source-bound quotes and positive model checks for task relevance, entity,
274
+ time and scope. Missing or failed checks withhold support even when a quote
275
+ matches. `bindResearchReview` callers must now supply `checks` to promote a field.
276
+ These checks are fallible model judgments, not human or independent verification.
277
+ Task instructions and schema are supplied to both extraction and review.
278
+ Internally, the reviewer selects numbered source passages and the runtime copies
279
+ their text verbatim into citations. Invented or ambiguous references fail closed;
280
+ this removes quote transcription errors without establishing factual entailment.
281
+
282
+ The evidence budget is shared across sources instead of consumed entirely by
283
+ first results. Fetched pages replace their snippets and receive four times the
284
+ allocation weight of snippet-only sources; short sources donate unused space.
285
+ This preserves more page context while reserving room for follow-up retrieval.
286
+ A maximum of 64 unique sources bounds retained evidence. Truncation can still
287
+ omit necessary context; inspect limitations and field verdicts.
288
+
289
+ A separate reviewer can be configured without changing planning or extraction:
290
+
291
+ ```ts
292
+ const research = createResearch({
293
+ search,
294
+ provider,
295
+ model: extractionModel,
296
+ reviewer: { provider: reviewProvider, model: reviewModel },
297
+ });
298
+ ```
299
+
300
+ Omitting `reviewer` uses the main provider/model. The host generation adapter and
301
+ admission/settlement path still apply to review calls. Price review reservations
302
+ and usage with the configured reviewer; do not apply the extraction model's rate
303
+ to a different model. A stronger reviewer does not repair missing retrieval.
304
+
305
+ An invalid planner schema no longer discards retrieved evidence: extraction and
306
+ review continue without retry, while the result remains partial and the failed
307
+ plan retains unknown settlement. Budget denial and cancellation still stop work.
package/changelog.json CHANGED
@@ -2,6 +2,28 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/rag",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "added",
9
+ "summary": "Allow a separately configured review provider and model through the existing host accounting path.",
10
+ "symbols": [
11
+ "ResearchConfig"
12
+ ]
13
+ },
14
+ {
15
+ "kind": "fixed",
16
+ "summary": "Require relevance, entity, time and scope checks before supporting research fields; carry task criteria into review, bound default findings and share evidence space with follow-up sources. Resolve model-selected passage IDs to exact source text instead of asking models to transcribe quotes.",
17
+ "symbols": [
18
+ "bindResearchReview",
19
+ "ResearchField",
20
+ "createResearch"
21
+ ]
22
+ }
23
+ ],
24
+ "date": "2026-09-24",
25
+ "version": "0.19.0"
26
+ },
5
27
  {
6
28
  "changes": [
7
29
  {
@@ -124141,8 +124141,16 @@ var ReviewSchema = Type2.Object({
124141
124141
  fields: Type2.Array(Type2.Object({
124142
124142
  path: Type2.String(),
124143
124143
  verdict: Type2.Union(["supported", "unsupported", "conflicting", "unknown"].map((value) => Type2.Literal(value))),
124144
- citations: Type2.Array(Type2.Object({ sourceId: Type2.String(), quote: Type2.String() }), { maxItems: 8 }),
124145
- reason: Type2.String()
124144
+ citations: Type2.Array(Type2.Object({ passageId: Type2.String() }), {
124145
+ maxItems: 8
124146
+ }),
124147
+ reason: Type2.String(),
124148
+ checks: Type2.Object({
124149
+ answersQuestion: Type2.Boolean(),
124150
+ correctEntity: Type2.Boolean(),
124151
+ correctTime: Type2.Boolean(),
124152
+ preservesScope: Type2.Boolean()
124153
+ })
124146
124154
  }), { maxItems: 256 })
124147
124155
  });
124148
124156
  var researchLeaves = (value, maxFields = 128) => {
@@ -124172,18 +124180,53 @@ var bindResearchReview = (leaves, reviews, sources) => leaves.map((leaf) => {
124172
124180
  return matching.length === 1 && sourceSupportsQuote(matching[0], citation.quote);
124173
124181
  }) ?? [];
124174
124182
  const valid = !!review && citations.length === review.citations.length;
124175
- const verdict = !valid || leaf.value === null || !citations.length ? "unknown" : review.verdict;
124183
+ const checksPass = review?.checks && [
124184
+ "answersQuestion",
124185
+ "correctEntity",
124186
+ "correctTime",
124187
+ "preservesScope"
124188
+ ].every((key2) => review.checks?.[key2] === true);
124189
+ const verdict = !valid || leaf.value === null || !citations.length ? "unknown" : review.verdict === "supported" && !checksPass ? "unknown" : review.verdict;
124176
124190
  return {
124177
124191
  ...leaf,
124178
124192
  verdict,
124179
124193
  citations,
124180
- reason: valid ? review.reason : "Missing, ambiguous, or invalid source-bound review"
124194
+ checks: review?.checks,
124195
+ reason: valid && review.verdict === "supported" && !checksPass ? "Task relevance, entity, time, or scope was not established: " + review.reason : valid ? review.reason : "Missing, ambiguous, or invalid source-bound review"
124181
124196
  };
124182
124197
  });
124198
+ var researchPassages = (sources) => sources.map((source) => {
124199
+ const passages = [];
124200
+ for (const excerpt of source.excerpts) {
124201
+ let start = 0;
124202
+ while (start < excerpt.length) {
124203
+ let end = Math.min(start + 600, excerpt.length);
124204
+ if (end < excerpt.length) {
124205
+ const boundary = excerpt.lastIndexOf(`
124206
+ `, end);
124207
+ if (boundary > start + 300)
124208
+ end = boundary;
124209
+ if (excerpt.length - end < 12)
124210
+ end = excerpt.length;
124211
+ }
124212
+ passages.push({
124213
+ id: `${source.id}:${passages.length}`,
124214
+ text: excerpt.slice(start, end)
124215
+ });
124216
+ start = end;
124217
+ }
124218
+ }
124219
+ const { excerpts: _excerpts, ...metadata } = source;
124220
+ return { ...metadata, passages };
124221
+ });
124222
+ var resolveResearchPassages = (citations, sources) => citations.map((citation) => {
124223
+ const matches2 = sources.flatMap((source) => source.passages.filter((passage) => passage.id === citation.passageId).map((passage) => ({ sourceId: source.id, quote: passage.text })));
124224
+ return matches2.length === 1 ? matches2[0] : { sourceId: "", quote: "" };
124225
+ });
124183
124226
 
124184
124227
  // src/research/runtime.ts
124185
124228
  var DefaultSchema = Type2.Object({
124186
- findings: Type2.Array(Type2.String(), { maxItems: 12 })
124229
+ findings: Type2.Array(Type2.String(), { maxItems: 6 })
124187
124230
  });
124188
124231
  var PlanSchema = Type2.Object({
124189
124232
  queries: Type2.Array(Type2.String({ maxLength: 600 }), { maxItems: 8 }),
@@ -124270,8 +124313,8 @@ var createResearch = (config) => {
124270
124313
  };
124271
124314
  const generate2 = async (kind, schema, prompt, payload) => {
124272
124315
  const generated = await operate(kind, () => (config.generateObject ?? generateObjectAI)({
124273
- provider: config.provider,
124274
- model: config.model,
124316
+ provider: kind === "review" ? config.reviewer?.provider ?? config.provider : config.provider,
124317
+ model: kind === "review" ? config.reviewer?.model ?? config.model : config.model,
124275
124318
  schema,
124276
124319
  signal,
124277
124320
  contextPolicy: false,
@@ -124294,11 +124337,30 @@ ${prompt}`,
124294
124337
  const urls = new Set;
124295
124338
  const queries = new Set;
124296
124339
  let evidenceChars = 0;
124340
+ const sourceText = new Map;
124341
+ const rebalanceEvidence = () => {
124342
+ let remaining = limits.evidenceChars;
124343
+ const weight = (source) => source.contentFetchedAt ? 4 : 1;
124344
+ const ordered = [...result.sources].sort((a, b) => sourceText.get(a.url).length / weight(a) - sourceText.get(b.url).length / weight(b));
124345
+ let remainingWeight = ordered.reduce((sum, source) => sum + weight(source), 0);
124346
+ for (const source of ordered) {
124347
+ const text = sourceText.get(source.url);
124348
+ const allowance = Math.floor(remaining * weight(source) / remainingWeight);
124349
+ remainingWeight -= weight(source);
124350
+ source.excerpts = [text.slice(0, allowance)];
124351
+ remaining -= source.excerpts[0].length;
124352
+ }
124353
+ evidenceChars = limits.evidenceChars - remaining;
124354
+ };
124297
124355
  const addSource = (source) => {
124298
124356
  if (!requiredPhrases.every((phrase) => normalize(`${source.title} ${source.excerpts.join(" ")}`).includes(normalize(phrase))))
124299
124357
  return;
124300
- if (result.sources.some((existing) => existing.url === source.url) || evidenceChars >= limits.evidenceChars)
124358
+ if (result.sources.some((existing) => existing.url === source.url))
124301
124359
  return;
124360
+ if (result.sources.length >= 64) {
124361
+ result.limitations.push("Evidence source limit reached");
124362
+ return;
124363
+ }
124302
124364
  try {
124303
124365
  const url = new URL(source.url);
124304
124366
  if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
@@ -124307,15 +124369,16 @@ ${prompt}`,
124307
124369
  return;
124308
124370
  }
124309
124371
  const text = source.excerpts.join(`
124310
- `).slice(0, limits.evidenceChars - evidenceChars);
124372
+ `).slice(0, limits.evidenceChars);
124311
124373
  if (!text.trim())
124312
124374
  return;
124313
- evidenceChars += text.length;
124375
+ sourceText.set(source.url, text);
124314
124376
  result.sources.push({
124315
124377
  ...source,
124316
124378
  id: `s${result.sources.length + 1}`,
124317
124379
  excerpts: [text]
124318
124380
  });
124381
+ rebalanceEvidence();
124319
124382
  };
124320
124383
  const doSearch = async (query2) => {
124321
124384
  for (const bounded of boundedQueries(query2)) {
@@ -124358,12 +124421,9 @@ ${prompt}`,
124358
124421
  if (page.text && page.status !== "error") {
124359
124422
  const prior = result.sources.find((source) => source.url === url);
124360
124423
  if (prior) {
124361
- const extra = page.text.slice(0, limits.evidenceChars - evidenceChars);
124362
- if (extra) {
124363
- prior.excerpts.push(extra);
124364
- evidenceChars += extra.length;
124365
- prior.contentFetchedAt = page.fetchedAt;
124366
- }
124424
+ sourceText.set(prior.url, page.text.slice(0, limits.evidenceChars));
124425
+ prior.contentFetchedAt = page.fetchedAt;
124426
+ rebalanceEvidence();
124367
124427
  } else
124368
124428
  addSource({
124369
124429
  id: "",
@@ -124381,16 +124441,24 @@ ${prompt}`,
124381
124441
  for (let round = 0;round < limits.rounds; round++) {
124382
124442
  if (!result.sources.length && queries.size >= limits.searches)
124383
124443
  break;
124384
- const plan = await generate2("plan", PlanSchema, "Select follow-up searches for missing evidence and primary-source URLs to read. URLs must come from the supplied search results. done means available evidence addresses the requested schema, not merely that some results exist. Return no actions if further work is not useful.", {
124385
- query: input.query,
124386
- schema: task.schema,
124387
- instructions: task.instructions,
124388
- sources: result.sources,
124389
- searched: [...queries],
124390
- read: [...urls],
124391
- remainingSearches: limits.searches - queries.size,
124392
- remainingReads: limits.reads - urls.size
124393
- });
124444
+ let plan;
124445
+ try {
124446
+ plan = await generate2("plan", PlanSchema, "Select follow-up searches for missing evidence and primary-source URLs to read. For current people or roles, seek the organization\u2019s own leadership/about/team pages; testimonials, package authors, and job listings are not staff identity evidence. For policies, read the full policy or agreement to preserve section scope and exceptions. URLs must come from the supplied search results. done means available evidence addresses the requested schema, not merely that some results exist. Return no actions if further work is not useful.", {
124447
+ query: input.query,
124448
+ schema: task.schema,
124449
+ instructions: task.instructions,
124450
+ sources: result.sources,
124451
+ searched: [...queries],
124452
+ read: [...urls],
124453
+ remainingSearches: limits.searches - queries.size,
124454
+ remainingReads: limits.reads - urls.size
124455
+ });
124456
+ } catch (error) {
124457
+ if (!(error instanceof Error) || error.message !== "Invalid plan output")
124458
+ throw error;
124459
+ result.limitations.push("Invalid planning output; continued with retrieved evidence");
124460
+ break;
124461
+ }
124394
124462
  for (const url of plan.urls)
124395
124463
  await doRead(url);
124396
124464
  for (const query2 of plan.queries)
@@ -124406,15 +124474,30 @@ ${prompt}`,
124406
124474
  }
124407
124475
  if (evidenceChars >= limits.evidenceChars)
124408
124476
  result.limitations.push("Evidence text limit reached");
124409
- const data = await generate2("extract", task.schema, `Extract only evidence-supported facts for the requested query and schema. ${task.instructions ?? ""}`, { query: input.query, sources: result.sources });
124477
+ const data = await generate2("extract", task.schema, `Extract only facts that directly answer the requested query and schema. Return concise, atomic answers; include only the minimum facts needed to answer the question. For a leadership query, report the named person and current title, not their biography, education, or acquisition history. Do not pad findings with background facts, excluded candidates, or statements that evidence is missing; return an empty findings array when no answer is established. Preserve all material qualifiers: program versus benefit/trial, customer versus partner, eligibility versus automatic approval, dates, exceptions, and attribution of promotional claims. Never generalize a section-specific condition to the whole organization or program. ${task.instructions ?? ""}`, {
124478
+ query: input.query,
124479
+ asOf: result.generatedAt,
124480
+ sources: result.sources
124481
+ });
124410
124482
  const leaves = researchLeaves(data, limits.fields);
124411
124483
  if (!leaves.length) {
124412
124484
  result.data = data;
124413
124485
  result.status = result.limitations.length ? "partial" : "empty";
124414
124486
  return result;
124415
124487
  }
124416
- const reviewed = await generate2("review", ReviewSchema, "Independently review EVERY JSON pointer and its value. supported requires exact quotes establishing the entire fact for the correct entity and time. A name mention or publication date is insufficient to establish employment, event date, ownership, or buying intent. Check all sources for conflicts. Use conflicting when sources disagree, unsupported for contradicted claims, unknown for missing support. Null is unknown. Quotes must appear in supplied excerpts. Return each pointer once. Never treat this review as independent real-world verification.", { query: input.query, fields: leaves, sources: result.sources });
124417
- result.fields = bindResearchReview(leaves, reviewed.fields, result.sources);
124488
+ const reviewSources = researchPassages(result.sources);
124489
+ const reviewed = await generate2("review", ReviewSchema, "Independently review EVERY JSON pointer and its value. supported requires exact quotes establishing the entire fact for the correct entity and time. A name mention or publication date is insufficient to establish employment, event date, ownership, or buying intent. Check all sources for conflicts. Use conflicting when sources disagree, unsupported for contradicted claims, unknown for missing support. Null is unknown. Cite only supplied passage IDs; their text will be copied verbatim by the runtime. For each field, explicitly check answersQuestion (directly answers query/schema/instructions, not background or a non-answer), correctEntity, correctTime, and preservesScope. Read surrounding context and headings, not only the matching quote. A benefit/trial condition is not a program-wide rule; customer conditions are not partner conditions; eligible is not approved or automatic. Preserve exceptions, qualifiers and marketing attribution. Every check must pass for supported. A leader\u2019s education or career history does not answer who currently leads a team. Keep each reason under 25 words. Cite only the minimum passages needed to establish the entire field, at most four. Do not invent passage IDs. Read adjacent passages for conditions and exceptions, even when citing only one. Return each pointer once. Never treat this review as independent real-world verification.", {
124490
+ query: input.query,
124491
+ asOf: result.generatedAt,
124492
+ instructions: task.instructions,
124493
+ schema: task.schema,
124494
+ fields: leaves,
124495
+ sources: reviewSources
124496
+ });
124497
+ result.fields = bindResearchReview(leaves, reviewed.fields.map((field) => ({
124498
+ ...field,
124499
+ citations: resolveResearchPassages(field.citations, reviewSources)
124500
+ })), result.sources);
124418
124501
  const complete = result.fields.length > 0 && result.fields.every((field) => field.verdict === "supported");
124419
124502
  result.data = complete ? data : null;
124420
124503
  result.status = complete && !result.limitations.length ? "reviewed" : "partial";
@@ -125282,5 +125365,5 @@ export {
125282
125365
  selectResearchMonitorEvents
125283
125366
  };
125284
125367
 
125285
- //# debugId=FE8A4140B2EAA36864756E2164756E21
125368
+ //# debugId=F9058798C65361FB64756E2164756E21
125286
125369
  //# sourceMappingURL=index.js.map