LOGS_SQL and LOGS_SQL_SEARCH left join the calls table on responses.id
(they share ids by construction). --json output now includes
head_input_message_id and head_output_message_id.
Rows that predate the DAG schema — or historical fixtures that only
wrote to responses — get NULL for those columns, so existing tests
are unaffected. Two new tests cover both cases: real prompt round-trip
(fields populated) and legacy-shape rows (fields NULL).
This is the minimum hook from the Deferred list in plans/dag-schema.md.
Union-read (prefer calls over responses, reconstruct prompt/response
from the DAG) is still deferred — a follow-up once there are calls
rows without matching responses rows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- plans/parts/plugin-upgrade-guide.md gains a "Storage: the DAG
message store (transparent to plugins)" section. Highlights the
two things plugin authors actually need to know: provider_metadata
is part of message identity (so echo it verbatim), and floats in
provider_metadata are rejected at hash time.
- plans/dag-schema.md's "Suggested implementation sequence" is
replaced with an "Implementation status" section reflecting what
actually shipped across the five commits, plus a Deferred section
listing llm logs tree view, full provider-adapter DAG reads, and
the eventual drop of responses writes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MessageStore.fork(source_message_id, name=, model=) creates a new
conversation rooted at an existing message. The shared prefix is
reused in place; only one conversations row is written.
- Model defaults to whatever most recently wrote a call touching the
source message; can be overridden. Raises ValueError if the message
doesn't exist or no model can be inferred.
- New `llm fork <message_id>` CLI prints the new conversation id.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Conversation (sync + async) gains head_message_id; from_row populates
it from the DB so CLI -c continuation resumes the existing chain
instead of starting a parallel one.
- MessageStore gains save_with_dedup(): one call that locates the
longest existing prefix and appends only the unmatched tail. Writes
zero rows when the full chain already exists — the stateless-API
continuation case from plans/dag-schema.md.
- Tests cover find_longest_existing_prefix (miss / partial / full),
save_with_dedup idempotency, and end-to-end: a second prompt on a
reloaded Conversation extends the chain via head_message_id.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrites m023 in place to the DAG-shaped message store from
plans/dag-schema.md:
- messages: id, parent_id, content_hash, role, provider_metadata_json,
created_at. Chain roots point at a self-referencing sentinel row
("root") so the unique (parent_id, content_hash) index works at
every chain position — NULL-parent uniqueness footgun avoided.
- message_parts: structurally unchanged.
- calls: one row per LLM call, anchoring head_input/head_output
message ids and recording model + timing + usage.
- conversations.head_message_id: advances each turn; history is
reconstructed by walking parent_id from the head.
New llm/storage.py provides MessageStore.save_chain (with dedup),
load_chain, and find_longest_existing_prefix (for the stateless-API
case wired in phase 3).
Response.log_to_db now writes the DAG + a calls row alongside the
existing responses-table writes (kept for llm logs compatibility
until phase 5). Response._load_messages_from_db walks the chain
using calls pointers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds llm/_canonical.py with canonical_message_json() and
message_content_hash(). The serialization is the hash contract for the
incoming DAG-shaped message store; snapshot tests in
tests/test_canonical.py pin the wire format.
The include_provider_metadata parameter is threaded through now so a
future semantic_hash column can be added without refactoring
(see plans/dag-provider-metadata-hashing.md).
Also commits the design docs:
- plans/dag-schema.md — full DAG storage design
- plans/dag-provider-metadata-hashing.md — Option 1 now, Option 2 later
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- python-api.md: 'Parts and stream events' section reworks the example
code around response.messages[].parts and drops the old parts=
parameter docs. New 'Prompting with messages' section shows the
user/assistant/system/tool_message helpers, parallel tool calls as
one assistant message, and Attachment-as-positional-arg.
- advanced-model-plugins.md: plugin author guide walks prompt.messages
instead of the old flat prompt.parts + legacy fields. provider_metadata
example no longer passes role= to TextPart; storage reference updated
to message_parts table.
Role now lives exclusively on the enclosing Message. Part subclasses no
longer accept or expose role; to_dict / from_dict drop the role key.
normalize_parts() lost its role= parameter (and helpers stopped passing
one), since emitted parts inherit their role from the Message they're
constructed into.
_parts_to_messages now wraps an output parts list in a single
assistant Message (which matches how providers like Anthropic package
server-side tool results — inside the assistant turn's content
blocks). Multi-message responses are represented via the
message_index field on StreamEvent (added earlier), not by reading
role off individual parts.
Tests updated to drop part.role assertions and to_dict role keys.
Migration m023 creates messages and message_parts tables. The old
parts table (from m022) is left in place for databases that already
ran that migration but is no longer read or written.
log_to_db now walks prompt.messages and response.messages, inserting
one messages row per Message and one message_parts row per Part.
Response.from_row loads via _load_messages_from_db into
_loaded_messages, which Response.messages returns directly — no more
group-parts-back-into-messages dance on load.
Tests updated to assert against the new schema. Per the branch
decision to ignore prior logs, no backfill migration is provided.
Hard removal of the flat parts-as-input API. Callers now use messages=
(with user/assistant/system/tool_message helpers) for explicit history
and response.messages for the structured response.
Changes:
- Prompt drops _parts and the parts= kwarg; Prompt.parts property gone.
- Response.parts and AsyncResponse.parts properties gone; messages is
now the canonical accessor.
- Model.prompt / Conversation.prompt / async variants drop parts= kwarg.
- log_to_db walks self.prompt.messages for input rows and
self._build_parts() for output rows, via a shared _part_to_row helper.
Tests migrated: TestPartsParameter deleted, TestBuildMessagesWithParts
rewritten to messages=. All remaining response.parts / r.parts /
loaded.parts usages flattened over response.messages.
Iterate prev_response.prompt.messages (the computed Message list) to
rebuild prior turn inputs, and funnel each through
_append_message_from_message. Output side still uses the flat text /
tool_calls accumulators (text_or_raise, tool_calls_or_raise) to avoid
calling _build_parts on historical responses whose StreamEvent shape
might have used the same part_index for mixed content types.
response.messages groups the flat parts list into a list of Message
objects by consecutive role. Typical case: one assistant message
wrapping all parts. Server-executed tool results (role='tool')
interleaved among assistant parts produce additional Messages at role
boundaries.
response.parts still works; hard-removal is deferred to a later commit
so mechanical test migration can happen in one focused change.
prompt.messages is now a computed property that synthesizes Messages
from legacy inputs (system=, parts=, prompt=, attachments=,
tool_results=) when messages= was not explicitly passed. Explicit
messages= passes through verbatim.
OpenAI build_messages() for the current prompt now has a single code
path that iterates prompt.messages — the old if/elif over _parts vs
legacy fields is gone. Conversation history reconstruction still uses
legacy fields (will flip in a later commit).
Accept messages= alongside the existing parts= parameter.
Conversation/AsyncConversation/Model/AsyncModel prompt() forward it to
Prompt, which stores it as prompt.messages.
OpenAI adapter gains _append_message_from_message which translates one
llm.Message into the correct OpenAI message dict(s), including the
parallel-tool-calls case (one assistant message with multiple
ToolCallParts becomes one OpenAI message with a tool_calls array).
Legacy paths untouched: parts=, prompt=, system=, attachments=, and
tool_results= still work when messages= is not set.
First step toward replacing flat parts=[] with structured messages=[].
Introduces the Message dataclass (role + parts + provider_metadata) and
convenience helpers that normalize strings, Attachments, Parts, and
nested lists into Message objects.
Additive only: existing Part.role, response.parts, parts= API still
work. Later commits flip Prompt/Response to consume messages and
remove the legacy surface.
Round out the parts API so transcripts survive serialization and so
providers can stash opaque multi-turn state on parts and stream events.
Serialization:
- AttachmentPart.from_dict now supported; inline content bytes round-trip
as base64.
- ToolResultPart.attachments round-trip through to_dict/from_dict.
Stream assembler:
- _build_parts raises ValueError when an incompatible StreamEvent type
appears at the same part_index, instead of silently overwriting the
earlier part. tool_call_name and tool_call_args stay compatible.
OpenAI parts=[] support:
- build_messages emits assistant tool_calls and role:"tool" messages for
ToolCallPart and ToolResultPart passed via parts=.
provider_metadata:
- New optional dict on TextPart, ReasoningPart, ToolCallPart,
ToolResultPart, and StreamEvent for opaque provider data that must be
echoed back on the next request (Anthropic signature/encrypted_content,
Gemini thoughtSignature, OpenAI Responses encrypted_content).
- StreamEvent values merge onto the finalized Part per top-level namespace
key, last non-None wins.
- Persisted via existing content_json column and reloaded by
_load_parts_from_db; no schema change.
- Plugin author guide in docs/plugins/advanced-model-plugins.md.
Types:
- Widen execute() return types to Iterator[str | StreamEvent] /
AsyncGenerator[str | StreamEvent, None] on abstract Model/AsyncModel
bases and OpenAI Chat implementations.
- Initialize _reasoning_token_count on _BaseResponse so mypy stops
flagging the OpenAI plugin.
New option: from llm.parts import Text then Text.system("..."),
Text.user("..."). Combined with role classmethods this is very
concise. Notes the Attachment/ToolCall name collision issue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New sections cover:
- response.parts for structured access to text, reasoning, tool calls
- ReasoningPart with both streamed (Anthropic) and redacted (OpenAI) examples
- ToolCallPart and ToolResultPart
- stream_events() / astream_events() for rich real-time streaming
- parts= parameter for constructing prompts with typed parts
- prompt.input_parts for unified view of all input parts
- All part types listed with serialization methods
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add notes about: requiring response.add_tool_call() alongside StreamEvents
for tool calls, yield from limitation in async generators, filtering empty
text chunks, and astream_events() vs stream_events() for async.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers: importing StreamEvent, converting yield str to yield
StreamEvent for text/reasoning/tool calls/server-side tools, part
index tracking, handling opaque reasoning tokens, testing patterns,
and the special case of plugins that inherit from built-in models.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents the " " space character that gets persisted as a real
TextPart when the Anthropic plugin works around a text-joining bug
in tool call chains. Lists four options to fix, recommends moving
the spacing concern to the display layer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
display_stream_events() helper handles writing text to stdout and
reasoning to stderr with proper newlines at each reasoning-to-text
transition. Used by sync prompt and chat streaming loops. Async prompt
loop has the same logic inline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Streaming loops in prompt and chat commands now use stream_events()
instead of __iter__. Reasoning events are displayed on stderr in
dim text. Text events go to stdout as before.
New flags:
-R / --no-reasoning Suppress reasoning output on stderr
-S / --no-stream (shortcut for existing --no-stream)
-L (shortcut for existing -n/--no-log)
ChainResponse.stream_events() and AsyncChainResponse.astream_events()
added so tool-calling flows also surface reasoning events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New m022_parts_table migration creates a parts table with direction
(input/output), role, part_type, content, content_json, tool_call_id,
and server_executed columns.
log_to_db() writes both input parts (from prompt.input_parts) and
output parts (from response.parts) to the table. from_row() loads
output parts and makes them available via the parts property.
Tested live: parts table created, input/output parts written and
loaded correctly with gpt-5.4-mini via CLI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
model.prompt() and conversation.prompt() now accept a parts= parameter
for passing explicit Part objects. Prompt.input_parts synthesizes a
unified list of input Parts from prompt=, system=, attachments=, and
parts= parameters.
prompt= remains sugar for a TextPart(role="user"). system= becomes
a TextPart(role="system"). attachments= become AttachmentParts.
All parameters combine (parts first, then system, then prompt, then
attachments).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents findings from all three plugin implementations: OpenAI
(set_usage mutation, opaque reasoning), Anthropic (streamed thinking,
server-side tools), Gemini (opaque thinking, complete parts per chunk).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chat.execute() and AsyncChat.execute() now yield StreamEvent instead
of bare strings. Text chunks, tool call names/args are all emitted
as typed events. Reasoning token counts from usage data are stored
on the response and _build_parts() prepends a redacted ReasoningPart.
StreamEvent gains server_executed and tool_name fields for use by
plugins with server-side tool execution.
Tested live against gpt-5.4-mini: text streaming, tool calls, and
reasoning tokens (with reasoning_effort='high') all work correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents design decisions, live testing observations against
gpt-5.4-mini, and things to watch for in Phase 3.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Response.__iter__ now handles str | StreamEvent from execute().
Plain str yields are backward compatible. StreamEvent yields are
processed by the assembler: text events yield as str to consumers,
reasoning/tool_call/tool_result events are filtered from __iter__
but available via stream_events(). Parts are assembled from events
after completion. Same changes for AsyncResponse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Response.stream_events() yields StreamEvents wrapping text chunks.
AsyncResponse.astream_events() is the async equivalent.
response.parts returns a list of Part objects after completion.
Currently only handles plain str chunks (Phase 1 baseline).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the parts project: define the Part dataclass hierarchy
(TextPart, ReasoningPart, ToolCallPart, ToolResultPart, AttachmentPart)
and StreamEvent in a new llm/parts.py module. All Part types have
to_dict()/from_dict() for JSON roundtripping. 14 tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>