@absolutejs/rag 0.18.0 → 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,22 @@ 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
+
19
+ ## 0.18.1 — 2026-09-24
20
+
21
+ ### Fixed
22
+
23
+ - **Accept Elysia 2 and TypeBox 1 research schemas with inferred result types and matching runtime validation while preserving TypeBox 0.34 compatibility.** (`ResearchTask`, `ResearchRuntime`, `ResearchSchema`, `ResearchStatic`, `createResearch`)
24
+
9
25
  ## 0.18.0 — 2026-09-24
10
26
 
11
27
  ### Added
package/RESEARCH.md CHANGED
@@ -5,8 +5,7 @@
5
5
  ## Configure once
6
6
 
7
7
  ```ts
8
- import { Elysia } from "elysia";
9
- import { Type } from "@sinclair/typebox";
8
+ import { Elysia, t } from "elysia";
10
9
  import { anthropic } from "@absolutejs/ai/anthropic";
11
10
  import { createBraveSearch } from "@absolutejs/search/brave";
12
11
  import {
@@ -21,7 +20,7 @@ const research = createResearch({
21
20
  model: process.env.RESEARCH_MODEL!,
22
21
  tasks: {
23
22
  company: {
24
- schema: Type.Object({ name: Type.String(), description: Type.String() }),
23
+ schema: t.Object({ name: t.String(), description: t.String() }),
25
24
  },
26
25
  signals: researchMonitorTask,
27
26
  },
@@ -45,7 +44,7 @@ const result = await research.run({
45
44
 
46
45
  `authorizeResearchRequest` is your application's authorization function, not an export of this package. It is required. For multiple tenants, pass `runtime: request => runtimeForAuthorizedTenant(request)`; authorization runs first. Scope tenant caches, workflows and budget ledgers to the same resolved identity. The plugin rejects cross-origin browser POSTs and sends `Cache-Control: no-store`. Do not treat a caller-supplied tenant header as identity.
47
46
 
48
- `run` selects server-registered schemas; browsers cannot override models, limits, schemas or credentials. `extract({schema, instructions}, input)` accepts a typed TypeBox schema directly for trusted server code. The same configured AI provider handles planning, extraction and independent review, with transport retries and schema repair retries disabled.
47
+ `run` selects server-registered schemas; browsers cannot override models, limits, schemas or credentials. `extract({schema, instructions}, input)` accepts JSON-compatible Elysia 2 `t`, TypeBox 1 (`typebox`), or legacy TypeBox 0.34 (`@sinclair/typebox`) schemas directly, preserving result type inference and runtime validation for trusted server code. The same configured AI provider handles planning, extraction and independent review, with transport retries and schema repair retries disabled.
49
48
 
50
49
  Default limits are four searches, four page reads, two planning rounds, 90 seconds, 32,000 evidence characters, 4,096 output tokens per model call and 128 primitive output fields. Limits are per research call, not per entire batch. Page reading uses the existing hardened public web transport; optional `render` enables your existing browser service. A browser process is not provisioned automatically. Cancellation must also be honored by custom providers and readers.
51
50
 
@@ -267,3 +266,42 @@ const mcpTools = toMcpToolRegistry(researchManifest, {
267
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.
268
267
 
269
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,45 @@
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
+ },
27
+ {
28
+ "changes": [
29
+ {
30
+ "kind": "fixed",
31
+ "summary": "Accept Elysia 2 and TypeBox 1 research schemas with inferred result types and matching runtime validation while preserving TypeBox 0.34 compatibility.",
32
+ "symbols": [
33
+ "ResearchTask",
34
+ "ResearchRuntime",
35
+ "ResearchSchema",
36
+ "ResearchStatic",
37
+ "createResearch"
38
+ ]
39
+ }
40
+ ],
41
+ "date": "2026-09-24",
42
+ "version": "0.18.1"
43
+ },
5
44
  {
6
45
  "changes": [
7
46
  {
@@ -124126,14 +124126,31 @@ __export(exports_value2, {
124126
124126
  Patch: () => Patch,
124127
124127
  ValueErrorIterator: () => ValueErrorIterator
124128
124128
  });
124129
+ // src/research/schema.ts
124130
+ import { Check as Check2 } from "typebox/value";
124131
+ var isLegacySchema = (schema) => (Kind in schema);
124132
+ var checkResearchValue = (schema, value) => {
124133
+ if (isLegacySchema(schema))
124134
+ return exports_value2.Check(schema, value);
124135
+ return Check2(schema, value);
124136
+ };
124137
+
124129
124138
  // src/research/evidence.ts
124130
124139
  import { sourceSupportsQuote } from "@absolutejs/search";
124131
124140
  var ReviewSchema = Type2.Object({
124132
124141
  fields: Type2.Array(Type2.Object({
124133
124142
  path: Type2.String(),
124134
124143
  verdict: Type2.Union(["supported", "unsupported", "conflicting", "unknown"].map((value) => Type2.Literal(value))),
124135
- citations: Type2.Array(Type2.Object({ sourceId: Type2.String(), quote: Type2.String() }), { maxItems: 8 }),
124136
- 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
+ })
124137
124154
  }), { maxItems: 256 })
124138
124155
  });
124139
124156
  var researchLeaves = (value, maxFields = 128) => {
@@ -124163,18 +124180,53 @@ var bindResearchReview = (leaves, reviews, sources) => leaves.map((leaf) => {
124163
124180
  return matching.length === 1 && sourceSupportsQuote(matching[0], citation.quote);
124164
124181
  }) ?? [];
124165
124182
  const valid = !!review && citations.length === review.citations.length;
124166
- 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;
124167
124190
  return {
124168
124191
  ...leaf,
124169
124192
  verdict,
124170
124193
  citations,
124171
- 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"
124172
124196
  };
124173
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
+ });
124174
124226
 
124175
124227
  // src/research/runtime.ts
124176
124228
  var DefaultSchema = Type2.Object({
124177
- findings: Type2.Array(Type2.String(), { maxItems: 12 })
124229
+ findings: Type2.Array(Type2.String(), { maxItems: 6 })
124178
124230
  });
124179
124231
  var PlanSchema = Type2.Object({
124180
124232
  queries: Type2.Array(Type2.String({ maxLength: 600 }), { maxItems: 8 }),
@@ -124261,8 +124313,8 @@ var createResearch = (config) => {
124261
124313
  };
124262
124314
  const generate2 = async (kind, schema, prompt, payload) => {
124263
124315
  const generated = await operate(kind, () => (config.generateObject ?? generateObjectAI)({
124264
- provider: config.provider,
124265
- 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,
124266
124318
  schema,
124267
124319
  signal,
124268
124320
  contextPolicy: false,
@@ -124273,7 +124325,7 @@ var createResearch = (config) => {
124273
124325
  ${prompt}`,
