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.