Skip to content

Branch

class Branch(Element, Relational)

One stateful unit of model work: manages message history, tools, model access, logs, optional memory, and LLM operations. Most SDK usage starts here.

Need Method
Normal recorded turn, no tools communicate()
Structured output or tool execution operate()
Low-level unrecorded invocation chat()
Low-level invocation that is recorded chat_and_record()
Iterative tool use ReAct()
Stream a CLI-backed provider run()

Constructor

branch = li.Branch(
    chat_model=li.iModel(model="gpt-4o"),
    system="You are a research assistant.",
    name="researcher",
)
Param Type Default Notes
user SenderRecipient \| None None Branch owner/sender identity
name str \| None None Human label
system System \| JsonValue None System prompt (str or dict)
system_sender SenderRecipient \| None None Override the system message sender
chat_model iModel \| dict \| str None Primary model for chat / communicate / operate
parse_model iModel \| dict \| str None Model used for parse() retries
tools FuncTool \| list[FuncTool] None Pre-register tools on construction
messages Pile[RoledMessage] None Restore prior conversation history
logs Pile[Log] None Restore prior activity logs
log_config DataLoggerConfig \| dict None Log output configuration
system_datetime bool \| str None Inject current timestamp into system prompt
system_template str \| None None Deprecated — emits DeprecationWarning, has no effect; will be removed in a future release
system_template_context dict None Deprecated — emits DeprecationWarning, has no effect; will be removed in a future release
use_lion_system_message bool False Prepend LIONAGI system preamble
memory MemoryStore \| None None Explicit memory backend; otherwise created lazily on first access

Properties

Property Type Writable Notes
system System \| None No Active system message
messages Pile[RoledMessage] No Full conversation history
logs Pile[Log] No Activity log pile
chat_model iModel Yes Swap chat provider at runtime
parse_model iModel Yes Swap parse provider at runtime
tools dict[str, Tool] No Registered tool registry
memory MemoryStore No Explicit store, or a lazily-created private InMemoryStore
msgs MessageManager No Internal message manager
acts ActionManager No Internal action manager
mdls iModelManager No Internal model manager

Operations

operate() — universal structured operation

from pydantic import BaseModel

class Summary(BaseModel):
    title: str
    key_points: list[str]

result = await branch.operate(
    instruction="Summarize this paper: ...",
    response_format=Summary,
    actions=True,
    tools=["search"],
    action_strategy="concurrent",
)
# result: Summary instance

Routes through the Middle protocol: communicate for API endpoints and run_and_collect for CLI endpoints. Supports tool calling, structured output, and streaming persistence. Registered tools are only exposed and invoked when actions=True (or the supplied Instruct enables actions); passing tools= alone does not enable them.

Param Type Default Notes
instruction Instruction \| JsonValue None User message
instruct Instruct None Alternative to instruction — structured instruct object
guidance JsonValue None Additional guidance injected into instruction
context JsonValue None Prompt context visible to the model
response_format type[BaseModel] None Parse output into this Pydantic model
actions bool False Enable tool calling
tools ToolRef None Subset of registered tools to expose
invoke_actions bool True Auto-invoke tool calls returned by model
action_strategy "sequential" \| "concurrent" "concurrent" Tool execution order
field_models list[FieldModel] None Dynamic field extensions
stream_persist bool False Write JSONL chunks live (CLI endpoints)
persist_dir str \| None None Directory for JSONL chunks
middle Middle \| None None Override default routing
handle_validation "raise" \| "return_value" \| "return_none" "return_value" Parse failure behavior
chat_model iModel None Override branch's chat model for this call
parse_model iModel None Override branch's parse model
skip_validation bool False Skip response parsing entirely
reason bool False Enable chain-of-thought reasoning field
sender / recipient SenderRecipient None Override message identity

