Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createResearchStepContext,
exaSource,
firecrawlSource,
mergeResearchClaims,
mergeResearchSources,
ResearchPassDraftSchema,
ResearchSynthesisDraftSchema,
Expand Down Expand Up @@ -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: [],
Expand All @@ -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,
Expand Down Expand Up @@ -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.",
"",
Expand All @@ -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.",
Expand Down
3 changes: 1 addition & 2 deletions packages/agent-core/src/mastra/workflows/deep-research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
30 changes: 19 additions & 11 deletions packages/agent-core/src/mastra/workflows/research-provenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});

Expand Down Expand Up @@ -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<string, ResearchClaim>();
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({
Expand Down