Core#

The building blocks of a conversation: messages, the code blocks inside them that tools execute, and the log that stores, loads, and branches a conversation. See Usage for managing conversations, and Glossary for terminology.

Message#

A message in the conversation.

class gptme.message.Message#

A message in the assistant conversation.

role#

The role of the message sender (system, user, or assistant).

content#

The content of the message.

timestamp#

The timestamp of the message.

files#

Files attached to the message, could e.g. be images for vision.

pinned#

Whether this message should be pinned to the top of the chat, and never context-trimmed.

hide#

Whether this message should be hidden from the chat output (but still be sent to the assistant).

quiet#

Whether this message should be printed on execution (will still print on resume, unlike hide). This is not persisted to the log file.

ui_only#

Whether this message is a UI/status message that must not be sent to the provider. Kept in the log for display, but excluded by prepare_messages().

metadata#

Optional metadata including token usage and cost information.

__init__(role: ~typing.Literal['system', 'user', 'assistant'], content: str, timestamp: ~datetime.datetime = <factory>, files: list[~pathlib.Path | ~gptme.util.uri.URI] = <factory>, file_hashes: dict[str, str] = <factory>, call_id: str | None = None, pinned: bool = False, hide: bool = False, quiet: bool = False, ui_only: bool = False, ephemeral_ttl: int | None = None, metadata: ~gptme.message.MessageMetadata | None = None, terminal_display_content: str | None = None) → None#
concat(other: Message, separator: str = '\n\n') → Message#

Concatenate two messages of the same role.

Merges content with separator, and combines files and file_hashes. Preserves timestamp from the first message.

Parameters:
  • other – Message to concatenate with this one

  • separator – String to join content with (default: “nn”)

Returns:

New Message with merged content and files

Raises:

ValueError – If messages have different roles

cost(model: str | None = None, output=False) → float#

Get the input cost of the message in USD.

format(oneline: bool = False, highlight: bool = False, max_length: int | None = None) → str#

Format the message for display.

Parameters:
  • oneline – Whether to format the message as a single line

  • highlight – Whether to highlight code blocks

  • max_length – Maximum length of the message. If None, no truncation is applied. If set, will truncate at first newline or max_length, whichever comes first.

classmethod from_toml(toml: str) → Self#

Converts a TOML string to a message.

The string can be a single [[message]].

get_codeblocks() → list[Codeblock]#

Get all codeblocks from the message content.

replace(**kwargs) → Self#

Replace attributes of the message.

to_dict(keys=None) → dict#

Return a dict representation of the message, serializable to JSON.

to_toml() → str#

Converts a message to a TOML string, for easy editing by hand in editor to then be parsed back.

to_xml() → str#

Converts a message to an XML string with proper escaping.

Codeblock#

A codeblock in a message, possibly executable by tools.

class gptme.codeblock.Codeblock#

Codeblock(lang: str, content: str, path: str | None = None, start: int | None = None, fence: str = <factory>)

__init__(lang: str, content: str, path: str | None = None, start: int | None = None, fence: str = <factory>) → None#
classmethod from_xml(content: str) → Codeblock#

Example

<codeblock lang=”python” path=”example.py”> print(“Hello, world!”) </codeblock>

classmethod iter_from_markdown(markdown: str, streaming: bool = False) → list[Codeblock]#

Extract codeblocks from markdown.

