@lazyingart/agintiflow 0.20.241 → 0.20.242

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.
@@ -35,6 +35,11 @@ every query an unbounded agent swarm:
35
35
  paper set erase the requested source class. Search snippets can guide
36
36
  discovery, but they cannot satisfy verified first-party coverage or enter
37
37
  synthesis as cited evidence.
38
+ Phrases such as “official Temporal docs” retain this requirement even when a
39
+ product name occurs between `official` and `docs`. Product-owned root pages
40
+ and vendor blogs are classified using the named discovery route; unrelated
41
+ third-party pages are not promoted merely because their URL repeats one
42
+ query word.
38
43
  Canonical duplicates found by multiple providers are promoted and retain
39
44
  per-provider rank evidence. Multi-domain corpora receive separate bounded `site:`
40
45
  queries matched to entity-specific subquestions instead of one fragile OR
@@ -99,6 +104,10 @@ every query an unbounded agent swarm:
99
104
  rather than arbitrary page text. Every substantive paragraph and finding
100
105
  cites exact evidence IDs instead of merely naming a source. A failed main
101
106
  synthesis gets one same-provider fast-model fallback.
107
+ A request for practical recommendations becomes a completion obligation:
108
+ every recommendation must cite verified evidence, and the run fails closed
109
+ rather than delivering an unsupported advice section. The renderer uses the
110
+ compact research objective instead of dumping the raw task prompt.
102
111
  9. **Audit**: deterministic code removes unknown or unverified evidence IDs,
103
112
  derives visible source citations from accepted evidence records, removes
104
113
  unsupported synthesis statements, then reports claim, quotation, citation,
@@ -110,6 +119,13 @@ every query an unbounded agent swarm:
110
119
  guarded workspace-relative `outputPath` when the caller requests a durable
111
120
  filename, then sent to the canvas.
112
121
 
122
+ The reader-facing report always includes an explicit limitations section and
123
+ separates quote-verified sources from inspected-but-uncited pages. When the
124
+ original request asks for reproducibility or exact quotations, deterministic
125
+ rendering adds a verification procedure, retrieval timestamps and SHA-256
126
+ digests, and a verified-evidence appendix. These sections come from the
127
+ retrieval/evidence ledger, not model memory.
128
+
113
129
  This combines the orchestrator/worker and separate citation-pass lessons
114
130
  described by [Anthropic's production research
115
131
  system](https://www.anthropic.com/engineering/multi-agent-research-system)
@@ -136,6 +152,8 @@ the authoritative original user goal. A planner may shorten the research
136
152
  question, but cannot silently drop requirements such as “compare at least three
137
153
  independent primary sources,” “read a paper/PDF when available,” or “include
138
154
  negative evidence.” These requirements are fingerprinted and checkpointed.
155
+ The same contract covers practical recommendations, reproducible verification
156
+ methods, and exact-quote evidence appendices.
139
157
 
140
158
  Planning and evidence extraction deliberately use the configured routing model
141
159
  when it belongs to a hosted active provider. Synthesis uses the configured main
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.241",
3
+ "version": "0.20.242",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -441,6 +441,7 @@ async function main() {
441
441
  ],
442
442
  }],
443
443
  keyFindings: [{ claim: "Claims retain exact evidence IDs.", evidenceIds: ["S1-C1", "S2-C1"], confidence: "high" }],
444
+ recommendations: [],
444
445
  contradictions: [],
445
446
  uncertainties: [],
446
447
  nextQuestions: [],
@@ -601,6 +602,7 @@ async function main() {
601
602
  executiveSummaryEvidenceIds: ["S1-C1"],
602
603
  sections: [{ heading: "Claim", paragraphs: [{ text: "The gain was measured.", evidenceIds: ["S1-C1"] }] }],
603
604
  keyFindings: [],
605
+ recommendations: [],
604
606
  contradictions: [],
605
607
  uncertainties: ["The exact source was inaccessible."],
606
608
  nextQuestions: [],
@@ -734,6 +736,7 @@ async function main() {
734
736
  { claim: "A second independent source corroborates the measurement.", evidenceIds: ["S2-C1"], confidence: "high" },
735
737
  { claim: "A third independent source supplies separate evidence.", evidenceIds: ["S3-C1"], confidence: "high" },
736
738
  ],
739
+ recommendations: [],
737
740
  contradictions: [],
738
741
  uncertainties: ["Benchmark scope remains limited."],
739
742
  nextQuestions: [],
@@ -1004,6 +1007,95 @@ async function main() {
1004
1007
  "a bounded official PDF was still constrained by the generic 2 MiB page limit"
1005
1008
  );
1006
1009
 
1010
+ const namedOfficialResearch = await deepResearch(
1011
+ {
1012
+ query: "Compare official Temporal docs and official LangGraph docs for durable agent execution.",
1013
+ depth: "quick",
1014
+ sourcePolicy: "primary",
1015
+ maxQueries: 1,
1016
+ maxSources: 2,
1017
+ gapPasses: 0,
1018
+ researchId: "named-official-products-smoke",
1019
+ outputPath: "reports/named-official-products.md",
1020
+ dryRun: true,
1021
+ },
1022
+ {
1023
+ provider: "mock",
1024
+ model: "mock-agent",
1025
+ commandCwd: researchWorkspace,
1026
+ webSearchImpl: async ({ query }) => ({
1027
+ ok: true,
1028
+ toolName: "web_search",
1029
+ provider: "test",
1030
+ query,
1031
+ results: /official system card engineering blog architecture/i.test(query)
1032
+ ? [
1033
+ {
1034
+ rank: 1,
1035
+ title: "Durable Execution Solutions",
1036
+ url: "https://temporal.io/blog/durable-execution-solutions",
1037
+ canonicalUrl: "https://temporal.io/blog/durable-execution-solutions",
1038
+ domain: "temporal.io",
1039
+ snippet: "Temporal explains durable execution and workflow recovery.",
1040
+ provider: "test",
1041
+ },
1042
+ {
1043
+ rank: 2,
1044
+ title: "Third-party retry commentary",
1045
+ url: "https://appscale.example.org/blog/retry-commentary",
1046
+ canonicalUrl: "https://appscale.example.org/blog/retry-commentary",
1047
+ domain: "appscale.example.org",
1048
+ snippet: "A secondary opinion about retry behavior.",
1049
+ provider: "test",
1050
+ },
1051
+ ]
1052
+ : [
1053
+ {
1054
+ rank: 1,
1055
+ title: "LangGraph",
1056
+ url: "https://www.langchain.com/langgraph",
1057
+ canonicalUrl: "https://www.langchain.com/langgraph",
1058
+ domain: "langchain.com",
1059
+ snippet: "LangGraph provides durable execution for long-running stateful agents.",
1060
+ provider: "test",
1061
+ },
1062
+ {
1063
+ rank: 2,
1064
+ title: "Generic durable systems survey",
1065
+ url: "https://link.springer.com/article/generic-durable-systems",
1066
+ canonicalUrl: "https://link.springer.com/article/generic-durable-systems",
1067
+ domain: "link.springer.com",
1068
+ snippet: "A broad survey with little agent-specific implementation detail.",
1069
+ provider: "test",
1070
+ },
1071
+ ],
1072
+ }),
1073
+ webPageReaderImpl: async ({ url }) => ({
1074
+ ok: true,
1075
+ toolName: "read_web_page",
1076
+ url,
1077
+ title: url.includes("temporal.io") ? "Temporal Durable Execution" : "LangGraph",
1078
+ readable: true,
1079
+ contentType: "text/html",
1080
+ retrievedAt: "2026-08-25T00:00:00.000Z",
1081
+ sha256: (url.includes("temporal.io") ? "1" : "2").repeat(64),
1082
+ content: "The official product page documents durable execution, checkpointing, and recovery behavior.",
1083
+ passages: ["The official product page documents durable execution, checkpointing, and recovery behavior."],
1084
+ }),
1085
+ },
1086
+ new SessionStore(path.join(tempRoot, "sessions"), "named-official-products-smoke")
1087
+ );
1088
+ assert(namedOfficialResearch.ok, `named official product research failed: ${namedOfficialResearch.error || "unknown"}`);
1089
+ assert(namedOfficialResearch.requirements.officialDiscovery, "'official Temporal docs' did not preserve first-party discovery intent");
1090
+ assert(
1091
+ namedOfficialResearch.sources.map((source) => source.domain).sort().join(",") === "langchain.com,temporal.io",
1092
+ `named first-party product pages were displaced by weaker filler: ${namedOfficialResearch.sources.map((source) => source.domain).join(", ")}`
1093
+ );
1094
+ assert(namedOfficialResearch.sources.every((source) => source.firstParty), "a named official product root was not classified as first-party");
1095
+ const namedOfficialReport = await fs.readFile(namedOfficialResearch.reportPath, "utf8");
1096
+ assert(namedOfficialReport.includes("## Limitations, Uncertainties, And Coverage Gaps"), "research report omitted an explicit limitations section");
1097
+ assert(namedOfficialReport.includes("## Sources Inspected But Not Cited"), "dry-run report hid the uncited-source boundary");
1098
+
1007
1099
  const scholarlyGapCalls = [];
1008
1100
  const scholarlyGapResearch = await deepResearch(
1009
1101
  {
@@ -1070,6 +1162,7 @@ async function main() {
1070
1162
  paragraphs: [{ text: "Two primary sources support the result.", evidenceIds: ["S1-C1", "S2-C1"] }],
1071
1163
  }],
1072
1164
  keyFindings: [{ claim: "Independent evidence was recovered.", evidenceIds: ["S1-C1", "S2-C1"], confidence: "high" }],
1165
+ recommendations: [],
1073
1166
  contradictions: [],
1074
1167
  uncertainties: [],
1075
1168
  nextQuestions: [],
@@ -1209,6 +1302,7 @@ async function main() {
1209
1302
  evidenceIds: ["S1-C1", "S2-C1", "S3-C1"],
1210
1303
  confidence: "high",
1211
1304
  }],
1305
+ recommendations: [],
1212
1306
  contradictions: [],
1213
1307
  uncertainties: ["Real throughput depends on local hardware."],
1214
1308
  nextQuestions: [],
@@ -34,7 +34,9 @@ When the user requests a specific Markdown report filename, pass it as
34
34
  `outputPath` so the bounded engine writes the complete report directly into the
35
35
  workspace. Do not try to read or copy a private session artifact afterward.
36
36
  Preserve explicit evidence requirements with `requirePdf`,
37
- `minIndependentSources`, and `includeNegativeEvidence` when they are present.
37
+ `minIndependentSources`, `includeNegativeEvidence`, `requireRecommendations`,
38
+ `requireVerificationMethod`, and `includeEvidenceAppendix` when they are
39
+ present.
38
40
  The engine also derives them from the authoritative original request so a
39
41
  shortened planner query cannot discard them.
40
42
 
@@ -86,6 +88,16 @@ reject an ordinary multi-megabyte system card using the lower HTML-page limit.
86
88
  Browser-verification, CAPTCHA, and access-challenge pages are unreadable; their
87
89
  warning text is not evidence.
88
90
 
91
+ Treat a requested report as a reader-facing decision artifact, never an
92
+ execution transcript. Do not dump the full raw prompt, task IDs, transport IDs,
93
+ schemas, or orchestration instructions into it. When recommendations are
94
+ requested, each must be concrete and traceable to verified evidence. Always
95
+ make limitations explicit. When requested, the engine adds a reproducible
96
+ verification method and an exact-quote evidence appendix from the deterministic
97
+ evidence ledger. It separates verified sources from pages inspected but not
98
+ cited, instead of padding the bibliography with unreadable or unsupported
99
+ entries.
100
+
89
101
  Return the substantive answer naturally. Include useful citations and the
90
102
  report path; do not narrate internal query loops or model calls. If coverage or
91
103
  quote verification is weak, say exactly what remains unsupported instead of
@@ -14,7 +14,7 @@ import {
14
14
  } from "./web-search.js";
15
15
  import { resolveWorkspacePath } from "./workspace-tools.js";
16
16
 
17
- const RESEARCH_VERSION = 12;
17
+ const RESEARCH_VERSION = 13;
18
18
  const DEPTH_BUDGETS = Object.freeze({
19
19
  quick: Object.freeze({ maxQueries: 3, maxSources: 6, concurrency: 3, gapPasses: 0 }),
20
20
  standard: Object.freeze({ maxQueries: 6, maxSources: 12, concurrency: 4, gapPasses: 1 }),
@@ -136,14 +136,29 @@ function researchRequirements(args = {}, config = {}) {
136
136
  /学术|學術|论文|論文|文献综述|文獻綜述|期刊|引用|系统综述|系統綜述/.test(intent);
137
137
  const officialDiscovery =
138
138
  sourcePolicy(args.sourcePolicy) === "official" ||
139
- /\b(?:first[- ]party|official (?:documentation|engineering|implementation|technical|writeups?)|system card|vendor engineering)\b/i.test(intent) ||
139
+ /\b(?:first[- ]party|official\b[^.\n]{0,100}\b(?:docs?|documentation|engineering|guidance|implementation|technical|writeups?)|(?:docs?|documentation|engineering|guidance|implementation|technical|writeups?)\b[^.\n]{0,100}\b(?:from|by)\s+(?:the\s+)?official|system card|vendor engineering)\b/i.test(intent) ||
140
140
  /官方(?:文档|文檔|工程|技术|技術)|第一方|系统卡|系統卡/.test(intent);
141
+ const requireRecommendations =
142
+ args.requireRecommendations === true ||
143
+ /\b(?:actionable|concrete|implementation|practical)\s+(?:advice|recommendations?|steps)|\brecommend(?:ation|ations|ed)?\b|\bwhat (?:we|i|the team) should do\b/i.test(intent) ||
144
+ /建议|建議|推荐|推薦|下一步|怎么做|怎麼做/.test(intent);
145
+ const requireVerificationMethod =
146
+ args.requireVerificationMethod === true ||
147
+ /\b(?:reproducible|repeatable)\s+(?:verification|validation|audit|method|procedure)|\b(?:verification|validation)\s+(?:method|procedure|protocol)|\bhow to (?:reproduce|verify|validate)\b/i.test(intent) ||
148
+ /可复现|可復現|验证方法|驗證方法|验证流程|驗證流程/.test(intent);
149
+ const includeEvidenceAppendix =
150
+ args.includeEvidenceAppendix === true ||
151
+ /\b(?:exact|verbatim|source-grounded)\s+(?:quotes?|quotations?|excerpts?)|\bevidence appendix\b|\bquote-level evidence\b/i.test(intent) ||
152
+ /原文引文|原文引用|证据附录|證據附錄/.test(intent);
141
153
  return {
142
154
  requirePdf,
143
155
  pdfMode: requirePdf ? (pdfWhenAvailable ? "when-available" : "required") : "optional",
144
156
  minIndependentSources: Math.max(requestedMinimum, inferredMinimum),
145
157
  scholarlyDiscovery,
146
158
  officialDiscovery,
159
+ requireRecommendations,
160
+ requireVerificationMethod,
161
+ includeEvidenceAppendix,
147
162
  includeNegativeEvidence:
148
163
  args.includeNegativeEvidence === true ||
149
164
  /\b(?:negative|conflicting|contradictory|counter[- ]?evidence|disagreement|limitations?|falsifying|unresolved)\b/i.test(intent) ||
@@ -350,6 +365,18 @@ const SYNTHESIS_SCHEMA = {
350
365
  additionalProperties: false,
351
366
  },
352
367
  },
368
+ recommendations: {
369
+ type: "array",
370
+ items: {
371
+ type: "object",
372
+ properties: {
373
+ text: { type: "string" },
374
+ evidenceIds: { type: "array", items: { type: "string" } },
375
+ },
376
+ required: ["text", "evidenceIds"],
377
+ additionalProperties: false,
378
+ },
379
+ },
353
380
  contradictions: { type: "array", items: { type: "string" } },
354
381
  uncertainties: { type: "array", items: { type: "string" } },
355
382
  nextQuestions: { type: "array", items: { type: "string" } },
@@ -360,6 +387,7 @@ const SYNTHESIS_SCHEMA = {
360
387
  "executiveSummaryEvidenceIds",
361
388
  "sections",
362
389
  "keyFindings",
390
+ "recommendations",
363
391
  "contradictions",
364
392
  "uncertainties",
365
393
  "nextQuestions",
@@ -678,7 +706,7 @@ function scholarlyCandidate(result = {}) {
678
706
  return scholarlyDomain(result.domain, result.url) || indexes.some((index) => ["arxiv", "crossref"].includes(index));
679
707
  }
680
708
 
681
- function firstPartyCandidate(result = {}) {
709
+ function firstPartyCandidate(result = {}, discoveryQuery = result.discoveryQuery || "") {
682
710
  if (repositoryCandidate(result) || scholarlyCandidate(result)) return false;
683
711
  const domain = String(result.domain || "").toLowerCase();
684
712
  let pathname = "";
@@ -699,6 +727,8 @@ function firstPartyCandidate(result = {}) {
699
727
  ? suffix
700
728
  : secondLevel;
701
729
  const titleAndSnippet = `${result.title || ""} ${result.snippet || ""}`.toLowerCase();
730
+ const discoveryText = String(discoveryQuery || "").toLowerCase();
731
+ const pathnameParts = pathname.split("/").filter(Boolean);
702
732
  const brandedOfficialArtifact =
703
733
  /\b(?:introducing|official|system card|technical report|whitepaper)\b/i.test(titleAndSnippet) &&
704
734
  brand.length >= 4 &&
@@ -706,11 +736,22 @@ function firstPartyCandidate(result = {}) {
706
736
  const cdnOfficialArtifact =
707
737
  /^cdn\./.test(domain) && /(?:system[-_]?card|technical[-_]?report|whitepaper)/.test(pathname);
708
738
  const discoveryIntentMatchesBrand =
709
- result.sourceIntent === "first-party" && brand.length >= 4 && titleAndSnippet.includes(brand);
739
+ result.sourceIntent === "first-party" && brand.length >= 4 &&
740
+ (titleAndSnippet.includes(brand) || discoveryText.includes(brand));
710
741
  const discoveryIntentLooksTechnical =
711
742
  result.sourceIntent === "first-party" &&
712
743
  domainParts.length >= 3 &&
713
744
  /\b(?:architecture|engineering|how (?:we|it) (?:built|works)|implementation|retrieval system|system card|technical (?:article|blog|report|writeup|write-up))\b/i.test(titleAndSnippet);
745
+ const queryNamesDomainBrand =
746
+ brand.length >= 4 &&
747
+ discoveryText.includes(brand) &&
748
+ !/\b(?:review of|comparison of|alternative to)\b/i.test(titleAndSnippet);
749
+ const queryNamesProductRoot =
750
+ domainParts.length === 2 &&
751
+ pathnameParts.length === 1 &&
752
+ pathnameParts[0].length >= 4 &&
753
+ discoveryText.includes(pathnameParts[0]) &&
754
+ titleAndSnippet.includes(pathnameParts[0]);
714
755
  return (
715
756
  /\.(gov|mil)$/.test(domain) ||
716
757
  /^(?:api|developer|developers|docs|engineering|research)\./.test(domain) ||
@@ -718,7 +759,9 @@ function firstPartyCandidate(result = {}) {
718
759
  brandedOfficialArtifact ||
719
760
  cdnOfficialArtifact ||
720
761
  discoveryIntentMatchesBrand ||
721
- discoveryIntentLooksTechnical
762
+ discoveryIntentLooksTechnical ||
763
+ queryNamesDomainBrand ||
764
+ queryNamesProductRoot
722
765
  );
723
766
  }
724
767
 
@@ -737,9 +780,9 @@ function lowEvidenceCandidate(result = {}) {
737
780
  );
738
781
  }
739
782
 
740
- function officialSource(result = {}) {
783
+ function officialSource(result = {}, discoveryQuery = result.discoveryQuery || "") {
741
784
  if (repositoryCandidate(result)) return false;
742
- return scholarlyCandidate(result) || firstPartyCandidate(result);
785
+ return scholarlyCandidate(result) || firstPartyCandidate(result, discoveryQuery);
743
786
  }
744
787
 
745
788
  const RANKING_STOPWORDS = new Set([
@@ -827,6 +870,9 @@ function rankCandidates(searches, policy, query) {
827
870
  ]);
828
871
  existing.workVariantCount = existing.variantUrls.length;
829
872
  existing.scholarlyIndexes = unique([...(existing.scholarlyIndexes || []), ...(result.scholarlyIndexes || []), result.scholarlyIndex]);
873
+ existing.discoveryRoutes = unique([...(existing.discoveryRoutes || []), search.id || resultDiscoveryQuery]);
874
+ existing.firstParty = Boolean(existing.firstParty || firstPartyCandidate(result, resultDiscoveryQuery));
875
+ existing.official = Boolean(existing.official || officialSource(result, resultDiscoveryQuery));
830
876
  if (lexicalHits > Number(existing.lexicalHits || 0)) {
831
877
  existing.lexicalHits = lexicalHits;
832
878
  existing.queryTermCount = queryTermCount;
@@ -834,9 +880,9 @@ function rankCandidates(searches, policy, query) {
834
880
  continue;
835
881
  }
836
882
  let score = lexicalHits;
837
- const official = officialSource(result);
883
+ const official = officialSource(result, resultDiscoveryQuery);
838
884
  const scholarly = scholarlyCandidate(result);
839
- const firstParty = firstPartyCandidate(result);
885
+ const firstParty = firstPartyCandidate(result, resultDiscoveryQuery);
840
886
  const repository = repositoryCandidate(result);
841
887
  const discoveryIndex = discoveryIndexCandidate(result);
842
888
  const lowEvidence = lowEvidenceCandidate(result);
@@ -867,6 +913,7 @@ function rankCandidates(searches, policy, query) {
867
913
  providerConsensusCount: providers.length,
868
914
  domainHint: search.domainHint || "",
869
915
  discoveredBy: [resultDiscoveryQuery],
916
+ discoveryRoutes: [search.id || resultDiscoveryQuery],
870
917
  workIdentity: result.workIdentity || scholarlyWorkIdentity(result),
871
918
  workAliases: aliases,
872
919
  variantUrls: unique(result.variantUrls || [result.url]),
@@ -945,6 +992,34 @@ function selectDiverseCandidates(candidates, limit, policy = "primary", requirem
945
992
  domainCounts.set(candidate.domain || "", 1);
946
993
  }
947
994
  }
995
+ // Preserve topical breadth from the planner without allowing one marginal
996
+ // page per query to fill the whole source budget. Reserve at most half of the
997
+ // budget for strong route leaders, then let global quality and domain
998
+ // diversity choose the remainder.
999
+ const routeLeaderBudget = Math.max(0, Math.min(selectionLimit - selected.length, Math.ceil(selectionLimit / 2)));
1000
+ if (routeLeaderBudget > 0) {
1001
+ const routeLeaders = new Map();
1002
+ for (const candidate of remaining) {
1003
+ if (strictPolicy && !preferred.includes(candidate)) continue;
1004
+ if (!candidateIsStronglyRelevant(candidate) && !(candidate.firstParty && Number(candidate.lexicalHits || 0) >= 1)) continue;
1005
+ for (const route of candidate.discoveryRoutes || candidate.discoveredBy || []) {
1006
+ const current = routeLeaders.get(route);
1007
+ if (!current || Number(candidate.score || 0) > Number(current.score || 0)) routeLeaders.set(route, candidate);
1008
+ }
1009
+ }
1010
+ const leaders = [...new Set(routeLeaders.values())]
1011
+ .filter((candidate) => !selected.some((item) => item.url === candidate.url))
1012
+ .sort((left, right) => Number(right.firstParty) - Number(left.firstParty) || Number(right.score || 0) - Number(left.score || 0))
1013
+ .slice(0, routeLeaderBudget);
1014
+ for (const leader of leaders) {
1015
+ const index = remaining.findIndex((candidate) => candidate.url === leader.url);
1016
+ if (index < 0 || selected.length >= selectionLimit) continue;
1017
+ const [candidate] = remaining.splice(index, 1);
1018
+ const repeatedDomainCount = domainCounts.get(candidate.domain || "") || 0;
1019
+ selected.push({ ...candidate, diversityAdjustedScore: Number(candidate.score || 0) - repeatedDomainCount * 4 });
1020
+ domainCounts.set(candidate.domain || "", repeatedDomainCount + 1);
1021
+ }
1022
+ }
948
1023
  while (remaining.length && selected.length < selectionLimit) {
949
1024
  let bestIndex = 0;
950
1025
  let bestAdjustedScore = Number.NEGATIVE_INFINITY;
@@ -1030,6 +1105,7 @@ async function readCandidates(candidates, args, config, budget, startIndex = 0)
1030
1105
  firstParty: Boolean(candidate.firstParty),
1031
1106
  score: candidate.score,
1032
1107
  discoveredBy: candidate.discoveredBy || [],
1108
+ discoveryRoutes: candidate.discoveryRoutes || [],
1033
1109
  providers: candidate.providers || [],
1034
1110
  providerConsensusCount: candidate.providerConsensusCount || 0,
1035
1111
  scholarlyIndexes: candidate.scholarlyIndexes || [],
@@ -1516,6 +1592,7 @@ function deterministicSynthesis(query, plan, sources, evidence, audit) {
1516
1592
  sourceIds: finding.sourceIds,
1517
1593
  confidence: finding.confidence,
1518
1594
  })),
1595
+ recommendations: [],
1519
1596
  contradictions: [],
1520
1597
  uncertainties: unique([...missingQuestions, ...limitations]),
1521
1598
  nextQuestions: (audit.missingQuestions || []).map((question) => conciseLine(question.question, 320)),
@@ -1549,6 +1626,9 @@ async function synthesize(query, plan, sources, evidence, audit, requirements, a
1549
1626
  requirements.includeNegativeEvidence
1550
1627
  ? "The evidence contract explicitly requires negative or conflicting evidence; preserve it in the relevant section, contradictions, or uncertainties."
1551
1628
  : "Preserve material negative evidence when supplied.",
1629
+ requirements.requireRecommendations
1630
+ ? "The request requires practical recommendations. Return a non-empty recommendations array; each recommendation must be concrete and cite the exact evidenceIds that justify it."
1631
+ : "Add recommendations only when the verified evidence supports a useful action.",
1552
1632
  "Prefer primary sources and give dates when claims may change over time.",
1553
1633
  "Do not cite a source that does not support the sentence.",
1554
1634
  ].join(" "),
@@ -1639,6 +1719,7 @@ export function auditResearchSynthesis(synthesis, sources, evidence = null) {
1639
1719
  }))
1640
1720
  .filter((section) => section.paragraphs.length);
1641
1721
  synthesis.keyFindings = (synthesis.keyFindings || []).filter((finding) => acceptStatement(finding));
1722
+ synthesis.recommendations = (synthesis.recommendations || []).filter((recommendation) => acceptStatement(recommendation));
1642
1723
 
1643
1724
  if (!summaryAccepted) {
1644
1725
  const supported = [
@@ -1687,7 +1768,7 @@ function renderMarkdown(state) {
1687
1768
  const lines = [
1688
1769
  `# ${synthesis.title || `Research report: ${state.query}`}`,
1689
1770
  "",
1690
- `**Question:** ${state.query}`,
1771
+ `**Research objective:** ${compact(state.plan?.objective || state.query, 480).replace(/\s+/g, " ")}`,
1691
1772
  `**As of:** ${state.updatedAt}`,
1692
1773
  `**Depth:** ${state.budget.depth}`,
1693
1774
  "",
@@ -1709,21 +1790,55 @@ function renderMarkdown(state) {
1709
1790
  }
1710
1791
  lines.push("");
1711
1792
  }
1793
+ if (synthesis.recommendations?.length) {
1794
+ lines.push("## Practical Recommendations", "");
1795
+ for (const recommendation of synthesis.recommendations) {
1796
+ lines.push(`- ${recommendation.text} ${citationText(recommendation.sourceIds)}`.trim());
1797
+ }
1798
+ lines.push("");
1799
+ }
1712
1800
  if (synthesis.contradictions?.length) {
1713
1801
  lines.push("## Contradictions", "", ...synthesis.contradictions.map((item) => `- ${item}`), "");
1714
1802
  }
1715
- if (synthesis.uncertainties?.length || state.coverage.missingQuestions?.length) {
1716
- lines.push("## Uncertainties And Coverage Gaps", "");
1717
- for (const item of unique([
1803
+ {
1804
+ lines.push("## Limitations, Uncertainties, And Coverage Gaps", "");
1805
+ const limitations = unique([
1718
1806
  ...(synthesis.uncertainties || []),
1719
1807
  ...(state.coverage.missingQuestions || []).map((question) => `Unresolved: ${question.question}`),
1720
- ])) {
1808
+ ]);
1809
+ if (!limitations.length) {
1810
+ limitations.push("No additional limitation was extracted as a verified claim; consult the source and coverage audit before generalizing the findings.");
1811
+ }
1812
+ for (const item of limitations) {
1721
1813
  lines.push(`- ${item}`);
1722
1814
  }
1723
1815
  lines.push("");
1724
1816
  }
1725
- lines.push("## Sources", "");
1726
- for (const source of state.sources) {
1817
+ if (state.requirements?.requireVerificationMethod) {
1818
+ lines.push(
1819
+ "## Reproducible Verification Method",
1820
+ "",
1821
+ "1. Re-run the recorded research objective with the same source policy, date boundary, and query/source budgets.",
1822
+ "2. Open each cited exact URL and compare the retrieved timestamp and SHA-256 digest recorded below.",
1823
+ "3. Locate every evidence-appendix quotation verbatim in the corresponding source; exclude any quotation that no longer matches.",
1824
+ "4. Recompute source independence, first-party coverage, citation coverage, and unresolved questions before accepting the recommendations.",
1825
+ ""
1826
+ );
1827
+ }
1828
+ if (state.requirements?.includeEvidenceAppendix) {
1829
+ lines.push("## Verified Evidence Appendix", "");
1830
+ for (const item of state.evidence || []) {
1831
+ for (const claim of item.claims || []) {
1832
+ if (!claim.quoteVerified) continue;
1833
+ lines.push(`- **${claim.evidenceId} (${claim.confidence || "confidence not rated"})**: “${claim.quote}” [${item.sourceId}]`);
1834
+ }
1835
+ }
1836
+ lines.push("");
1837
+ }
1838
+ const verifiedSources = state.sources.filter((source) => verifiedSourceIds.has(source.id));
1839
+ const otherSources = state.sources.filter((source) => !verifiedSourceIds.has(source.id));
1840
+ lines.push("## Verified Sources", "");
1841
+ for (const source of verifiedSources) {
1727
1842
  const labels = [
1728
1843
  source.firstParty ? "first-party official" : "",
1729
1844
  source.scholarly ? "scholarly" : "",
@@ -1736,7 +1851,19 @@ function renderMarkdown(state) {
1736
1851
  : "no verified evidence",
1737
1852
  ].filter(Boolean).join(", ");
1738
1853
  const pdf = source.pdfTextExtracted && source.pdfUrl ? `; [parsed PDF](${source.pdfUrl})` : "";
1739
- lines.push(`- [${source.id}] [${source.title}](${source.url})${labels ? ` — ${labels}` : ""}${source.publishedAt ? `; ${source.publishedAt}` : ""}${pdf}`);
1854
+ const digest = source.sha256 ? `; SHA-256 ${source.sha256}` : "";
1855
+ lines.push(`- [${source.id}] [${source.title}](${source.url})${labels ? ` — ${labels}` : ""}${source.publishedAt ? `; ${source.publishedAt}` : ""}${source.retrievedAt ? `; retrieved ${source.retrievedAt}` : ""}${digest}${pdf}`);
1856
+ }
1857
+ if (otherSources.length) {
1858
+ lines.push("", "## Sources Inspected But Not Cited", "");
1859
+ for (const source of otherSources) {
1860
+ const status = searchExcerptSourceIds.has(source.id)
1861
+ ? "search-excerpt-only, not cited"
1862
+ : source.readable
1863
+ ? "readable but no quote-verified claim"
1864
+ : `unreadable: ${source.readError || "no readable content"}`;
1865
+ lines.push(`- [${source.id}] [${source.title}](${source.url}) — ${status}`);
1866
+ }
1740
1867
  }
1741
1868
  lines.push(
1742
1869
  "",
@@ -1930,6 +2057,7 @@ export async function deepResearch(args = {}, config = {}, store = null) {
1930
2057
 
1931
2058
  if (existing?.status === "failed") {
1932
2059
  state.status = "running";
2060
+ const previousError = String(state.error || "");
1933
2061
  delete state.error;
1934
2062
  if (!state.sources?.length) {
1935
2063
  state.searches = [];
@@ -1939,6 +2067,10 @@ export async function deepResearch(args = {}, config = {}, store = null) {
1939
2067
  state.audit = {};
1940
2068
  state.gapPassCompleted = false;
1941
2069
  }
2070
+ if (/requested practical recommendations/i.test(previousError)) {
2071
+ state.synthesis = null;
2072
+ state.audit = {};
2073
+ }
1942
2074
  }
1943
2075
 
1944
2076
  try {
@@ -2155,6 +2287,9 @@ export async function deepResearch(args = {}, config = {}, store = null) {
2155
2287
  state.stage = "synthesized";
2156
2288
  await saveState(state, paths, store);
2157
2289
  }
2290
+ if (requirements.requireRecommendations && !state.synthesis.recommendations?.length) {
2291
+ throw new Error("The requested practical recommendations were not supported by verified evidence.");
2292
+ }
2158
2293
  state.status = "completed";
2159
2294
  state.stage = "completed";
2160
2295
  await saveState(state, paths, store);