Skip to content

Make persisted tool-call IDs portable across provider switches #385

Description

@alejandro-ao

Bug summary

Switching providers or models in an existing conversation can fail when the session contains tool calls created by another provider. Tau currently persists a provider-produced tool-call identifier directly in the provider-neutral ToolCall.id field and reuses that value when serializing history for the next provider.

This is incompatible with providers that use different identifier formats or semantics.

A concrete failure occurs when switching from the OpenAI Codex Responses provider to Anthropic. A Codex function call contains two identifiers:

{
  "type": "function_call",
  "id": "fc_0ca1b059695ff22f...",
  "call_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI"
}

Tau currently combines these values into one persisted ID in src/tau_ai/openai_codex.py:

call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...

The Codex adapter can split this value when replaying the conversation to Codex, but the Anthropic adapter forwards it unchanged as both tool_use.id and tool_result.tool_use_id. Anthropic requires tool-use IDs to match:

^[a-zA-Z0-9_-]+$

The | separator is invalid, so Anthropic rejects the entire request with HTTP 400:

messages.1.content.0.tool_use.id: String should match pattern '^[a-zA-Z0-9_-]+$'

This reveals a broader abstraction leak: the single ToolCall.id field currently acts as Tau's correlation ID, a provider-native tool-call ID, and—in the Codex case—an encoding of multiple provider-native IDs.

How to reproduce it

  1. Start a Tau session using the openai-codex provider.
  2. Ask the model to perform an operation that invokes at least one tool.
  3. Allow the tool call and corresponding tool result to be persisted in the session.
  4. Switch the active model/provider to Anthropic without starting a new session.
  5. Send another user message.
  6. Observe that Anthropic rejects the compiled history before generating a response.

The outgoing Anthropic payload contains a block resembling:

{
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...",
      "name": "bash",
      "input": {}
    }
  ]
}

and a corresponding result resembling:

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...",
      "content": "..."
    }
  ]
}

Relevant serialization paths include:

  • src/tau_agent/messages.py: provider-neutral ToolCall and ToolResultMessage models
  • src/tau_ai/openai_codex.py: combines and later splits Codex call_id and item id
  • src/tau_ai/anthropic.py: forwards persisted IDs into Anthropic tool_use and tool_result blocks
  • src/tau_ai/openai_compatible.py: forwards persisted IDs into Chat Completions tool calls/results
  • src/tau_agent/session/jsonl.py: session compatibility and persistence

A regression test should build the history in memory; it should not require live provider credentials.

Proposed direction

Separate Tau's stable, provider-neutral tool-call identity from provider-native replay metadata, and add a target-provider history compilation step.

1. Introduce a canonical Tau correlation ID

Each tool call should have a stable Tau-owned ID such as:

tc_7c38b819be06457e9280fb16db87ee23

The canonical ID should:

  • use a conservative character set accepted by all supported providers;
  • remain stable in persisted sessions;
  • be referenced by ToolResultMessage.tool_call_id;
  • never encode provider-specific structure.

2. Preserve provider-native metadata separately

For same-provider replay, retain the native values needed by the originating transport. For Codex Responses, that includes at least:

{
  "provider": "openai-codex",
  "api": "responses",
  "call_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI",
  "item_id": "fc_0ca1b059695ff22f..."
}

The exact schema should remain typed and portable. tau_agent should define correlation semantics without learning Anthropic validation rules or Codex payload details; provider adapters in tau_ai should own native serialization.

3. Map IDs while compiling history for the target provider

When the target differs from the origin provider, generate a deterministic target-safe identifier and use it consistently for both the call and its result. A hash/base64url-derived mapping is preferable to replacing invalid characters because replacement can collide—for example, both a|b and a_b could become a_b.

Suggested behavior:

  • Codex → Codex: preserve native call_id and item id when available.
  • Anthropic → Anthropic: preserve the native tool_use.id when available.
  • Codex → Anthropic: derive an Anthropic-safe ID and apply it to the call and result.
  • Anthropic → Codex: derive a valid Codex correlation ID and omit optional native item metadata when it cannot be reconstructed safely.
  • Any provider → OpenAI-compatible: emit a target-valid ID and use it consistently.

Parallel calls must remain distinct, and mapping must be scoped consistently across the complete outgoing history.

4. Support existing sessions

Existing JSONL sessions may contain the current call_id|item_id representation. Loading or compiling these sessions should recognize that legacy shape, derive a deterministic canonical ID, retain the parsed Codex metadata, and rewrite matching tool-result references in memory. The migration must not mutate or corrupt the original session unexpectedly.

A smaller compatibility patch could first normalize IDs only in the Anthropic adapter, but this should be treated as an interim fix. It would address the immediate HTTP 400 while leaving provider identity embedded in the core message model.

5. Implement and verify in focused stages

A PR can be developed in these stages:

  1. Add failing provider-conversion tests for Codex history sent to Anthropic.
  2. Define canonical identity and provider replay metadata semantics.
  3. Add deterministic target-provider ID mapping.
  4. Add legacy-session decoding/migration.
  5. Cover same-provider replay and round-trip persistence.
  6. Update provider/model-switching and session documentation.

Tests should cover:

  • a Codex composite ID compiled for Anthropic;
  • matching remapping of tool_use.id and tool_result.tool_use_id;
  • parallel calls and collision resistance;
  • Anthropic history compiled for Codex and OpenAI-compatible providers;
  • same-provider replay preserving native metadata;
  • legacy JSONL sessions containing call_id|item_id;
  • switching providers more than once in one session;
  • malformed or missing provider metadata with a safe fallback.

Expected benefit

  • Users can switch models/providers mid-conversation after tools have run.
  • Provider-specific identifiers no longer leak into Tau's portable agent/session abstraction.
  • Same-provider replay can retain native fidelity while cross-provider replay remains valid.
  • Persisted sessions become more robust as additional providers and transports are added.
  • Provider conversion failures can be detected and tested without live API calls.
  • Error handling can report a clear compatibility problem instead of appearing as an unresponsive agent after a provider-side HTTP 400.

Tradeoff (if any)

  • The message/session schema becomes more explicit and may need a backward-compatible migration path.
  • Provider adapters must maintain an ID map while compiling historical messages.
  • Deterministic remapping adds modest implementation complexity, especially for parallel calls and malformed legacy history.
  • Some provider-native replay data may be impossible or unsafe to translate. Cross-provider replay may need to discard optional metadata while preserving visible text, tool calls, and results.
  • Existing extensions that assume ToolCall.id is the raw provider ID may require compatibility handling and documentation.

Related follow-up

Tool-call IDs are one part of cross-provider model switching. A separate follow-up should define a complete target-provider history policy for other opaque or provider-specific state, including:

  • thinking/reasoning signatures and encrypted reasoning items;
  • text signatures, response IDs, and cache/replay metadata;
  • differences in accepted message ordering and content-block formats;
  • target-model context-window estimation and compaction before sending;
  • clear TUI rendering of provider conversion and context-limit errors.

These concerns should not block the tool-ID fix, but the design should leave room for a reusable history compiler rather than accumulating unrelated conditionals in each provider adapter.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions