Maisaka Reasoning Engine
Maisaka is MaiBot's core AI runtime, responsible for dialogue reasoning, pacing control, and tool invocation. This document details its internal architecture, state machine, and execution flow.
Architecture Overview
MaisakaHeartFlowChatting
Source location: src/maisaka/runtime.py
Each chat session corresponds to a MaisakaHeartFlowChatting instance, managed by HeartflowManager.
State Machine
The runtime has three states:
running— Executing reasoning loopwait— Waiting state, wait tool set a timeoutstop— Idle state, waiting for new external message trigger
Core Properties
session_idstr— Session ID_chat_historylist[LLMContextMessage]— Internal context historymessage_cachelist[SessionMessage]— Pending message cache_internal_turn_queueasyncio.Queue— Internal loop trigger queue ("message" / "timeout")_tool_registryToolRegistry— Unified tool registry_reasoning_engineMaisakaReasoningEngine— Reasoning engine_chat_loop_serviceMaisakaChatLoopService— Chat loop service_max_internal_roundsint— Max internal rounds (default 6)_max_context_sizeint— Max context message count_message_debounce_secondsfloat— Message debounce seconds (default 1.0)_talk_frequency_adjustfloat— Talk frequency multiplierdeferred_tool_specs_by_namedict[str, ToolSpec]— Deferred discovery tool pooldiscovered_tool_namesset[str]— Discovered deferred tools
Message Trigger Mechanism
Trigger threshold calculation:
effective_frequency = talk_value * _talk_frequency_adjust # Reply frequency
trigger_threshold = ceil(1.0 / effective_frequency) # Required message countGap Compensation: When new messages are insufficient but the gap time is long, calculate equivalent message count based on recent average reply time.
Force Continue Mechanism
When @ or mention is detected, _arm_force_next_timing_continue() sets a flag so that the next Timing Gate directly returns continue, ensuring the bot responds to direct mentions.
MaisakaReasoningEngine
Source location: src/maisaka/reasoning_engine.py
Core reasoning engine, responsible for internal thinking loops and tool execution.
Key Constants
TIMING_GATE_CONTEXT_LIMIT— 24 · Timing Gate context message limitTIMING_GATE_MAX_TOKENS— 384 · Timing Gate max output tokensTIMING_GATE_TOOL_NAMES—{"continue", "no_reply", "wait"}· Timing Gate available toolsACTION_HIDDEN_TOOL_NAMES—{"continue", "no_reply"}· Action Loop hidden toolsMAX_INTERNAL_ROUNDS— 6 · Max internal thinking rounds
run_loop Main Loop
async def run_loop(self) -> None:
while runtime._running:
# 1. Wait for trigger signal
queued_trigger = await runtime._internal_turn_queue.get()
message_triggered, timeout_triggered = _drain_ready_turn_triggers(queued_trigger)
# 2. Message debounce
if message_triggered:
await runtime._wait_for_message_quiet_period()
# 3. Collect pending messages
cached_messages = runtime._collect_pending_messages()
# 4. Inject messages to history
await _ingest_messages(cached_messages)
# 5. Internal thinking loop
for round_index in range(max_internal_rounds):
# 5a. Timing Gate (if needed)
if timing_gate_required:
timing_action = await _run_timing_gate(anchor_message)
if timing_action != "continue":
break # wait or no_reply, end this cycle
# 5b. Planner (Action Loop)
response = await _run_interruptible_planner()
# 5c. Similarity detection
if _should_replace_reasoning(response.content):
# Replace with rethinking prompt
response.content = "I should reflect on my thinking above..."
# 5d. Tool execution
if response.tool_calls:
should_pause, summaries, monitors = await _handle_tool_calls(...)
if should_pause:
break
continue # New info after tool execution, continue loop
break # No tool call and no content, endTiming Gate
Timing Gate is an independent sub-agent that decides dialogue pacing:
Timing Gate system prompt:
- Primarily loads from
maisaka_timing_gatetemplate - Fallback prompt emphasizes call only one tool, do not output plain text
- Available tools are only
wait,no_reply,continue
Planner (Action Loop)
Planner is the main reasoning and tool execution phase:
Build Tool Definitions:
_build_action_tool_definitions()- Filter
ACTION_HIDDEN_TOOL_NAMES(continue, no_reply) - Built-in Action tools directly exposed
- Third-party/plugin tools are placed in the deferred pool by default and discovered via
tool_search; plugin tools declared withcore_tool=Trueorvisibility="visible"are exposed directly
- Filter
Run Interruptible Planner:
_run_interruptible_planner()- Bind
asyncio.Eventinterrupt flag - When new message arrives → set flag → LLM request aborts (
ReqAbortException) - Consecutive interrupts have a limit (
planner_interrupt_max_consecutive_count)
- Bind
Thought Deduplication:
_should_replace_reasoning()- When current and previous thoughts have similarity > 90%
- Replace with "rethink" prompt to avoid circular idle
Planner Interrupt Mechanism
Post-interrupt behavior:
- If
has_pending_messagesand max rounds not reached → Skip Timing Gate, re-enter Planner - Otherwise → End current loop
Tool Execution
Tool calls are routed through unified ToolRegistry:
Built-in Tool Definitions
Source location: src/maisaka/builtin_tool/
Timing Gate Tools
continue—continue_tool.py· Allow continuing to next thinking round · Key Parameters: Noneno_reply—no_reply.py· Stop current loop, wait for new external message · Key Parameters: Nonewait—wait.py· Pause dialogue for N seconds then re-judge · Key Parameters:seconds(default 30)
Action Tools
reply—reply.py· Generate and send reply message · Key Parameters:reply_text,msg_id,set_quotesend_emoji—send_emoji.py· Send emoji · Key Parameters:emoji_description,msg_idfinish—finish.py· End current thinking round · Key Parameters: Nonequery_jargon—query_jargon.py· Query jargon/terms · Key Parameters:wordsquery_memory—query_memory.py· Query long-term memory · Key Parameters:query,mode,limitquery_person_info—query_person_info.py· Query person information · Key Parameters:person_nameview_complex_message—view_complex_message.py· View complete forwarded message · Key Parameters:message_idtool_search—tool_search.py· Search deferred discovery tools · Key Parameters:query,limit
Deferred Tool Discovery Mechanism
In Action Loop, normal third-party/plugin tools are not directly exposed to Planner by default, but discovered in two steps:
- tool_search: Search deferred tool pool, mark matched tool names as "discovered"
- Next round Planner: Discovered tools added to visible tool list
This reduces the number of tools Planner sees at once, avoiding choice paralysis.
If a plugin Tool declares core_tool=True or visibility="visible", it skips deferred discovery and enters the Planner-visible tool list directly. This is intended for high-frequency, low-risk, strongly contextual tools; regular plugin tools should usually keep the default deferred behavior.
MaisakaChatLoopService
Source location: src/maisaka/chat_loop_service.py
Responsible for single-step LLM request encapsulation, including context selection, Prompt building, and Hook triggering.
chat_loop_step Flow
Context Selection Strategy
select_llm_context_messages() selects context for LLM from history:
- Filter by
request_kind(planner requests hide Timing Gate tool chain) - Traverse from end to front, select entries that can be successfully converted to LLM messages
- Only count messages with
count_in_context=True(ToolResultMessageandReferenceMessagedon't occupy window) - Stop after reaching
max_context_size - Hide earliest 50% of assistant text messages (preserve tool call chains)
Hook Specs
maisaka.planner.before_request— Can Abort ✗ · Can Rewrite ✓ · Can rewrite message list and tool definitionsmaisaka.planner.after_response— Can Abort ✗ · Can Rewrite ✓ · Can adjust text result and tool call listmaisaka.replyer.before_request— Can Abort ✗ · Can Rewrite ✓ · Can rewrite replyer task name, requested model, extra prompt, andreply_tool_argsmaisaka.replyer.before_model_request— Can Abort ✗ · Can Rewrite ✓ · Can rewrite the fully built replyermessagesthat are about to be sent to the modelmaisaka.replyer.after_response— Can Abort ✗ · Can Rewrite ✓ · Can rewrite the reply text or request replyer regeneration
Context Message Types
Source location: src/maisaka/context_messages.py
ReferenceMessageType
custom— Custom reference messagejargon— Jargon/term query resultmemory— Long-term memory retrieval resulttool_hint— Tool hint information (e.g., deferred tools reminder)
Context Window Occupation
SessionBackedMessage— Occupies Window ✓ · Real user messageComplexSessionMessage— Occupies Window ✓ · Complex/forwarded messageReferenceMessage— Occupies Window ✗ · Reference info (doesn't occupy window)AssistantMessage(assistant) — Occupies Window ✓ · Internal thinking textAssistantMessage(perception) — Occupies Window ✗ · Perception text (interrupt hints, etc.)ToolResultMessage— Occupies Window ✗ · Tool execution result
Planner Message Prefix
Source location: src/maisaka/planner_message_utils.py
When each user message is injected into Planner, a structured prefix is added:
[Time]HH:MM:SS
[Username]nickname
[User Group Nickname]group_card
[msg_id]message_id
[Message Content]actual message textbuild_planner_prefix() builds the prefix, build_planner_user_prefix_from_session_message() extracts parameters from SessionMessage.
Monitor Events
Source location: src/maisaka/monitor_events.py
Broadcasts events to frontend monitoring panel via WebSocket:
session.start— Runtime starts · Key Data: session_id, session_namemessage.ingested— Message injected to history · Key Data: speaker_name, content, message_idcycle.start— Thinking loop starts · Key Data: cycle_id, round_index, max_roundstiming_gate.result— Timing Gate decision completed · Key Data: action, content, tool_calls, prompt_tokensplanner.finalized— Planner completed · Key Data: Complete cycle data, token statistics, time spent
Complete Reasoning Flow Example
Example: User sends an @bot message in a group chat: