Skip to content

Commit 0c9c638

Browse files
authored
fix(agent): bound generated research queries (#165)
## Why Production QA of the canonical Markdown research PDF flow exposed a mismatch between accepted research topics and generated provider queries. A valid detailed topic could exceed the narrower generated-query schema once an angle suffix was appended, causing `research_deep` to fail validation and the agent to fall back to manual research. ## What changed - align generated research queries with the provider's 2,000-character query boundary - preserve every distinct research angle when a long topic must be shortened - apply the same safe composition to fan-out goals and entity queries - clarify that tool-generated topics should remain concise and focused ## Architecture and migration impact No database, deployment-topology, or public API changes. This is an internal agent workflow validation fix. ## Verification - `pnpm lint` - `pnpm typecheck` - `pnpm turbo build --force` - `pnpm deadcode` - `pnpm architecture:check` - `pnpm turbo skills:build` - targeted 2,000-character deep-research and fan-out query boundary exercise, including distinct-angle and suffix-preservation assertions - production natural-language research flow will be rerun after merge/deploy The local machine reports the existing Node engine warning (repo pins 24.18.0; local Node is 26.4.0); all checks completed successfully.
1 parent f1b0f63 commit 0c9c638

2 files changed

Lines changed: 45 additions & 23 deletions

File tree

packages/agent-core/src/mastra/workflows/research-schemas.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const ResearchReportSchema = z.strictObject({
4141
});
4242

4343
export const ResearchQuerySchema = z.strictObject({
44-
query: z.string().trim().min(1).max(1_000),
44+
query: z.string().trim().min(1).max(2_000),
4545
});
4646

4747
export const DeepResearchInputSchema = z.strictObject({
@@ -54,7 +54,14 @@ export const DeepResearchInputSchema = z.strictObject({
5454
.describe(
5555
"Focused research pass count: use 3 for a concise or narrow report, 4 by default, and 5-6 only when the user explicitly asks for deeper coverage.",
5656
),
57-
topic: z.string().trim().min(1).max(2_000),
57+
topic: z
58+
.string()
59+
.trim()
60+
.min(1)
61+
.max(2_000)
62+
.describe(
63+
"Concise research topic and requested report focus. Preserve the user's requirements without adding unrelated detail.",
64+
),
5865
});
5966

6067
export const DeepResearchFanoutInputSchema = z.strictObject({

packages/agent-core/src/mastra/workflows/research-support.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
const MAX_RESEARCH_QUERY_CHARACTERS = 2_000;
2+
13
export function buildDeepResearchQueries(
24
topic: string,
35
maxQueries: number,
46
): Array<{ query: string }> {
57
const trimmedTopic = topic.trim();
6-
const templates = [
7-
`${trimmedTopic} current landscape overview and authoritative primary sources`,
8-
`${trimmedTopic} technical product and implementation details constraints`,
9-
`${trimmedTopic} risks limitations failure modes criticism`,
10-
`${trimmedTopic} evidence data benchmarks case studies adoption`,
11-
`${trimmedTopic} legal regulatory economic and operational considerations`,
12-
`${trimmedTopic} recent developments future trends disagreements open questions`,
8+
const angles = [
9+
"current landscape overview and authoritative primary sources",
10+
"technical product and implementation details constraints",
11+
"risks limitations failure modes criticism",
12+
"evidence data benchmarks case studies adoption",
13+
"legal regulatory economic and operational considerations",
14+
"recent developments future trends disagreements open questions",
1315
];
14-
return dedupeQueries(templates)
16+
return dedupeQueries(angles.map((angle) => buildResearchQuery(trimmedTopic, angle)))
1517
.slice(0, maxQueries)
1618
.map((query) => ({ query }));
1719
}
@@ -24,27 +26,40 @@ export function buildFanoutQueries(input: {
2426
const goal = input.goal.trim();
2527
if (input.entities && input.entities.length > 0) {
2628
return input.entities.slice(0, input.maxQueries).map((entity) => ({
27-
query: `${goal}: ${entity}`,
29+
query: buildResearchQuery(goal, entity, ": "),
2830
}));
2931
}
3032

31-
const templates = [
32-
`${goal} top entities overview`,
33-
`${goal} company and competitor landscape`,
34-
`${goal} pricing and packaging comparison`,
35-
`${goal} recent news and announcements`,
36-
`${goal} customer segments and use cases`,
37-
`${goal} product capabilities matrix`,
38-
`${goal} funding traction hiring signals`,
39-
`${goal} strengths weaknesses opportunities threats`,
40-
`${goal} market size and growth estimates`,
41-
`${goal} risks and open questions`,
33+
const angles = [
34+
"top entities overview",
35+
"company and competitor landscape",
36+
"pricing and packaging comparison",
37+
"recent news and announcements",
38+
"customer segments and use cases",
39+
"product capabilities matrix",
40+
"funding traction hiring signals",
41+
"strengths weaknesses opportunities threats",
42+
"market size and growth estimates",
43+
"risks and open questions",
4244
];
43-
return dedupeQueries(templates)
45+
return dedupeQueries(angles.map((angle) => buildResearchQuery(goal, angle)))
4446
.slice(0, input.maxQueries)
4547
.map((query) => ({ query }));
4648
}
4749

50+
function buildResearchQuery(subject: string, qualifier: string, separator = " "): string {
51+
const suffix = `${separator}${qualifier}`;
52+
const maxSubjectCharacters = MAX_RESEARCH_QUERY_CHARACTERS - suffix.length;
53+
if (subject.length <= maxSubjectCharacters) {
54+
return `${subject}${suffix}`;
55+
}
56+
const candidate = subject.slice(0, maxSubjectCharacters - 1).trimEnd();
57+
const wordBoundary = candidate.lastIndexOf(" ");
58+
const bounded =
59+
wordBoundary >= maxSubjectCharacters * 0.75 ? candidate.slice(0, wordBoundary) : candidate;
60+
return `${bounded.trimEnd()}${suffix}`;
61+
}
62+
4863
function dedupeQueries(queries: string[]): string[] {
4964
const seen = new Set<string>();
5065
const deduped: string[] = [];

0 commit comments

Comments
 (0)