Patterns¶
Runnable examples in examples/. Diagrams auto-generated from each example's graph.namespaces() (always in sync with code).
| If you need⦠| See | Related docs |
|---|---|---|
| Invariants + pinned reactions | Order | control-flow |
| Human-in-the-loop approval | Expense | control-flow |
| Tool-calling agent + AG-UI streaming | Conversation | agui, reducers |
| Supervisor loop / fan-in | Task | reducers |
| Scatter fan-out / map-reduce | Batch | control-flow |
| Safety gates + live streaming | Content | streaming, concepts |
Retries + escalation via raises= |
Question | control-flow |
ποΈ Diagram vocabulary
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Example["Namespace"]
direction LR
Command{{Command}}:::cmd
DomainEvent(DomainEvent):::devt
Halted([Halted]):::halt
Invariant{Invariant}:::inv
Rejected(Rejected):::devt
end
IntegrationEvent[/IntegrationEvent/]:::intg
SystemEvent([SystemEvent]):::syst
_seed_[ ]:::entry ==> Command
Command --> DomainEvent
Command -.->|"(raises)"| SystemEvent
Command -.->|scatter| IntegrationEvent
Command -.- Halted
Command -.->|invariant| Invariant -.->|reactor| Rejected
DomainEvent -->|"reactor [orchestrate]"| Command
Command -->|"[chain]"| Command
linkStyle 1 stroke:#6b7280,stroke-dasharray:3 3
linkStyle 2 stroke:#7c3aed,stroke-width:2.5px,stroke-dasharray:8 3
linkStyle 3 stroke:#9ca3af,stroke-dasharray:3 3
linkStyle 5,6 stroke:#c2410c,stroke-dasharray:4 2
linkStyle 7 stroke:#0369a1,stroke-width:3px
linkStyle 8 stroke:#b91c1c,stroke-width:2px,stroke-dasharray:5 3
Invariants & reactions (Order namespace)¶
- Commands:
Place,Ship. Outcomes:Placed,Rejected,Shipped. - Invariants:
CustomerNotBanned,OrderTotalWithinLimit(typed, with pinned reactors turning violations into namespace-levelOrder.Rejected). - State:
current_status(ScalarReduceras domain attribute).
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Order["Order namespace"]
direction LR
Place{{Place}}:::cmd
Placed(Placed):::devt
Rejected(Rejected):::devt
Ship{{Ship}}:::cmd
Shipped(Shipped):::devt
CustomerNotBanned{CustomerNotBanned}:::inv
OrderTotalWithinLimit{OrderTotalWithinLimit}:::inv
end
_e0_[ ]:::entry ==> Place
_e1_[ ]:::entry ==> Ship
Place --> Placed
Ship --> Shipped
CustomerNotBanned -.->|explain_banned| Rejected
OrderTotalWithinLimit -.->|explain_over_limit| Rejected
Place -.->|invariant| CustomerNotBanned
Place -.->|invariant| OrderTotalWithinLimit
linkStyle 4,5,6,7 stroke:#c2410c,stroke-dasharray:4 2
Namespaces:
Order
Command: Place (invariant: CustomerNotBanned, OrderTotalWithinLimit)
β Placed
Command: Ship
β Shipped
Event: Rejected
System events:
InvariantViolated
Invariants:
CustomerNotBanned (on Place; reacted by: explain_banned)
OrderTotalWithinLimit (on Place; reacted by: explain_over_limit)
Policies:
explain_banned (InvariantViolated β Rejected)
explain_over_limit (InvariantViolated β Rejected)
Seed events:
Place
Ship
Full code Β· Raw diagrams on GitHub
Domain-named inline handlers
A Command's inline handler can be named after its verb (place, ship, submit, β¦) instead of the generic handle. The framework picks up the sole public method in the class body; underscore-prefix helpers. Declaring more than one public method on a Command raises TypeError at class creation. See examples/order.py for the canonical form.
Human-in-the-loop approval (Expense namespace)¶
- LLM extracts expense data; policy checker auto-approves small expenses or pauses with
Interruptedfor manager review. - Resume with an
ApproveorRejectcommand.
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Expense["Expense namespace"]
direction LR
Approve{{Approve}}:::cmd
Approved(Approved):::devt
Invalidated(Invalidated):::devt
Reject{{Reject}}:::cmd
Rejected(Rejected):::devt
Submit{{Submit}}:::cmd
Submitted(Submitted):::devt
end
ApprovalRequired([ApprovalRequired]):::syst
_e0_[ ]:::entry ==> Reject
_e1_[ ]:::entry ==> Submit
Submit --> Submitted
Submit --> Invalidated
Approve --> Approved
Reject --> Rejected
Submitted -->|"check_policy [orchestrate]"| Approve
Submitted -->|check_policy| ApprovalRequired
linkStyle 6 stroke:#0369a1,stroke-width:3px
Namespaces:
Expense
Command: Submit
β Submitted
β Invalidated
Command: Approve
β Approved
Command: Reject
β Rejected
System events:
ApprovalRequired
Policies:
check_policy (Submitted β Approve, ApprovalRequired)
Causal notes:
Submitted β Approve via check_policy [orchestrate]
Seed events:
Reject
Submit
Full code Β· Raw diagrams on GitHub
Tool-calling + AG-UI (Conversation namespace)¶
ReAct tool-calling agent wired end-to-end to AG-UI frontend tools (CopilotKit useFrontendTool).
Conversation.Sendenforces content moderation before the LLM sees the message.- Frontend-declared tools bound to the LLM via
build_langchain_tools. - Tool calls stream as
ToolCallStart/ToolCallArgs/ToolCallEnd; results return viadetect_new_tool_resultsβToolsExecuted. DomainEvent + MessageEventmixin withmessage_reducer().- For the handler-initiated
FrontendToolCallRequestedpattern, see AG-UI.
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Conversation["Conversation namespace"]
direction LR
Blocked(Blocked):::devt
Send{{Send}}:::cmd
Sent(Sent):::devt
end
AnswerProduced[/AnswerProduced/]:::intg
LLMResponded[/LLMResponded/]:::intg
ToolsExecuted[/ToolsExecuted/]:::intg
_e0_[ ]:::entry ==> Send
_e1_[ ]:::entry ==> ToolsExecuted
Send --> Sent
Send --> Blocked
Sent -->|call_llm| LLMResponded
ToolsExecuted -->|call_llm| LLMResponded
LLMResponded -->|finalize_answer| AnswerProduced
%% Side-effect handlers: audit_trail (Auditable)
Namespaces:
Conversation
Command: Send
β Sent
β Blocked
Integration events:
ToolsExecuted
LLMResponded
AnswerProduced
Policies:
call_llm (Sent, ToolsExecuted β LLMResponded)
finalize_answer (LLMResponded β AnswerProduced)
audit_trail (Auditable) [side-effect]
Seed events:
Send
ToolsExecuted
Full code Β· Raw diagrams on GitHub
Supervisor fan-in (Task namespace)¶
Task.Runkicks off the supervisor loop; supervisor dispatches sub-commandsTask.Research/Task.Codeor emits the terminalTask.Finalizedfact.- Custom
Reducerfolds specialist outputs into shared context.
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Task["Task namespace"]
direction LR
Code{{Code}}:::cmd
Completed(Completed):::devt
Finalized(Finalized):::devt
Produced(Produced):::devt
Research{{Research}}:::cmd
Run{{Run}}:::cmd
end
_e0_[ ]:::entry ==> Run
Run -->|"supervisor [orchestrate]"| Research
Run -->|"supervisor [orchestrate]"| Code
Run -->|supervisor| Finalized
Completed -->|"supervisor [orchestrate]"| Research
Completed -->|"supervisor [orchestrate]"| Code
Completed -->|supervisor| Finalized
Produced -->|"supervisor [orchestrate]"| Research
Produced -->|"supervisor [orchestrate]"| Code
Produced -->|supervisor| Finalized
Research --> Completed
Code --> Produced
%% Side-effect handlers: audit_trail (Auditable)
linkStyle 1,2,4,5,7,8 stroke:#0369a1,stroke-width:3px
Namespaces:
Task
Command: Run (handlers: supervisor)
Command: Research
β Completed
Command: Code
β Produced
Event: Finalized
Policies:
audit_trail (Auditable) [side-effect]
Causal notes:
Run β Research via supervisor [orchestrate]
Run β Code via supervisor [orchestrate]
Completed β Research via supervisor [orchestrate]
Completed β Code via supervisor [orchestrate]
Produced β Research via supervisor [orchestrate]
Produced β Code via supervisor [orchestrate]
Seed events:
Run
Full code Β· Raw diagrams on GitHub
Scatter fan-out (Batch namespace)¶
Batch.Summarizefans out to per-document work viaScatter[DocDispatched].- Gather handler uses
EventLog.filter()to wait for allDocSummarizedfacts, then emitsBatch.Summarized(namespace-level sibling β gather isn'tSummarize.handle(), so the outcome can't be Command-private).
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Batch["Batch namespace"]
direction LR
DocDispatched(DocDispatched):::devt
DocSummarized(DocSummarized):::devt
Summarize{{Summarize}}:::cmd
Summarized(Summarized):::devt
end
_e0_[ ]:::entry ==> Summarize
Summarize -.->|split_batch| DocDispatched
DocDispatched -->|summarize_one| DocSummarized
DocSummarized -->|gather_summaries| Summarized
%% Side-effect handlers: audit_trail (Auditable)
linkStyle 1 stroke:#7c3aed,stroke-width:2.5px,stroke-dasharray:8 3
Namespaces:
Batch
Command: Summarize (handlers: split_batch; scatters Scatter[DocDispatched])
Event: DocDispatched
Event: DocSummarized
Event: Summarized
Policies:
summarize_one (DocDispatched β DocSummarized)
gather_summaries (DocSummarized β Summarized)
audit_trail (Auditable) [side-effect]
Seed events:
Summarize
Full code Β· Raw diagrams on GitHub
Safety gates + streaming (Content namespace)¶
- Inline
Content.Process.handleclassifies text (keyword-based β no LLM). - External reactors gate approval, emitting
Content.Blocked(Haltedsubtype) orContent.Approved. - Live streaming via
astream_events().
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Content["Content namespace"]
direction LR
Analyzed(Analyzed):::devt
Approved(Approved):::devt
Blocked([Blocked]):::halt
Classified(Classified):::devt
Process{{Process}}:::cmd
end
_e0_[ ]:::entry ==> Process
Process --> Classified
Classified -->|gate| Blocked
Classified -->|gate| Approved
Approved -->|analyze| Analyzed
Namespaces:
Content
Command: Process
β Classified
Event: Blocked [Halted]
Event: Approved
Event: Analyzed
Policies:
gate (Classified β Blocked, Approved)
analyze (Approved β Analyzed)
Seed events:
Process
Full code Β· Raw diagrams on GitHub
Retries & escalation (Question namespace)¶
Question.Askdeclaresraises=(RateLimitError,); its inlinehandle()may raise.- A rate-limit catcher re-issues
Question.Askwithattempt+1, looping throughAsk.handle(). Ask.Answeredstays Command-private (produced only byAsk.handle()); chained catchers escalate toQuestion.GaveUp(Haltedsubtype) afterMAX_ATTEMPTS.
graph LR
classDef entry fill:none,stroke:none,color:none
classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
subgraph Question["Question namespace"]
direction LR
Answered(Answered):::devt
Ask{{Ask}}:::cmd
GaveUp([GaveUp]):::halt
end
HandlerRaised([HandlerRaised]):::syst
Ask -.->|"(raises)"| HandlerRaised
Ask --> Answered
HandlerRaised -.->|"backoff_and_retry (raises)"| HandlerRaised
HandlerRaised -->|"backoff_and_retry [orchestrate]"| Ask
HandlerRaised -->|give_up| GaveUp
linkStyle 0,2 stroke:#6b7280,stroke-dasharray:3 3
linkStyle 3 stroke:#0369a1,stroke-width:3px
Namespaces:
Question
Command: Ask (raises RateLimitError)
β Answered
Event: GaveUp [Halted]
System events:
HandlerRaised
Policies:
backoff_and_retry (HandlerRaised β Ask) [raises QuotaExhaustedError]
give_up (HandlerRaised β GaveUp)
Causal notes:
HandlerRaised β Ask via backoff_and_retry [orchestrate]