Skip to content

Agent Pipeline

The pipeline is a strict linear sequence: each agent receives the accumulated SermonDraft state and adds its own output before passing control to the next. No stage runs until the previous one has successfully completed.

Stage sequence

sequenceDiagram
    participant C as Client (browser)
    participant A as API (FastAPI)
    participant O as Orchestrator
    participant P as Planner
    participant R as Research
    participant E as Exegesis
    participant S as Structure
    participant CR as Critique
    participant DB as PostgreSQL

    C->>A: POST /sermons {"topic": "..."}
    A->>O: prepare_sermon_stream(topic, session)

    O-->>C: SSE: {stage:"planner", status:"started"}
    O->>P: run_planner(deps)
    P-->>O: SermonPlan
    O-->>C: SSE: {stage:"planner", status:"completed", payload:{...}}

    O-->>C: SSE: {stage:"research", status:"started"}
    O->>R: run_research(deps)
    R->>DB: hybrid_search(passages, commentary, cultural)
    R-->>O: ResearchBrief
    O-->>C: SSE: {stage:"research", status:"completed", payload:{...}}

    O-->>C: SSE: {stage:"exegesis", status:"started"}
    O->>E: run_exegesis(deps)
    E->>DB: hybrid_search(passages, commentary)
    E-->>O: ExegesisReport
    O-->>C: SSE: {stage:"exegesis", status:"completed", payload:{...}}

    O-->>C: SSE: {stage:"structure", status:"started"}
    O->>S: run_structure(deps)
    S-->>O: SermonOutline
    O-->>C: SSE: {stage:"structure", status:"completed", payload:{...}}

    O-->>C: SSE: {stage:"critique", status:"started"}
    O->>CR: run_critique(deps)
    CR-->>O: CritiqueReport
    O-->>C: SSE: {stage:"critique", status:"completed", payload:{...}}

    O-->>C: SSE: {stage:"brief", status:"completed", payload:{markdown:"..."}}

Stages

Stage 1 — Planner

Agent: agents/planner.py
Output model: SermonPlan
Stored on: SermonDraft.plan

The Planner receives only the raw topic string. It selects a passage_reference to anchor the sermon, defines the audience and occasion, formulates a main_question, and produces goal lists for the research, exegesis, and structure stages. No retrieval tools — the Planner operates entirely on the LLM's knowledge.

Stage 2 — Research

Agent: agents/research.py
Output model: ResearchFindings
Stored on: SermonDraft.research
Tools: hybrid_search (bible, commentary, cultural), lookup_strongs_tool

The Research agent receives the plan and a pre-fetched ResearchBrief (corpus results assembled by retrieval/research.py). It synthesises the raw retrieval results into structured findings: passage_summary, key_themes, notable_cross_references, commentary_highlights, word_study_notes, and open_questions.

Stage 3 — Exegesis

Agent: agents/exegesis.py
Output model: ExegesisNotes
Stored on: SermonDraft.exegesis
Tools: hybrid_search (bible, commentary), lexicon_tool, cross_reference_tool

The Exegesis agent performs deep analysis of the key scripture identified by the Planner. It pulls lexicon entries for significant Greek/Hebrew words (via Strong's numbers) and uses commentary excerpts to ground its interpretation historically and grammatically. It produces ExegesisNotes with fields: main_idea, literary_context, theological_moves, key_word_studies (each a WordStudy with Strong's number), and application_principles.

Stage 4 — Structure

Agent: agents/structure.py
Output model: SermonOutline
Stored on: SermonDraft.outline

The Structure agent receives the complete accumulated draft — plan, research brief, and exegesis report — and organises the material into a coherent sermon outline. It produces sections with headings, transition text, and illustrative suggestions. No retrieval tools; all input comes from prior stages.

Stage 5 — Critique

Agent: agents/critique.py
Output model: CritiqueReport
Stored on: SermonDraft.critique

The Critique agent reviews the entire draft from start to finish. It evaluates theological accuracy, structural coherence, and practical applicability. It produces a list of strengths and specific improvement suggestions. This is the final agent before brief rendering.

Brief rendering

After all five stages complete, render_brief() in agents/brief.py assembles the SermonDraft into a Markdown document. The final StageEvent with stage="brief" carries the full markdown in its payload.

State flow

flowchart TD
    T[topic: str] --> DRAFT[SermonDraft]
    DRAFT --> P[run_planner → .plan]
    P --> R[run_research → .research]
    R --> E[run_exegesis → .exegesis]
    E --> S[run_structure → .outline]
    S --> C[run_critique → .critique]
    C --> B[render_brief → SermonBrief.markdown]

SermonDraft (agents/state.py) is the shared mutable accumulator. All five agents receive an AgentDeps object that holds a reference to the same draft instance.

Stage events

All events are JSON-serialised StageEvent objects emitted as SSE data: lines.

class StageEvent(BaseModel):
    stage: Literal["planner", "research", "exegesis", "structure", "critique", "brief", "error"]
    status: Literal["started", "completed", "failed"]
    elapsed_ms: int                  # milliseconds since stage start
    payload: dict | None             # stage output (on completed) or error detail (on failed)
    sources: list[dict] | None       # RetrievalResult dicts; present on research completed events

sources carries the raw retrieval results that fed the stage — each entry is a serialised RetrievalResult with id, source_type, reference, text, score, and metadata. The IDs are stable and designed as the attachment point for inline per-claim citations in a future iteration.

Event sequence for a successful run

# stage status payload
1 planner started null
2 planner completed SermonPlan dict
3 research started null
4 research completed ResearchFindings dict
5 exegesis started null
6 exegesis completed ExegesisNotes dict
7 structure started null
8 structure completed SermonOutline dict
9 critique started null
10 critique completed CritiqueReport dict
11 brief completed SermonBrief dict (includes markdown)

Error events

If any stage throws, the orchestrator emits a failed event and stops the stream:

{
  "stage": "research",
  "status": "failed",
  "elapsed_ms": 3421,
  "payload": {
    "error": "Connection refused",
    "type": "ConnectionError"
  }
}

No subsequent stages run after a failure. The SSE stream closes cleanly.

Observability

Every stage is wrapped in a Logfire span:

sermon_pipeline (root)
├── stage.planner
├── stage.research
│   └── hybrid_search  ← retrieval span
│       └── embed_query
├── stage.exegesis
│   └── hybrid_search
├── stage.structure
└── stage.critique

Each Agent.run() call is auto-traced by logfire.instrument_pydantic_ai(), capturing model name, token counts, and all tool invocations.