Docs

Examples

Research Agent

A real-case research workflow with evidence collection, synthesis, citations, and background execution.

A research agent should separate evidence collection from synthesis. Read-only tools gather sources; the final answer explains what is known, what is uncertain, and which sources support each claim.

Scenario

An analyst asks for a market brief. The workflow searches internal notes, reads approved sources, optionally asks specialists, and produces a cited summary as a background report.

Flow

Step Pattern
enqueue report Pipeline Worker
collect sources Retrieval Agent and read-only tools
ask specialists Multi-agent Coordination
synthesize answer Agent Runtime Composition
save report/evidence Runtime State and Persistence

Example

import { AgentBuilder } from "@anvia/core";

const RESEARCH_INSTRUCTIONS = [
  "Collect evidence before writing.",
  "Cite source ids from retrieved notes or search tools.",
  "Separate findings from assumptions.",
  "If sources disagree, explain the disagreement.",
].join("\n");

export function createResearchAgent(scope: ResearchAgentScope) {
  return new AgentBuilder("research", scope.model)
    .instructions(RESEARCH_INSTRUCTIONS)
    .tools([
      createSearchInternalNotesTool(scope),
      createReadApprovedSourceTool(scope),
      createAskPolicySpecialistTool(scope),
    ])
    .defaultMaxTurns(8)
    .build();
}

export async function runResearchBrief(input: ResearchBriefInput) {
  const user = await input.auth.requireUser();
  const agent = createResearchAgent({
    model: input.model,
    user,
    sources: input.sources,
    notesIndex: input.notesIndex,
  });

  const prompt = [
    `Research topic: ${input.topic}`,
    `Audience: ${input.audience}`,
    "Return a brief with findings, assumptions, and cited source ids.",
  ].join("\n");

  const response = await agent
    .prompt(prompt)
    .withTrace({
      name: "research-brief",
      userId: user.id,
      metadata: {
        tenantId: user.tenantId,
        reportId: input.reportId,
        topic: input.topic,
      },
    })
    .send();

  await input.reports.save({
    reportId: input.reportId,
    topic: input.topic,
    output: response.output,
    requestedBy: user.id,
    traceId: response.trace?.traceId,
  });

  return { output: response.output };
}

Source Tool Boundary

const readApprovedSource = createTool({
  name: "read_approved_source",
  description: "Read an approved source by id.",
  input: z.object({ sourceId: z.string() }),
  output: z.object({
    sourceId: z.string(),
    title: z.string(),
    excerpt: z.string(),
  }),
  execute: ({ sourceId }) =>
    scope.sources.readForUser({
      sourceId,
      userId: scope.user.id,
      tenantId: scope.user.tenantId,
    }),
});

Research tools should be read-only unless the page is explicitly about publishing or filing a report.

Failure Modes

  • Search tools can write or mutate data.
  • Final synthesis omits source ids.
  • The agent treats weak search snippets as confirmed facts.
  • Long research is run in the request path instead of a worker.
  • Evidence is saved only inside prose and cannot be audited.

Next Patterns