reasoning_state_library#
Predefined body factories for reasoning states — states whose behaviour is driven by an LLM in a tool-calling loop instead of a hand-written body.
The reasoning_body() factory builds a state body that runs a plan→act→observe
loop. On each turn the LLM may:
call any registered tool, including the three built-in planning tools —
add_tasks,complete_taskandskip_task— that read and write a request-scopedTaskList;emit a final text answer, which is sent back to the user only when every task on the list is either
completedorskipped(or no tasks were ever added). If unresolved tasks remain, the orchestrator pushes back with a system message and the loop continues.
This evolves the earlier pure-ReAct loop: simple single-step requests still work with no task list at all (the LLM just answers), while complex multi-step requests are explicitly planned, tracked and either finished or skipped before a final answer is allowed through.
See baf.reasoning.tool.Tool, baf.reasoning.skill.Skill, and
baf.reasoning.workspace.Workspace for the user-facing primitives.
- class baf.library.state.reasoning_state_library.ReasoningStep(kind, step, summary, details=<factory>)[source]#
Bases:
objectOne observable event emitted by the reasoning loop.
A
ReasoningStepis the canonical shape forwarded to the user viasession.platform.reply_reasoning_stepand consumed by the UI client. Every kind shares the same envelope (kind+step+summary+details); kind-specific data lives insidedetailsso adding a new kind never breaks existing consumers.- details#
Kind-specific structured payload, e.g.
{"tool_calls": [...]}forLLM_TOOL_CALLSor{"task_id": ..., "result": ...}forTASK_COMPLETED.
- kind#
One of the constants on
ReasoningStepKind.
- step#
The loop iteration number this event belongs to (0-indexed).
REASONING_STARTEDandREASONING_FINISHEDcarry the step at which they were emitted (0 for started, last step for finished).
- summary#
A short human-readable line — suitable for direct rendering in a UI without inspecting
details.
- class baf.library.state.reasoning_state_library.ReasoningStepKind[source]#
Bases:
objectString constants for every kind of
ReasoningStepthe loop emits.Two of these —
REASONING_STARTEDandREASONING_FINISHED— bracket every reasoning_body invocation so a streaming UI knows when to open and close a “live trace” group around the steps in between.- LLM_TEXT = 'llm_text'#
- LLM_TOOL_CALLS = 'llm_tool_calls'#
- MAX_STEPS = 'max_steps'#
- PUSHBACK = 'pushback'#
- REASONING_FINISHED = 'reasoning_finished'#
- REASONING_STARTED = 'reasoning_started'#
- TASK_ADDED = 'task_added'#
- TASK_COMPLETED = 'task_completed'#
- TASK_SKIPPED = 'task_skipped'#
- TOOL_RESULT = 'tool_result'#
- class baf.library.state.reasoning_state_library.Task(id, description, status='pending', result='')[source]#
Bases:
objectA single planned subtask within a reasoning loop.
- description#
A short imperative description.
- id#
Integer id, unique within a
TaskList. Surfaced to the LLM so it can refer to specific tasks viacomplete_task/skip_task.
- result = ''#
A short summary of the outcome — populated when the task is completed or skipped.
- status = 'pending'#
One of
pending/in_progress/completed/skipped. New tasks start aspending.
- class baf.library.state.reasoning_state_library.TaskList[source]#
Bases:
objectA request-scoped list of tasks the LLM is expected to resolve.
A new TaskList is created at the start of every reasoning_body invocation — it does not persist across user messages. The reasoning loop checks
all_resolved()before accepting a final answer from the LLM.
- baf.library.state.reasoning_state_library._assistant_tool_call_message(tool_calls)[source]#
Build the assistant message that records the tool calls the LLM just emitted.
OpenAI’s chat format requires the model’s tool-call announcement to be preserved in the conversation history (with matching tool_call_id values) before the tool-result messages.
- baf.library.state.reasoning_state_library._build_pushback_message(pending)[source]#
Compose the system message sent when the LLM tries to finalize too early.
- baf.library.state.reasoning_state_library._build_system_prompt(skills, workspaces, base_prompt, task_list)[source]#
Concatenate base prompt + skills + workspace previews + task list.
- baf.library.state.reasoning_state_library._execute_tool_calls(tool_calls, tools_by_name, session=None, stream_steps=False, step=0)[source]#
Run each requested tool call and return the tool-result messages.
Each call is logged at
DEBUGlevel with its arguments and a truncated view of the result. Whenstream_stepsis True, atool_resultevent is also forwarded to the platform for each non-task tool right after it runs — task tools emit their owntask_added/task_completed/task_skippedevents from inside the tool function, so they are skipped here to avoid duplicates.
- baf.library.state.reasoning_state_library._run_reasoning_loop(session, llm, messages, tool_schemas, tools_by_name, skills, workspaces, task_list, step_cell, scratchpad, max_steps, system_prompt, fallback_message, stream_steps)[source]#
The actual think→act→observe loop, factored out so
reasoning_body()can wrap it in a singlereasoning_started/reasoning_finishedbracket viatry/finally.
- baf.library.state.reasoning_state_library._send_step(session, step, stream_steps=True)[source]#
Forward a
ReasoningSteptosession.platform.reply_reasoning_step.Silently no-ops when
stream_stepsis False or when the platform does not expose areply_reasoning_stepmethod (Telegram, A2A, etc.). Any exception from the platform is caught and logged so a UI bug cannot kill the reasoning loop.
- baf.library.state.reasoning_state_library._send_task_list(session, task_list, stream_steps=True)[source]#
Forward the current task list snapshot to
session.platform.reply_task_list_update.Same gating as
_send_step(). Sent alongside (not in place of) the matchingtask_added/task_completed/task_skippedstep events so a UI can render a live task panel without reconstructing it from the step stream.
- baf.library.state.reasoning_state_library._truncate_for_log(text, limit=500)[source]#
Truncate
texttolimitchars with a clear marker if cut.
- baf.library.state.reasoning_state_library._truncate_for_payload(text, limit=4000)[source]#
Truncate
texttolimitchars with a clear marker if cut.
- baf.library.state.reasoning_state_library.new_reasoning_state(agent, llm, name='reasoning_state', initial=True, max_steps=8, system_prompt="You are an autonomous reasoning agent. You have access to tools and to filesystem workspaces. When the user asks about a topic that could plausibly be answered by data in a registered workspace, ALWAYS browse the workspace first — call `list_directory` to see what's there, then `read_file` on relevant files — instead of answering from your training data. Use other tools when their description matches the user's intent. Keep replies concise and grounded in tool outputs.You MUST call `add_tasks` whenever a request requires more than one tool call or more than one piece of information to assemble a complete answer. Only skip the task list for trivial single-step questions like 'what time is it?'.", fallback_message="I couldn't reach a final answer within the step budget. Please rephrase or narrow your question.", enable_task_planning=True, stream_steps=True)[source]#
Create a predefined reasoning state on
agentand return it.Equivalent to:
state = agent.new_state(name, initial=initial) state.set_body(reasoning_body(llm, ...)) return state
Importable from the library package so callers can register predefined states by import (consistent with how future predefined states will be added):
from baf.library.state.reasoning_state_library import new_reasoning_state state = new_reasoning_state(agent, llm=gpt) state.when_event().go_to(state) # caller wires transitions externally
Transitions are intentionally NOT added here — the developer chooses how to connect this state to the rest of the agent’s state machine (typically a
state.when_event().go_to(state)self-loop, but other shapes are valid: gating on a condition, chaining from another state, etc.).- Parameters:
agent (Agent) – the agent the state is created on.
llm (LLM) – the LLM that drives the reasoning loop. Must implement
predict_with_tools.name (str) – the state’s name. Defaults to
'reasoning_state'.initial (bool) – whether this state is the agent’s initial state. Defaults to True.
max_steps (int) – maximum LLM turns per user message. Defaults to 8.
system_prompt (str) – the base system prompt prepended to the skill / workspace / task-list blocks.
fallback_message (str) – the message sent if
max_stepsis exhausted.enable_task_planning (bool) – when False, fall back to a pure ReAct loop (no built-in planning tools, no push-back, first text response wins). Defaults to True.
stream_steps (bool) – when True, forward every intermediate event to the user via
session.platform.reply_reasoning_stepif implemented. Defaults to True.
- Returns:
the newly created reasoning state, ready for transition wiring.
- Return type:
- baf.library.state.reasoning_state_library.reasoning_body(llm, max_steps=8, system_prompt="You are an autonomous reasoning agent. You have access to tools and to filesystem workspaces. When the user asks about a topic that could plausibly be answered by data in a registered workspace, ALWAYS browse the workspace first — call `list_directory` to see what's there, then `read_file` on relevant files — instead of answering from your training data. Use other tools when their description matches the user's intent. Keep replies concise and grounded in tool outputs.You MUST call `add_tasks` whenever a request requires more than one tool call or more than one piece of information to assemble a complete answer. Only skip the task list for trivial single-step questions like 'what time is it?'.", fallback_message="I couldn't reach a final answer within the step budget. Please rephrase or narrow your question.", enable_task_planning=True, stream_steps=True)[source]#
Build a state body that runs an LLM-driven plan→act→observe loop.
The body reads the user message from
session.eventand, on each iteration, callsllm.predict_with_toolswith:the agent’s registered tools (read at execution time from
agent._tools);the three built-in planning tools (
add_tasks,complete_task,skip_task) whenenable_task_planning=True;a system message containing the active skills, workspace previews, task-planning guidance, and the live task list.
The loop terminates when the LLM returns a final text answer and every task on the list is resolved. If unresolved tasks remain, the orchestrator appends a push-back system message and continues. If
max_stepsis reached without convergence the configured fallback message is sent.The final task list and a per-step trace (tool calls, results, push-backs) are persisted to
session['reasoning_scratchpad']for inspection.- Parameters:
llm (LLM) – the LLM that drives the loop. Must implement
predict_with_tools.max_steps (int) – maximum number of LLM turns per user message.
system_prompt (str) – the base system prompt prepended to the skill / workspace / task-list blocks.
fallback_message (str) – the message sent if
max_stepsis exhausted.enable_task_planning (bool) – when False, fall back to a pure ReAct loop (no built-in planning tools, no push-back, first text response wins). Defaults to True.
stream_steps (bool) – when True, forward every intermediate event (LLM tool calls, tool results, task add/complete/skip, push-back, max-steps fallback) to the user via
session.platform.reply_reasoning_stepif the platform implements it. The final answer is still sent viasession.reply. No-op on platforms without areply_reasoning_stepmethod. Defaults to True.
- Returns:
a state body suitable for
baf.core.state.State.set_body().- Return type:
Callable[[Session], None]