Breaking Changes (2026-02-11)

This document summarizes the breaking changes introduced in this branch and shows how to migrate with minimal effort.

Summary

Error handling is now fully aligned with standard Python style:

  • Success paths return plain values.
  • Failure paths raise ErrorPayload.
  • StructuredOutput(value, error) is no longer used as a general return wrapper.

If your code still depends on .value / .error, migrate using the patterns below.

Scope

1. StructuredOutput Removed

StructuredOutput has been removed from core types and public exports.

  • src/republic/core/results.py
  • src/republic/core/__init__.py
  • src/republic/__init__.py

2. Non-Streaming APIs Now Use "Return Value + Exception"

The following APIs no longer return StructuredOutput:

  • LLM.chat(...) -> str
  • LLM.chat_async(...) -> str
  • LLM.tool_calls(...) -> list[dict[str, Any]]
  • LLM.tool_calls_async(...) -> list[dict[str, Any]]
  • LLM.if_(...) -> bool
  • LLM.if_async(...) -> bool
  • LLM.classify(...) -> str
  • LLM.classify_async(...) -> str
  • LLM.embed(...) -> Any
  • LLM.embed_async(...) -> Any

Tape session shortcuts changed in the same way:

  • Tape.chat(...) -> str
  • Tape.chat_async(...) -> str
  • Tape.tool_calls(...) -> list[dict[str, Any]]
  • Tape.tool_calls_async(...) -> list[dict[str, Any]]

3. ToolExecutor.execute Error Semantics Changed

ToolExecutor.execute(...) now raises ErrorPayload for invalid input, validation failures, missing context, unknown tools, and similar failures. It no longer returns a result object with an error field for these cases.

4. Tape Return-Type Simplification

This branch also finalizes:

  • ContextSelection removed. read_messages(...) now returns list[dict[str, Any]].
  • QueryResult removed. TapeQuery.all() now returns list[TapeEntry], and errors are raised as ErrorPayload.
  • read_entries() is deprecated. Use tape.query.all() for full entry reads.

Migration Examples

Chat

Before:

out = llm.chat("Ping")
if out.error:
    handle_error(out.error)
else:
    print(out.value)

After:

from republic.core.results import ErrorPayload

try:
    text = llm.chat("Ping")
    print(text)
except ErrorPayload as exc:
    handle_error(exc)

Text Decision / Classify

Before:

decision = llm.if_("service down", "should page?")
if decision.error is None and decision.value:
    page_oncall()

After:

from republic.core.results import ErrorPayload

try:
    decision = llm.if_("service down", "should page?")
    if decision:
        page_oncall()
except ErrorPayload as exc:
    handle_error(exc)

Embedding

Before:

out = llm.embed("incident summary")
if out.error:
    handle_error(out.error)
else:
    vectors = out.value

After:

from republic.core.results import ErrorPayload

try:
    vectors = llm.embed("incident summary")
except ErrorPayload as exc:
    handle_error(exc)

ToolExecutor

Before:

result = executor.execute(calls, tools=tools)
if result.error:
    handle_error(result.error)
else:
    print(result.tool_results)

After:

from republic.core.results import ErrorPayload

try:
    result = executor.execute(calls, tools=tools)
    print(result.tool_results)
except ErrorPayload as exc:
    handle_error(exc)

Fast Detection

Use this command to locate old calling patterns:

rg -n "StructuredOutput|\\.value\\b|\\.error\\b" src tests

Then migrate matches to "return value + try/except ErrorPayload".

Release Guidance

  • This is a clear API breaking change. Use a major version bump, or at minimum a minor bump with explicit release notes.
  • If you maintain downstream SDK consumers, provide a migration note or codemod focused on replacing .value / .error branches.