A response that sets action_strategy to "sequential" narrows a "concurrent" call for that turn only: its tool calls run one at a time, in request order, under the caller's call_params except max_concurrent, which is 1 for that batch (the timeout, retries and throttle still apply). The guard reads the fields the parsed response actually set, so a field the turn omits is not read as a declaration even when the response model's own default for it is "sequential", while an explicit "sequential" narrows even when it equals that default. A value a validator assigns or injects counts as a declaration, because the parsed model has set the field; the field is read by its field name, so a turn that sends only an alias leaves it at its default and does not narrow unless the model accepts the field name too (populate_by_name=True). A response never widens a "sequential" call, and an action_strategy="sequential" argument is not narrowed this way: act() awaits its calls in request order without applying call_params.

Returns: list | BaseModel | None | dict | str

For cookbook usage, see Research synthesis.


communicate() — single-turn with history accumulation

result = await branch.communicate(
    "What are the main causes of climate change?",
    response_format=None,  # returns str
)
# adds both user message and assistant response to branch.messages

Simpler than operate() — no tool calling. Accumulates messages in history automatically. Use when you need history building without tool invocation.

Param Type Default Notes
instruction Instruction \| JsonValue None User message
guidance JsonValue None Additional guidance
context JsonValue None Prompt context
plain_content str None Bypass instruction formatting
response_format type[BaseModel] None Parse output into Pydantic model
request_fields dict \| list[str] None Request specific fields from model
num_parse_retries int 3 Parse retry attempts
clear_messages bool False Clear history before this turn

Returns: BaseModel | dict | str | None


chat() — low-level, unrecorded invocation

text = await branch.chat("Draft an outline for a research paper on RAG.")
# does NOT add to branch.messages — caller manages history

Low-level building block. It does not add messages to history and returns the assistant response value, usually a string, by default.

Request the generated message objects explicitly when you need them:

instruction_msg, response_msg = await branch.chat(
    "Draft an outline for a research paper on RAG.",
    return_ins_res_message=True,
)

Returns: str by default, or (Instruction, AssistantResponse) when return_ins_res_message=True.

chat_and_record() — low-level invocation with history

text = await branch.chat_and_record("Draft an outline for a research paper on RAG.")

Calls chat(return_ins_res_message=True), adds both generated messages through the hook-aware async message path, and returns the assistant response string. Use it when you need chat()'s low-level parameters but still want observers and persistence to see the turn. For ordinary stateful calls, prefer communicate().


run() — streaming CLI endpoint

async for msg in branch.run("Write a detailed analysis of..."):
    if hasattr(msg, "content"):
        print(msg.content, end="", flush=True)

Async generator — yields RoledMessage objects as chunks arrive. Requires a CLI endpoint model (e.g., iModel(provider="claude_code", model="sonnet")).

Param Type Default Notes
instruction str "" User message
chat_model iModel \| None None Override to CLI endpoint model
stream_persist bool False Write JSONL chunks to disk
persist_dir str \| None None JSONL output directory
response_format type None Parse final accumulated text

parse() — structured extraction from text

class Verdict(BaseModel):
    score: int
    reasoning: str

verdict = await branch.parse(
    text='{"score": 8, "reasoning": "Strong methodology"}',
    response_format=Verdict,
    handle_validation="raise",
)

Extracts structured data from raw text without a new LLM call (unless retries are needed). Fuzzy key matching is enabled by default — handles minor key name variations from the model.

Param Type Default Notes
text str required Raw text to parse
response_format type[BaseModel] None Target Pydantic model
handle_validation "raise" \| "return_value" \| "return_none" "return_value" Failure behavior
max_retries int 3 LLM retry attempts on parse failure
fuzzy_match bool True Enable fuzzy key matching
similarity_threshold float 0.85 Minimum similarity for key matching

Returns: BaseModel | dict | str | None


act() — tool execution

responses = await branch.act(
    action_request=[{"function": "search", "arguments": {"query": "LLM benchmarks 2025"}}],
    strategy="concurrent",
)

Directly invoke tool calls. Takes ActionRequest, dict, or list.

Param Type Default Notes
action_request list \| ActionRequest \| BaseModel \| dict required Tool call(s) to execute
strategy "concurrent" \| "sequential" "concurrent" Execution order
suppress_errors bool True Catch tool errors instead of raising
verbose_action bool False Log each invocation

