diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index 4a10318e..6dab5f92 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -71,8 +71,10 @@ primary result. A single tool-free model pass structures each byte-bounded provi evidence pack. Nested model calls use stage-appropriate output bounds, an operational timeout, and one in-memory retry for transient provider or invalid structured-output failures; request cancellation always wins and no secret-bearing state is snapshotted. -Claim citations and the final synthesis are schema-validated against that evidence; -prose URL scraping is not an accepted provenance boundary. +Claim citations are schema-validated against that evidence, and the durable claim map +is assembled deterministically from those validated passes. The model-authored final +synthesis contains only the canonical Markdown report; prose URL scraping is not an +accepted provenance boundary. Successful top-level deep-research and fan-out tools render the validated report's canonical GitHub-flavored Markdown directly into a PDF artifact. The chat response and PDF therefore preserve the same headings, prose, lists, tables, links, citations, diff --git a/packages/agent-core/src/mastra/workflows/deep-research-fanout.ts b/packages/agent-core/src/mastra/workflows/deep-research-fanout.ts index 5f06f44a..ceb58ae0 100644 --- a/packages/agent-core/src/mastra/workflows/deep-research-fanout.ts +++ b/packages/agent-core/src/mastra/workflows/deep-research-fanout.ts @@ -22,8 +22,7 @@ function fanoutSynthesisPrompt(findings: unknown): string { return [ "Synthesize the following parallel research findings into a comparison-oriented report.", "Include a comparison matrix when the findings cover multiple entities.", - "The claim sourceIds must exactly match IDs in the input sources. Do not invent or rewrite IDs.", - "The structured claim map is authoritative provenance; keep the report readable and evidence-bound.", + "Use only the source URLs present in the findings, and keep the report readable and evidence-bound.", "", JSON.stringify(findings, null, 2), ].join("\n"); diff --git a/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts b/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts index 03de2cb1..ded9c6b3 100644 --- a/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts +++ b/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts @@ -11,6 +11,7 @@ import { createResearchStepContext, exaSource, firecrawlSource, + mergeResearchClaims, mergeResearchSources, ResearchPassDraftSchema, ResearchSynthesisDraftSchema, @@ -179,6 +180,7 @@ function createSynthesisStep(id: string, config: ResearchWorkflowPrompts) { execute: async ({ abortSignal, inputData, mastra, requestContext }) => { const agent = mastra.getAgent("general"); const sources = mergeResearchSources(inputData); + const claims = validateSynthesisClaims(mergeResearchClaims(inputData), sources); return generateResearchOutput(abortSignal, async (generationSignal) => { const response = await agent.generate(researchSynthesisPrompt(config, inputData), { activeTools: [], @@ -190,7 +192,7 @@ function createSynthesisStep(id: string, config: ResearchWorkflowPrompts) { }); const draft = parseResearchSynthesisDraft(response.object); return ResearchReportSchema.parse({ - claims: validateSynthesisClaims(draft.claims, sources), + claims, findings: inputData, report: draft.report, sources, @@ -304,7 +306,7 @@ function researchPassPrompt( config.queryPrompt(query), "Use only the provider evidence below. For Exa citations, copy providerResultId and URL exactly. For Firecrawl citations, copy the URL exactly.", "Set providerResultId to an empty string for every Firecrawl citation.", - "Return 3-4 distinct, synthesis-ready claims. Keep each claim under 450 characters, use no more than 2 sources per claim, and keep the summary under 700 characters.", + "Return only 3-4 distinct, synthesis-ready claims. Keep each claim under 450 characters and use no more than 2 sources per claim.", "Prioritize the strongest guidance instead of exhaustively restating the evidence.", "Do not cite sourceId directly and do not add sources that are absent from this evidence pack.", "", @@ -318,7 +320,7 @@ function researchSynthesisPrompt( ): string { return [ config.synthesisPrompt(findings), - "Consolidate overlapping evidence into at most 16 distinct claims with no more than 4 source IDs per claim.", + "Return only the report field requested by the output schema; the provenance index is assembled deterministically from the validated findings.", "Keep the report focused and complete within 1,200 words while retaining actionable findings and citations.", "Write report as polished GitHub-flavored Markdown for direct display and PDF rendering. Preserve a clear heading hierarchy, lists, and comparison tables where useful.", "Cite factual claims with descriptive Markdown links to the exact source URLs in the findings, and finish with a Sources heading containing only sources used in the report.", diff --git a/packages/agent-core/src/mastra/workflows/deep-research.ts b/packages/agent-core/src/mastra/workflows/deep-research.ts index a5ded946..20046b01 100644 --- a/packages/agent-core/src/mastra/workflows/deep-research.ts +++ b/packages/agent-core/src/mastra/workflows/deep-research.ts @@ -21,8 +21,7 @@ function deepResearchPrompt(query: string): string { function synthesisPrompt(kind: string, findings: unknown): string { return [ `Synthesize the following findings into a cited ${kind}.`, - "The claim sourceIds must exactly match IDs in the input sources. Do not invent or rewrite IDs.", - "The structured claim map is authoritative provenance; keep the report readable and evidence-bound.", + "Use only the source URLs present in the findings, and keep the report readable and evidence-bound.", "", JSON.stringify(findings, null, 2), ].join("\n"); diff --git a/packages/agent-core/src/mastra/workflows/research-provenance.ts b/packages/agent-core/src/mastra/workflows/research-provenance.ts index feeddead..49003ddf 100644 --- a/packages/agent-core/src/mastra/workflows/research-provenance.ts +++ b/packages/agent-core/src/mastra/workflows/research-provenance.ts @@ -40,19 +40,9 @@ export const ResearchPassDraftSchema = z.strictObject({ ) .min(1) .max(4), - summary: z.string().trim().min(1).max(1_000), }); export const ResearchSynthesisDraftSchema = z.strictObject({ - claims: z - .array( - z.strictObject({ - claim: z.string().trim().min(1).max(1_000), - sourceIds: z.array(z.string().trim().min(1).max(4_096)).min(1).max(4), - }), - ) - .min(1) - .max(16), report: z.string().trim().min(1).max(20_000), }); @@ -104,10 +94,28 @@ export function validateResearchPass( claims, query, sources: [...citedSources.values()], - summary: draft.summary, + summary: claims + .slice(0, 2) + .map((claim) => claim.claim) + .join(" "), }); } +export function mergeResearchClaims(findings: Array<{ claims: ResearchClaim[] }>): ResearchClaim[] { + const claims = new Map(); + for (const finding of findings) { + for (const claim of finding.claims) { + const key = claim.claim.trim().replace(/\s+/g, " ").toLowerCase(); + const existing = claims.get(key); + claims.set(key, { + claim: existing?.claim ?? claim.claim, + sourceIds: [...new Set([...(existing?.sourceIds ?? []), ...claim.sourceIds])].slice(0, 4), + }); + } + } + return [...claims.values()].slice(0, 16); +} + function sourceReferenceFromDraft(draft: SourceReferenceDraft): SourceReference { if (draft.provider === "exa") { return SourceReferenceSchema.parse({