From 1d7a7ba0c788c31f8c14518b66f51eeb77c9f7d8 Mon Sep 17 00:00:00 2001 From: iamjr15 Date: Wed, 5 Aug 2026 03:54:10 +0530 Subject: [PATCH] fix(agent): stabilize research structured output --- .../workflows/deep-research-workflow.ts | 6 ++++ .../mastra/workflows/research-provenance.ts | 29 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) 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 ac13f4f5..cffdc603 100644 --- a/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts +++ b/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts @@ -31,6 +31,9 @@ const RESEARCH_QUERY_CONCURRENCY = 3; const RESEARCH_RESULTS_PER_QUERY = 6; const RESEARCH_RESULT_TEXT_CHARACTERS = 2_500; const RESEARCH_SCRAPE_CHARACTERS = 12_000; +const RESEARCH_PROVIDER_OPTIONS = { + anthropic: { structuredOutputMode: "outputFormat" as const }, +}; interface ResearchEvidenceSource { content: string; @@ -143,6 +146,7 @@ function createQueryStep(id: string, config: ResearchWorkflowPrompts) { const response = await agent.generate(researchPassPrompt(config, inputData.query, evidence), { activeTools: [], abortSignal, + providerOptions: RESEARCH_PROVIDER_OPTIONS, requestContext: research.requestContext, structuredOutput: { schema: ResearchPassDraftSchema }, }); @@ -167,6 +171,7 @@ function createSynthesisStep(id: string, config: ResearchWorkflowPrompts) { const response = await agent.generate(config.synthesisPrompt(inputData), { activeTools: [], abortSignal, + providerOptions: RESEARCH_PROVIDER_OPTIONS, requestContext, structuredOutput: { schema: ResearchSynthesisDraftSchema }, }); @@ -283,6 +288,7 @@ function researchPassPrompt( return [ 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.", "Do not cite sourceId directly and do not add sources that are absent from this evidence pack.", "", JSON.stringify(evidence, null, 2), diff --git a/packages/agent-core/src/mastra/workflows/research-provenance.ts b/packages/agent-core/src/mastra/workflows/research-provenance.ts index 22738e47..8e0343b8 100644 --- a/packages/agent-core/src/mastra/workflows/research-provenance.ts +++ b/packages/agent-core/src/mastra/workflows/research-provenance.ts @@ -24,11 +24,17 @@ const SourceReferenceSchema = z.discriminatedUnion("provider", [ }), ]); +const SourceReferenceDraftSchema = z.strictObject({ + provider: z.enum(["exa", "firecrawl"]), + providerResultId: z.string().trim().max(500), + url: z.string().trim().min(1), +}); + export const ResearchPassDraftSchema = z.strictObject({ claims: z.array( z.strictObject({ claim: z.string().trim().min(1), - sources: z.array(SourceReferenceSchema).min(1), + sources: z.array(SourceReferenceDraftSchema).min(1), }), ), summary: z.string().trim().min(1), @@ -45,6 +51,7 @@ export const ResearchSynthesisDraftSchema = z.strictObject({ }); type SourceReference = z.infer; +type SourceReferenceDraft = z.infer; type ResearchPassDraft = z.infer; interface EvidenceCollector { @@ -81,7 +88,11 @@ export function validateResearchPass( const citedSources = new Map(); const claims = draft.claims.map((claim) => ({ claim: claim.claim, - sourceIds: resolveClaimSources(claim.sources, collector, citedSources), + sourceIds: resolveClaimSources( + claim.sources.map(sourceReferenceFromDraft), + collector, + citedSources, + ), })); return ResearchFindingSchema.parse({ claims, @@ -91,6 +102,20 @@ export function validateResearchPass( }); } +function sourceReferenceFromDraft(draft: SourceReferenceDraft): SourceReference { + if (draft.provider === "exa") { + return SourceReferenceSchema.parse({ + provider: draft.provider, + providerResultId: draft.providerResultId, + url: draft.url, + }); + } + if (draft.providerResultId.length > 0) { + throw new Error("A Firecrawl citation cannot include an Exa result identifier."); + } + return SourceReferenceSchema.parse({ provider: draft.provider, url: draft.url }); +} + export function validateSynthesisClaims( claims: ResearchClaim[], sources: ResearchSource[],