Returns: list[ActionResponse]


ReAct() — think-act-observe reasoning loops

result = await branch.ReAct(
    instruct={"instruction": "Find the latest papers on diffusion models and summarize."},
    tools=["search", "read_url"],
    max_extensions=5,
    response_format=ResearchReport,
)

Multi-round reasoning with tool use. Iterates until max_extensions or a terminal response.

Param Type Default Notes
instruct Instruct \| dict required Initial instruction
tools Any None Tools to expose (defaults to all registered)
response_format type[BaseModel] None Final output schema
max_extensions int \| None 3 Max reasoning iterations
reasoning_effort "low" \| "medium" \| "high" None Reasoning depth hint
return_analysis bool False Return every collected output instead of only the final result
verbose bool False Print each iteration

Returns: the final answer or result by default. With return_analysis=True, returns the collected outputs as a list.

For a full working example, see Research synthesis.


interpret() — prompt rewriting

refined = await branch.interpret(
    "llm stuff for code gen",
    domain="software engineering",
    style="precise and technical",
)
# refined: "Explain LLM-based code generation techniques for production use."

Rewrites raw user input into a refined prompt. Does not add to history.


Tool management

# register functions
branch.register_tools([search_fn, read_file_fn])

# register with update=True to overwrite existing
branch.register_tools(new_search_fn, update=True)

# register an API endpoint as a callable tool
branch.connect(
    provider="openai",
    endpoint="chat",
    name="gpt_tool",
    description="GPT-4o as a callable tool",
)

Context manager

async with li.Branch(chat_model=li.iModel(model="gpt-4o")) as branch:
    result = await branch.operate(instruction="Analyze this dataset: ...")
# logs auto-dumped on exit

Serialization

# round-trip
data = branch.to_dict()
restored = li.Branch.from_dict(data)

# messages as DataFrame
df = branch.to_df()

# clone (sync or async)
b2 = branch.clone()
b2 = await branch.aclone(sender=new_sender_id)

# dump logs
branch.dump_logs(clear=True, persist_path="./logs/session.json")
await branch.adump_logs(clear=False)

Branch.from_dict reads its input as trusted. The class names it holds are resolved as ADR-0001 D3 describes, which can import a module the data names, and its stored models choose the provider, model, base URL and request settings. Restore only data that your own process, or a writer you trust, produced.

Some settings lionagi sends with an HTTP model's requests belong to the process restoring the data rather than to the data, so a stored model never supplies them, whoever wrote it: a credential, the local-network opt-in, a header that can carry a credential (one named as a credential, such as Authorization or x-api-key, one whose name ends in -Key, Cookie and X-Auth), the Host header, and any client option other than its headers. iModel.from_dict attaches the provider's credential from the environment only when the stored base URL is the provider's registered one, and takes the local-network setting from the provider's registration; a stored config can turn that setting off, never on, and any stored value but true turns it off. The opt-in admits loopback addresses only (localhost, 127.0.0.1, ::1). A caller that wants another endpoint, credential or setting passes it, and a key passed without a base_url goes to the stored URL:

live = li.iModel(provider="openai", base_url=url, api_key="MY_GATEWAY_KEY")
restored = li.Branch.from_dict(data, chat_model=live, parse_model=live)
model = li.iModel.from_dict(model_data, base_url=url, api_key="MY_GATEWAY_KEY")

A string api_key is read first as the name of a lionagi setting or an environment variable that holds the key, and is used as the key itself only when no setting or variable of that name is set, so "MY_GATEWAY_KEY" above names a variable. Pass a SecretStr to give the key itself.

A CLI model sends no request of its own, so the rule above does not reach it. Its settings are restored as stored, and the child process it starts reads them: Codex config_overrides can name another provider URL and the environment variable its key is read from, Claude Code's settings and permission_mode set what that child may do, and a stored mcp_servers or mcp_config names the MCP servers it starts. The child inherits this process's environment unless a live caller sets one. Restore a CLI row only from a writer you trust.

Next: Session — manage multiple branches