What We’re Building
A conversational agent that:- Receives natural language math questions
- Uses an LLM to understand intent and call tools
- Performs calculations through a custom calculator tool
- Returns formatted results
Input
“What is 42 multiplied by 13? Then add 100 to that result.”
Output
“Result: 646.00”
The Building Blocks in Action
This example perfectly illustrates why tinychat’s primitives are powerful: each abstraction handles exactly one concern, and they compose naturally.Message: Defining Information Flow
We use two message types to mark system boundaries:IngressMessage and EgressMessage are semantic markers. They tell us “this is where data enters” and “this is where data exits,” making the system’s boundaries explicit.
MessageProcessor: Encapsulating Behavior
TheLLMProcessor is where transformation happens:
Why this design works
Why this design works
The processor is stateful—the
OpenAIAgent maintains conversation context and tool definitions. But the messages it processes are stateless values.This separation is crucial:- The processor can be warm-started, cached, or replicated
- Messages can be logged, replayed, or tested independently
- State is explicit and contained, not scattered across message objects
Type signature tells the story
Type signature tells the story
The signature
IngressMessage → Optional[EgressMessage] documents the entire flow:- Input: Natural language question
- Output: Final answer (or
Noneif processing fails)
CompositeProcessor: Orchestrating Flow
Even though this example has just one processor, we useCompositeProcessor to set up the routing topology:
Scaling the Topology
Imagine extending this to a more sophisticated agent:The same three primitives, the same composition pattern—just more message types and more processors. Complexity grows linearly, not exponentially.
The Tool: Arbitrary Complexity Welcome
TheCalculatorTool demonstrates that processors can wrap arbitrarily complex behavior:
Tools are just processors in disguise
Tools are just processors in disguise
The calculator validates, computes, and formats—all the behaviors of a processor. But because it’s defined as a
Tool, the LLM can invoke it automatically.This is composition at the abstraction level: the LLM processor composes with tool processors, but the framework doesn’t dictate how tools work internally.Replace with anything
Replace with anything
Want to swap the calculator for a database query tool? A web search tool? An API call? Just implement the The framework doesn’t care. It just passes messages.
run method:Lifecycle: Setup and Execution
The main flow demonstrates the lifecycle pattern:1
Configuration phase
Create shared resources like the
TaskManager and observers. These are injected into all processors during setup.2
Assembly phase
Define your processors and declare their output types. Build the routing topology by mapping message types to handlers.
Output type declaration enables validation—the composite verifies that all declared types have handlers before you process any messages.
3
Setup phase
Call
setup() to initialize processors with shared configuration. This is where observers get registered and async resources are prepared.4
Execution phase
Process messages. The composite automatically routes through handlers, chains transformations, and returns when a terminal condition is met.
Observability Without Coupling
Notice theLoggingObserver:
Observers handle cross-cutting concerns like logging, metrics, tracing, and debugging without polluting processor logic.
Why This Architecture Works
Let’s connect this example back to tinychat’s philosophy:Simplicity
Three primitives, clearly separated. Messages carry data, processors transform it, composites route it.
Composability
Add more processors, more message types, more complex routing—the pattern stays the same.
Explicitness
Types document flow, setup is explicit, boundaries are marked. No magic, no surprises.
Flexibility
Swap tools, change LLMs, add preprocessing, inject state—it’s all just processors.
What You Don’t See (By Design)
This example notably doesn’t include:- Retry logic with exponential backoff
- Rate limiting for API calls
- Caching of responses
- Authentication/authorization
- Request idempotency
- Persistent conversation history
These are policy concerns, not primitive concerns. tinychat gives you the building blocks; you implement the policies that make sense for your use case.
Key Takeaways
Messages are the contract
Messages are the contract
Well-defined message types document your system’s information flow. Look at the types, understand the system.
Processors are independent units
Processors are independent units
Each processor does one thing. State is internal, interface is simple. Test in isolation, compose in production.
Composition creates systems
Composition creates systems
CompositeProcessor turns independent units into sophisticated flows. Type-based routing makes the topology explicit and verifiable.
Simplicity scales
Simplicity scales
The same three primitives work for a single LLM call or a multi-agent system with dozens of processors. Complexity is in the topology, not the abstractions.
Ready to dive deeper?
Learn about the information theory foundations and advanced topology patterns in the Core Primitives guide.