124274
124326
  messages: [{ role: "user", content: JSON.stringify(payload) }],
124275
124327
  validate: (value) => {
124276
- if (!exports_value2.Check(schema, value))
124328
+ if (!checkResearchValue(schema, value))
124277
124329
  throw new Error(`Invalid ${kind} output`);
124278
124330
  return value;
124279
124331
  }
@@ -124285,11 +124337,30 @@ ${prompt}`,
124285
124337
  const urls = new Set;
124286
124338
  const queries = new Set;
124287
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
+ };
124288
124355
  const addSource = (source) => {
124289
124356
  if (!requiredPhrases.every((phrase) => normalize(`${source.title} ${source.excerpts.join(" ")}`).includes(normalize(phrase))))
124290
124357
  return;
124291
- if (result.sources.some((existing) => existing.url === source.url) || evidenceChars >= limits.evidenceChars)
124358
+ if (result.sources.some((existing) => existing.url === source.url))
124359
+ return;
124360
+ if (result.sources.length >= 64) {
124361
+ result.limitations.push("Evidence source limit reached");
124292
124362
  return;
124363
+ }
124293
124364
  try {
124294
124365
  const url = new URL(source.url);
124295
124366
  if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
@@ -124298,15 +124369,16 @@ ${prompt}`,
124298
124369
  return;
