Continuing Conversations Across Turns¶
You want a model that remembers prior exchanges, or one that calls tools and reasons over the results. This page covers the conversation mechanics: carrying context forward, injecting history, and setting up tool calling.
The complete agent loop that brings these pieces together lives on the next page: Building an Agent Loop.
At the API level, LLMs are stateless. Each call is independent. Multi-turn conversations work by passing the full conversation history (previous user messages, assistant responses, and tool results) back to the model with each new prompt. The model stores nothing between calls; your code carries state forward. Tool calling extends this: the model can request actions from your code mid-conversation, and you execute them and feed results back for the next turn.
Boundary
Pollux owns: delivering tool definitions to the provider, surfacing
tool_calls in the Output model, carrying conversation state via
continuation, and translating history formats across providers.
You own: the loop structure, tool implementations, turn limits, error handling per turn, deciding when to stop, and persisting history across sessions.
Continuing a Conversation with Continuation¶
Pass a prior result's continuation back into the next
Input(continuation=...) to resume the provider-correct conversation. Treat the
value as opaque: serialize it with to_jsonable(), restore it with
Continuation.from_jsonable(), and otherwise pass it back unchanged. Do not
inspect, edit, merge, or summarize its serialized provider state.
To get a continuation for subsequent turns in plain conversational calls, the first turn must opt into conversation tracking by passing history=[] (or an empty list/tuple). Without it, Pollux treats the call as stateless and does not build continuation state.
Note
When tool calling is active, Pollux auto-populates continuation state whenever the model returns tool_calls, so no explicit history=[] is needed. The opt-in requirement only applies to plain conversational calls without tools.
import asyncio
from pollux import Config, Environment, Input, interact
async def chat_loop() -> None:
config = Config(provider="gemini", model="gemini-2.5-flash-lite")
env = Environment()
# Turn 1: opt into conversation tracking with history=[]
print("User: Hello! Please remember my name is Sean.")
result1 = await interact(
env,
Input(content="Hello! Please remember my name is Sean.", history=[]),
config=config,
)
print(f"Assistant: {result1.text}")
# Turn 2: continue from prior result's continuation
print("\nUser: What is my name?")
result2 = await interact(
env,
Input(continuation=result1.continuation, content="What is my name?"),
config=config,
)
print(f"Assistant: {result2.text}")
asyncio.run(chat_loop())
Using history for Manual Control¶
If you need to inject mid-conversation context, groom old context out to
save tokens, or resume a chat from a database, a prior continuation alone
is not enough.
Instead, pass explicit typed Message history:
import asyncio
from pollux import Config, Environment, Input, Message, interact
async def manual_history_injection() -> None:
config = Config(provider="openai", model="gpt-5-nano")
env = Environment()
# Pull from database, wrapped in Message objects
previous_chat = [
Message(role="user", content="What is the capital of France?"),
Message(role="assistant", content="The capital of France is Paris.")
]
# Resume the chat by passing history into Input
result = await interact(
env,
Input(content="And what is its population?", history=previous_chat),
config=config,
)
print(f"Assistant: {result.text}")
asyncio.run(manual_history_injection())
Pollux treats the history block chronologically before the prompt you
provide to interact(). This deliberately gives up response IDs and opaque
provider replay state. A successful interaction creates a fresh continuation
for the active provider.
Persisting Agent Transcripts¶
For agent frameworks, persist Pollux continuation state separately from your product transcript:
- Keep the
output.continuation.to_jsonable()serialization whenoutput.continuationis present and you want Pollux to continue a provider-correct interaction later. - Maintain your own user-visible transcript for display, search, audit logs, and summarization.
- Capture media-bearing inputs as application records that can be reconstructed
as
Sourcevalues, or send them as current-turn input. Do not flatten PDFs, images, audio, video, or provider-native media parts into replay messages.
Application-Owned Transcripts¶
If your application is the source of truth for history because it supports
resume, compaction, truncation, or audit logs, keep that transcript in your own
store and rebuild typed Message history for each Pollux turn:
from pollux import Environment, Input, Message, Source, interact
history = [Message.from_openai(message) for message in text_messages]
env = Environment(sources=[Source.from_file("current-report.pdf")])
result = await interact(
env,
Input(content="Continue the analysis.", history=history),
config=config,
)
Message.from_openai() and Message.to_openai() bridge individual portable
text/tool messages. Move OpenAI system messages to Environment.instructions.
Display reasoning is diagnostic output and should not be copied into transcript
messages.
If you need to resume an older session that included files or images, load
those application records and rebuild Source.from_file(...),
Source.from_uri(...), or another source constructor explicitly. Pollux's
replay messages are for conversation turns, not hidden media transport.
Portable History Shapes¶
Portable history consists of non-empty user text, assistant text and/or
normalized ToolCall values, and tool messages with the matching
tool_call_id. Keep parallel tool results in call order. System messages,
media, reasoning, response IDs, and provider-native state are not portable.
Anthropic extended-thinking tool turns are a special boundary: signed thinking blocks live only in the untouched continuation. Return pending tool results with that continuation, then compact at a completed interaction boundary. Groomed history cannot recreate the signatures Anthropic requires.
Handling Tool Messages in History¶
If your conversation includes tool execution (the model asked for data, you
retrieved it, and now you must return it), history is how you manually
format tool responses:
from pollux import Input, Message, ToolCall, interact
history = [
Message(role="user", content="What's the weather in NYC?"),
Message(role="assistant", content="", tool_calls=(
ToolCall.from_text(id="call_1", name="get_weather", arguments_text='{"location": "NYC"}'),
)),
Message(role="tool", tool_call_id="call_1", content='{"temp_f": 72}'),
]
result = await interact(
env,
Input(content="Given that weather, what should I wear?", history=history),
config=config,
)
Setting Up Tool Calling¶
Pollux passes function tool definitions to providers and surfaces tool call responses in the output. Environment.tools is for Pollux-normalized client/application tools that your code executes. Provider-hosted server tools such as web search or code execution are provider-specific and belong in the provider_options= keyword argument.
Defining tools: pass ToolDeclaration objects in Environment.tools:
from pollux import Environment, ToolDeclaration
env = Environment(
tools=[
ToolDeclaration(
name="get_weather",
description="Get weather for a location",
parameters={
"type": "object",
"properties": {"location": {"type": "string"}},
},
)
],
)
Pollux normalizes tool parameter schemas at the provider boundary. For OpenAI (which defaults to strict mode), additionalProperties: false and required are injected automatically. For Gemini, unsupported fields like additionalProperties are stripped. You can define one schema and use it across all providers without modification.
Reading tool calls: when the model invokes tools, the completed Output includes a tool_calls field:
result = await interact(env, Input("What's the weather in NYC?"), config=config, tool_choice="auto")
if result.tool_calls:
for call in result.tool_calls:
print(call.name, call.arguments)
Feeding Tool Results Back with interact()¶
When the model requests tool calls, you execute them in your code and feed the results back for the next turn. interact() handles this handoff: it takes the previous Output's continuation handle along with your tool results, and returns the model's next response.
from pollux import Input, ToolResult, interact
# After executing the tool calls from a previous result...
tool_results = [
ToolResult(call_id="call_1", content='{"temp_f": 72}'),
]
next_result = await interact(
env,
Input(continuation=result.continuation, tool_results=tool_results),
config=config,
)
interact() internally reconstructs the conversation history from the previous continuation, appends your tool results, and calls the provider to get the model's next response. The returned Output may contain another round of tool_calls or a final text answer.
What to Watch For¶
tool_callsis flat onOutput. When usinginteract(),result.tool_callsis a flat tuple ofToolCallobjects.- Conversation continuity requires one prompt.
interact()takes a singleInputturn. - Plain conversations need
history=[]on the first turn. Withouthistoryorcontinuation, Pollux treats a call as stateless and does not produce continuation state. Passhistory=[]on the first turn to enablecontinuationon subsequent turns. - Tool-call responses auto-populate continuation. When a call returns
tool calls, Pollux builds the
continuationstate automatically, even without explicithistory=[]. This means continuation works for the next turn of any tool call, with no opt-in needed. - Reasoning is output data unless a provider requires opaque replay.
Pollux surfaces reasoning text on
Output.reasoningfor display and debugging. Provider-specific signed thinking or reasoning blocks are kept inside the opaque continuation only when the provider requires them for valid follow-up turns; do not copy display reasoning into future messages. - Provider differences exist. Gemini, OpenAI, and Anthropic support tool calling and tool messages in history. OpenRouter supports them on models that advertise tool support. See Provider Capabilities for details.
Durable Environment Identity¶
Use environment.fingerprint(provider=config.provider) to bind saved work to
the model-facing instructions, sources, tools, and provider. Compose
config.model plus application policy/schema identifiers separately. Cache
preferences and environment metadata intentionally do not affect this identity.
Now that you understand the conversation mechanics, see Building an Agent Loop to put them together into a complete tool-calling agent. For production error handling in agent loops, see Handling Errors and Recovery.