diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index f2857845..4a10318e 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -69,10 +69,10 @@ Each concurrent research pass gets an isolated evidence collector populated only from one bounded Exa discovery call and an optional Firecrawl extraction of its primary result. A single tool-free model pass structures each byte-bounded provider evidence pack. Nested model calls use stage-appropriate output bounds, an operational -timeout, and one in-memory retry for transient provider 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. +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. 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-workflow.ts b/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts index 6762d3a5..03de2cb1 100644 --- a/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts +++ b/packages/agent-core/src/mastra/workflows/deep-research-workflow.ts @@ -148,21 +148,24 @@ function createQueryStep(id: string, config: ResearchWorkflowPrompts) { research.requestContext, abortSignal, ); - const response = await generateResearchOutput(abortSignal, (generationSignal) => - agent.generate(researchPassPrompt(config, inputData.query, evidence), { - activeTools: [], - abortSignal: generationSignal, - modelSettings: { maxOutputTokens: RESEARCH_PASS_MAX_OUTPUT_TOKENS }, - providerOptions: RESEARCH_PROVIDER_OPTIONS, - requestContext: research.requestContext, - structuredOutput: { schema: ResearchPassDraftSchema }, - }), - ); - return validateResearchPass( - parseResearchPassDraft(response.object), - inputData.query, - research.collector, - ); + return generateResearchOutput(abortSignal, async (generationSignal) => { + const response = await agent.generate( + researchPassPrompt(config, inputData.query, evidence), + { + activeTools: [], + abortSignal: generationSignal, + modelSettings: { maxOutputTokens: RESEARCH_PASS_MAX_OUTPUT_TOKENS }, + providerOptions: RESEARCH_PROVIDER_OPTIONS, + requestContext: research.requestContext, + structuredOutput: { schema: ResearchPassDraftSchema }, + }, + ); + return validateResearchPass( + parseResearchPassDraft(response.object), + inputData.query, + research.collector, + ); + }); }, }); } @@ -176,22 +179,22 @@ function createSynthesisStep(id: string, config: ResearchWorkflowPrompts) { execute: async ({ abortSignal, inputData, mastra, requestContext }) => { const agent = mastra.getAgent("general"); const sources = mergeResearchSources(inputData); - const response = await generateResearchOutput(abortSignal, (generationSignal) => - agent.generate(researchSynthesisPrompt(config, inputData), { + return generateResearchOutput(abortSignal, async (generationSignal) => { + const response = await agent.generate(researchSynthesisPrompt(config, inputData), { activeTools: [], abortSignal: generationSignal, modelSettings: { maxOutputTokens: RESEARCH_SYNTHESIS_MAX_OUTPUT_TOKENS }, providerOptions: RESEARCH_PROVIDER_OPTIONS, requestContext, structuredOutput: { schema: ResearchSynthesisDraftSchema }, - }), - ); - const draft = parseResearchSynthesisDraft(response.object); - return ResearchReportSchema.parse({ - claims: validateSynthesisClaims(draft.claims, sources), - findings: inputData, - report: draft.report, - sources, + }); + const draft = parseResearchSynthesisDraft(response.object); + return ResearchReportSchema.parse({ + claims: validateSynthesisClaims(draft.claims, sources), + findings: inputData, + report: draft.report, + sources, + }); }); }, }); @@ -301,7 +304,8 @@ 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 4-6 distinct, synthesis-ready claims, no more than 3 sources per claim, and a concise summary. Prioritize the strongest guidance instead of exhaustively restating the evidence.", + "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.", + "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.", "", JSON.stringify(evidence, null, 2), @@ -315,7 +319,7 @@ function researchSynthesisPrompt( return [ config.synthesisPrompt(findings), "Consolidate overlapping evidence into at most 16 distinct claims with no more than 4 source IDs per claim.", - "Keep the report focused and complete within 2,000 words while retaining actionable findings and citations.", + "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.", ].join("\n"); @@ -381,8 +385,19 @@ function isRetriableModelError(error: unknown): boolean { if (error.name === "TimeoutError") { return true; } - const record = error as Error & { isRetryable?: unknown; statusCode?: unknown }; - if (record.isRetryable === true) { + const record = error as Error & { + code?: unknown; + id?: unknown; + isRetryable?: unknown; + retriable?: unknown; + statusCode?: unknown; + }; + if ( + record.isRetryable === true || + record.retriable === true || + record.id === "STRUCTURED_OUTPUT_SCHEMA_VALIDATION_FAILED" || + record.code === "STRUCTURED_OUTPUT_SCHEMA_VALIDATION_FAILED" + ) { return true; } return ( diff --git a/packages/agent-core/src/mastra/workflows/research-provenance.ts b/packages/agent-core/src/mastra/workflows/research-provenance.ts index 4e2cf956..feeddead 100644 --- a/packages/agent-core/src/mastra/workflows/research-provenance.ts +++ b/packages/agent-core/src/mastra/workflows/research-provenance.ts @@ -34,26 +34,26 @@ export const ResearchPassDraftSchema = z.strictObject({ claims: z .array( z.strictObject({ - claim: z.string().trim().min(1), - sources: z.array(SourceReferenceDraftSchema).min(1).max(3), + claim: z.string().trim().min(1).max(600), + sources: z.array(SourceReferenceDraftSchema).min(1).max(2), }), ) .min(1) - .max(6), - summary: z.string().trim().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), + 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), + report: z.string().trim().min(1).max(20_000), }); type SourceReference = z.infer;