124299
124370
  }
124300
124371
  const text = source.excerpts.join(`
124301
- `).slice(0, limits.evidenceChars - evidenceChars);
124372
+ `).slice(0, limits.evidenceChars);
124302
124373
  if (!text.trim())
124303
124374
  return;
124304
- evidenceChars += text.length;
124375
+ sourceText.set(source.url, text);
124305
124376
  result.sources.push({
124306
124377
  ...source,
124307
124378
  id: `s${result.sources.length + 1}`,
124308
124379
  excerpts: [text]
124309
124380
  });
124381
+ rebalanceEvidence();
124310
124382
  };
124311
124383
  const doSearch = async (query2) => {
124312
124384
  for (const bounded of boundedQueries(query2)) {
@@ -124349,12 +124421,9 @@ ${prompt}`,
124349
124421
  if (page.text && page.status !== "error") {
124350
124422
  const prior = result.sources.find((source) => source.url === url);
124351
124423
  if (prior) {
124352
- const extra = page.text.slice(0, limits.evidenceChars - evidenceChars);
124353
- if (extra) {
124354
- prior.excerpts.push(extra);
124355
- evidenceChars += extra.length;
124356
- prior.contentFetchedAt = page.fetchedAt;
124357
- }
124424
+ sourceText.set(prior.url, page.text.slice(0, limits.evidenceChars));
124425
+ prior.contentFetchedAt = page.fetchedAt;
124426
+ rebalanceEvidence();
124358
124427
  } else
124359
124428
  addSource({
124360
124429
  id: "",
@@ -124372,16 +124441,24 @@ ${prompt}`,
124372
124441
  for (let round = 0;round < limits.rounds; round++) {
124373
124442
  if (!result.sources.length && queries.size >= limits.searches)
124374
124443
  break;
124375
- 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.", {
124376
- query: input.query,
124377
- schema: task.schema,
124378
- instructions: task.instructions,
124379
- sources: result.sources,
124380
- searched: [...queries],
124381
- read: [...urls],
124382
- remainingSearches: limits.searches - queries.size,
124383
- remainingReads: limits.reads - urls.size
124384
- });
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
+ }
124385
124462
  for (const url of plan.urls)
124386
124463
  await doRead(url);
124387
124464
  for (const query2 of plan.queries)
@@ -124397,15 +124474,30 @@ ${prompt}`,
124397
124474
  }
124398
124475
  if (evidenceChars >= limits.evidenceChars)
124399
124476
  result.limitations.push("Evidence text limit reached");
124400
- 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
+ });
124401
124482
  const leaves = researchLeaves(data, limits.fields);
124402
124483
  if (!leaves.length) {
124403
124484
  result.data = data;
124404
124485
  result.status = result.limitations.length ? "partial" : "empty";
124405
124486
  return result;
124406
124487
  }
124407
- 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 });
124408
- 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);
124409
124501
  const complete = result.fields.length > 0 && result.fields.every((field) => field.verdict === "supported");
124410
124502
  result.data = complete ? data : null;
124411
124503
  result.status = complete && !result.limitations.length ? "reviewed" : "partial";
@@ -125273,5 +125365,5 @@ export {
125273
125365
  selectResearchMonitorEvents
125274
125366
  };
125275
125367
 
125276
- //# debugId=815A4FA45BDD337464756E2164756E21
125368
+ //# debugId=F9058798C65361FB64756E2164756E21
125277
125369
  //# sourceMappingURL=index.js.map