Note: Tracing removed from this function as it’s called hundreds of times per conversation, creating ~97% of all trace spans (see Issue #199).

to_xml() → str#

Converts codeblock to XML with proper escaping.

LogManager#

Holds the current conversation as a list of messages, saves and loads the conversation to and from files, supports branching, etc.

Conversation log management for gptme.

Split into focused modules:

  • manager: Log data structure, LogManager orchestrator, message processing

  • conversations: ConversationMeta, conversation querying and management

class gptme.logmanager.ConversationMeta#

Metadata about a conversation.

__init__(id: str, name: str, path: str, created: float, modified: float, messages: int, branches: int, workspace: str, agent_name: str | None = None, agent_path: str | None = None, agent_avatar: str | None = None, agent_urls: dict[str, str] | None = None, model: str | None = None, total_cost: float = 0.0, total_input_tokens: int = 0, total_output_tokens: int = 0, total_cache_read_tokens: int = 0, models_usage: dict[str, dict[str, ~typing.Any]] = <factory>, last_message_role: str | None = None, last_message_preview: str | None = None) → None#
format(metadata=False) → str#

Format conversation metadata for display.

class gptme.logmanager.Log#

Log(messages: list[gptme.message.Message] = <factory>, persisted: tuple[gptme.message.Message, …] = (), persisted_path: pathlib.Path | None = None, persisted_size: int | None = None)

__init__(messages: list[~gptme.message.Message] = <factory>, persisted: tuple[~gptme.message.Message, ...] = (), persisted_path: ~pathlib.Path | None = None, persisted_size: int | None = None) → None#
property persisted_messages: int#

Number of messages known to be on disk already.

print(show_hidden: bool = False) → int#

Prints the log to the console. Returns the number of messages shown.

class gptme.logmanager.LogManager#

Manages a conversation log.

Can be used as a context manager to ensure locks are properly released:
with LogManager.load(logdir) as manager:

# use manager pass

__init__(log: list[Message] | None = None, logdir: str | Path | None = None, branch: str | None = None, lock: bool = True, view: str | None = None)#
append(msg: Message) → None#

Appends a message to the log, writes the log, prints the message.

When on a view branch, implements dual-write: - Appends to master branch (preserves full history) - Appends to current view (maintains compacted context)

branch(name: str) → None#

Switches to a branch.

create_view(name: str, log: Log | list[Message]) → None#

Create a new view branch with compacted content.

Parameters:
  • name – View name (e.g., ‘compacted-001’)

  • log – The compacted log to store

diff(branch: str) → str | None#

Prints the diff between the current branch and another branch.

edit(new_log: Log | list[Message]) → None#

Edits the log.

fork(name: str) → None#

Copy the conversation folder to a new name.

classmethod get_current_log() → LogManager | None#

Get the current LogManager instance for this context.

get_next_view_name() → str#

Generate the next sequential view name.

classmethod load(logdir: str | Path, initial_msgs: list[Message] | None = None, branch: str = 'main', create: bool = False, lock: bool = True, **kwargs) → LogManager#

Loads a conversation log.

property master_log: Log#

Get the master log (always the full lossless history).

property name: str#

Get the user-friendly display name from ChatConfig, fallback to chat_id.

preserve_lossless_log() → None#

Write the full lossless history to lossless.jsonl.

Call before manual compaction to ensure the original transcript is preserved even after conversation.jsonl is replaced. Raises on failure so callers can abort before touching conversation.jsonl.

read_model_trace()#

Read the persisted ModelSelectionTrace from logdir, or None if absent.

Returns:

ModelSelectionTrace if model_selection_trace.json exists, else None.

snapshot_message_files(msgs: list[Message]) → list[Message]#

Snapshot message attachments while preserving valid existing hashes.

switch_to_master() → None#

Switch back to master (full uncompacted history).

switch_view(name: str) → None#

Switch to a view branch.

Parameters:

name – View name to switch to

to_dict(branches=False) → dict#

Returns a dict representation of the log.

undo(n: int = 1, quiet=False) → None#

Removes the last message from the log.

property workspace: Path#

Path to workspace directory (resolves symlink if exists).

write(branches=True, sync=False) → None#

Writes to the conversation log.

Parameters:
  • branches – Whether to write other branches

  • sync – Sync written transcripts, recovery logs and namespace before returning. POSIX barrier failures propagate to the caller.

write_model_trace() → Path | None#

Persist the active ModelSelectionTrace and return its path.

Called automatically by write(). No-op when no trace is active in the current context (e.g. tests or sessions that pre-date Phase 0).

gptme.logmanager.check_for_modifications(log: Log) → bool#

Check if the most recent assistant message (since last user) has file modifications.

Only checks the last assistant message to prevent re-triggering pre-commit/autocommit when the agent responds to hook output (e.g. pre-commit failure) without making new file changes. Checking all assistant messages since the last user would cause infinite loops: the original save is still visible after the agent writes a text response to the failure.

gptme.logmanager.conversation_name_error(value: str) → str | None#

Return a validation error for unsafe conversation names, if any.

gptme.logmanager.delete_conversation(conv_id: str) → bool#

Delete a conversation by its ID.

Parameters:

conv_id – The conversation ID to delete

Returns:

True if deleted successfully, False if not found

Raises:

PermissionError – If the conversation directory cannot be deleted

gptme.logmanager.ephemeral_cache_boundary(msgs_after_pruning: list[Message]) → int | None#

Return the index (in msgs_after_pruning) of the last message before the first surviving ephemeral message, or None if no ephemeral messages remain.

This is the stable boundary that should receive a cache breakpoint: it’s the last message in the non-ephemeral prefix that will remain stable across turns as the ephemeral block continues to expire.

gptme.logmanager.get_conversation_by_id(conv_id: str, *, detail: bool = True) → ConversationMeta | None#

Get a conversation by its ID.

Uses direct path lookup (O(1) file access) rather than scanning all conversations.

Parameters:

conv_id – The conversation ID to find

Returns:

ConversationMeta if found, None otherwise

gptme.logmanager.get_conversation_meta_direct(conv_id: str, *, detail: bool = True, logs_dir: Path | None = None) → ConversationMeta | None#

Get a single conversation’s metadata by direct path lookup.

Bypasses the full glob+stat scan used by get_conversations(). O(1) file access instead of O(N_conversations) — suitable for partial cache updates where only one conversation changed.

Returns None when the conversation directory or JSONL file does not exist.

gptme.logmanager.get_conversations(*, detail: bool = True, include_test: bool = True) → Generator[ConversationMeta, None, None]#

Returns all conversations.

Parameters:
  • detail – If True (default), performs a full JSONL scan to compute exact costs, token counts, and model info. If False, reads only the tail of each file for a faster scan — suitable for list/search endpoints where cost/token aggregates are not displayed.

  • include_test – If True (default), includes test/eval conversations. If False, skips them before scanning any files or loading per-conversation config.

gptme.logmanager.get_user_conversations(*, detail: bool = True) → Generator[ConversationMeta, None, None]#

Returns all user conversations, excluding ones used for testing, evals, etc.

gptme.logmanager.list_conversations(limit: int = 20, include_test: bool = False, *, detail: bool = True) → list[ConversationMeta]#

List conversations with a limit.

Parameters:
  • limit – Maximum number of conversations to return

  • include_test – Whether to include test conversations

  • detail – If True, performs full JSONL scan for costs/tokens. If False, uses fast tail-only scan.

gptme.logmanager.prepare_messages(msgs: list[Message], workspace: Path | None = None, logdir: Path | None = None) → list[Message]#

Prepares the messages before sending to the LLM. - Takes the stored gptme conversation log - Enhances it with context such as file contents - Transforms it to the format expected by LLM providers

gptme.logmanager.prune_ephemeral_messages(msgs: list[Message]) → list[Message]#

Remove messages whose ephemeral_ttl has been exceeded.

ephemeral_ttl=N means: keep the message for N later assistant turns, then drop it. Walk backward counting assistant messages seen after each message; drop ephemeral messages once the count exceeds their TTL. Pinned messages are never dropped regardless of TTL.

gptme.logmanager.rename_conversation(conv_id: str, new_name: str) → bool#

Rename a conversation by updating its display name in the chat config.

Parameters:
  • conv_id – The conversation ID to rename

  • new_name – The new display name for the conversation

Returns:

True if renamed successfully, False if not found