Skip to content

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 result envelope, carrying conversation state via continue_from, 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 continue_from

Pass a prior ResultEnvelope back into Options(continue_from=...) to automatically resume a conversation. Pollux unpacks the initial prompt, the assistant's previous response, and any tool calls directly into the context payload without requiring manual dictionary manipulation.

To use continue_from, the first turn must opt into conversation tracking by passing history=[] (an empty list is enough). Without it, Pollux treats the call as stateless and does not build conversation state.

Note

When tool calling is active, Pollux auto-populates conversation state whenever the model returns tool_calls — no explicit history=[] needed. The opt-in requirement only applies to plain conversational calls without tools.

import asyncio
from pollux import Config, Options, run

async def chat_loop() -> None:
    config = Config(provider="gemini", model="gemini-2.5-flash-lite")

    # Turn 1: opt into conversation tracking with history=[]
    print("User: Hello! Please remember my name is Sean.")
    result1 = await run(
        "Hello! Please remember my name is Sean.",
        config=config,
        options=Options(history=[]),  # Enable conversation state
    )
    print(f"Assistant: {result1['answers'][0]}")

    # Turn 2: continue from prior result
    print("\nUser: What is my name?")
    result2 = await run(
        "What is my name?",
        config=config,
        options=Options(continue_from=result1),  # Picks up where result1 left off
    )
    print(f"Assistant: {result2['answers'][0]}")

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, continue_from won't cut it.

Instead, pass an explicit history list of dictionaries containing role and content:

import asyncio
from pollux import Config, Options, run

async def manual_history_injection() -> None:
    config = Config(provider="openai", model="gpt-5-nano")

    # Imagine this was pulled from a database
    previous_chat = [
        {"role": "user", "content": "What is the capital of France?"},
        {"role": "assistant", "content": "The capital of France is Paris."}
    ]

    # Resume the chat by passing history into Options
    result = await run(
        "And what is its population?",
        config=config,
        options=Options(history=previous_chat),
    )

    print(f"Assistant: {result['answers'][0]}")

asyncio.run(manual_history_injection())

Pollux treats the history block chronologically before the prompt you provide to run().

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:

history = [
    {"role": "user", "content": "What's the weather in NYC?"},
    {"role": "assistant", "content": "", "tool_calls": [...]},
    # Format your tool response like this:
    {"role": "tool", "tool_call_id": "call_1", "content": '{"temp_f": 72}'},
]

result = await run(
    "Given that weather, what should I wear?",
    config=config,
    options=Options(history=history, tools=[...]),
)

Setting Up Tool Calling

Pollux passes tool definitions to providers and surfaces tool call responses in the result envelope.

Defining tools: pass a list of tool schemas in Options.tools:

from pollux import Options

options = Options(
    tools=[
        {
            "name": "get_weather",
            "description": "Get weather for a location",
            "parameters": {
                "type": "object",
                "properties": {"location": {"type": "string"}},
            },
        }
    ],
    tool_choice="auto",  # "auto", "required", "none", or {"name": "..."}
)

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 result envelope includes a tool_calls field:

result = await run("What's the weather in NYC?", config=config, options=options)

if "tool_calls" in result:
    for call in result["tool_calls"][0]:  # per-prompt list
        print(call["name"], call["arguments"])

Feeding Tool Results Back with continue_tool()

When the model requests tool calls, you execute them in your code and feed the results back for the next turn. continue_tool() handles this handoff: it takes the previous ResultEnvelope (which contains the model's tool-call requests and conversation state) along with your tool-result messages, and returns the model's next response.

from pollux import continue_tool

# After executing the tool calls from a previous result...
tool_results = [
    {"role": "tool", "tool_call_id": "call_1", "content": '{"temp_f": 72}'},
]

next_result = await continue_tool(
    continue_from=result,       # The ResultEnvelope containing tool_calls
    tool_results=tool_results,  # Your tool outputs
    config=config,
    options=Options(tools=tools),
)

continue_tool() internally reconstructs the conversation history from the previous envelope's state, appends your tool results, and calls run() to get the model's next response. The returned ResultEnvelope may contain another round of tool_calls (if the model needs more data) or a final text answer.

What to Watch For

  • tool_calls is per-prompt. result["tool_calls"] is a list of lists, one per prompt. For run() (single prompt), access result["tool_calls"][0].
  • Conversation continuity requires one prompt. Both history and continue_from work with single-prompt run() calls, not run_many().
  • Plain conversations need history=[] on the first turn. Without history or continue_from, Pollux treats a call as stateless and does not produce conversation state. Pass history=[] on the first turn to enable continue_from on subsequent turns.
  • Tool-call responses auto-populate conversation state. When run() returns tool calls, Pollux builds conversation state automatically, even without explicit history or continue_from. This means continue_tool() works on any result that contains tool calls — no opt-in needed.
  • 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.

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.