Skip to content

refactor(loop): convert _process_message to functional state machine#3715

Merged
Re-bin merged 12 commits into
mainfrom
worktree-loop-fsm-refactor
May 9, 2026
Merged

refactor(loop): convert _process_message to functional state machine#3715
Re-bin merged 12 commits into
mainfrom
worktree-loop-fsm-refactor

Conversation

@chengyongru
Copy link
Copy Markdown
Collaborator

@chengyongru chengyongru commented May 9, 2026

Summary

Refactor the 300-line _process_message method into an explicit functional state machine to improve readability and maintainability.

Changes

  • Extracted state handlers: RESTORE → COMPACT → COMMAND → BUILD → RUN → SAVE → RESPOND
  • Added TurnState enum and TurnContext dataclass to carry cross-state data
  • Short-circuited system messages (subagent announces) to a separate _process_system_message method
  • Driver loop uses getattr dispatch (while ctx.state != DONE)
  • Zero behavior change: all 794 agent tests pass

Verification

  • pytest tests/agent/ — 794 passed
  • ruff check nanobot/agent/loop.py — clean

- Extract TurnState enum and TurnContext dataclass
- Extract state handlers: _state_restore, _state_compact, _state_command,
  _state_build, _state_run, _state_save, _state_respond
- Extract _process_system_message for system message short-circuit
- Driver loop uses getattr dispatch over explicit state transitions
- Preserve all existing behavior (794 tests passing)
- Fix _assemble_outbound on_stream type annotation (Callable[[str], Awaitable[None]] | None)
- Use last_msg consistently in _state_save instead of re-indexing
- Remove dead  fallback in _state_respond (guaranteed non-None by _state_save)
- Change pending_summary type from Any to str | None
- Make session optional in TurnContext to avoid redundant fetch
- Add defensive dispatch with RuntimeError for missing handlers
@chengyongru chengyongru marked this pull request as ready for review May 9, 2026 08:35
- State handlers now return event strings ('ok', 'dispatch', 'shortcut')
- Driver loop uses _TRANSITIONS lookup table: (state, event) -> next_state
- State graph is centralized and visible at a glance
- Added StateTraceEntry to record per-state timing and events
- Driver loop logs state duration + event at debug level
- Exception paths are traced with error field for observability
@chengyongru
Copy link
Copy Markdown
Collaborator Author

chengyongru commented May 9, 2026

2026-05-09 17:05:42 | INFO  | - | Processing message from websocket:anon-0ae5838a2377: /new
2026-05-09 17:05:42 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317542757830300] State RESTORE took 0.9ms -> event ok
2026-05-09 17:05:42 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317542757830300] State COMPACT took 0.0ms -> event ok
2026-05-09 17:05:42 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317542757830300] State COMMAND took 6.7ms -> event shortcut
2026-05-09 17:05:42 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317542757830300] Turn completed after 3 states
2026-05-09 17:05:46 | INFO  | - | Processing message from websocket:anon-0ae5838a2377: hi
2026-05-09 17:05:46 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] State RESTORE took 0.3ms -> event ok
2026-05-09 17:05:46 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] State COMPACT took 0.0ms -> event ok
2026-05-09 17:05:46 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] State COMMAND took 0.0ms -> event dispatch
2026-05-09 17:05:46 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] State BUILD took 75.5ms -> event ok
2026-05-09 17:05:46 | DEBUG | - | Starting agent loop iteration 0 for session websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e
2026-05-09 17:05:47 | DEBUG | - | LLM usage: prompt=7162 completion=13 cached=7040
2026-05-09 17:05:47 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] State RUN took 1213.1ms -> event ok
2026-05-09 17:05:47 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] State SAVE took 1.2ms -> event ok
2026-05-09 17:05:47 | INFO  | - | Response to websocket:anon-0ae5838a2377: Hi! 有什么我可以帮你的吗? 😊
2026-05-09 17:05:47 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] State RESPOND took 0.1ms -> event ok
2026-05-09 17:05:47 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317546153088700] Turn completed after 7 states
2026-05-09 17:05:47 | DEBUG | - | Token consolidation idle websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e: 7278/200000 via tiktoken, msgs=2
2026-05-09 17:06:32 | INFO  | - | Processing message from websocket:anon-0ae5838a2377: 今天天气怎么样?
2026-05-09 17:06:32 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] State RESTORE took 0.2ms -> event ok
2026-05-09 17:06:32 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] State COMPACT took 0.0ms -> event ok
2026-05-09 17:06:32 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] State COMMAND took 0.0ms -> event dispatch
2026-05-09 17:06:32 | DEBUG | - | Token consolidation idle websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e: 7278/200000 via tiktoken, msgs=2
2026-05-09 17:06:32 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] State BUILD took 144.4ms -> event ok
2026-05-09 17:06:32 | DEBUG | - | Starting agent loop iteration 0 for session websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e
2026-05-09 17:06:34 | INFO  | - | Routed follow-up message to pending queue for session websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e
2026-05-09 17:06:36 | INFO  | - | Tool call: read_file({"path": "D:\\Documents\\GitHub\\nanobot\\.claude\\worktrees\\loop-fsm-refactor\\nanobot\\skills\\weather\\SKILL.md"})
2026-05-09 17:06:36 | INFO  | - | Injected 1 follow-up message(s) after tool execution (1/5)
2026-05-09 17:06:36 | DEBUG | - | LLM usage: prompt=7221 completion=81 cached=7040
2026-05-09 17:06:36 | DEBUG | - | Starting agent loop iteration 1 for session websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e
2026-05-09 17:06:39 | INFO  | - | Tool call: exec({"command": "curl -s \"wttr.in/Hangzhou?format=%l:+%c+%t+%h+%w&lang=zh\"", "timeout": 10})
2026-05-09 17:06:40 | DEBUG | - | LLM usage: prompt=7844 completion=65 cached=7168
2026-05-09 17:06:40 | DEBUG | - | Starting agent loop iteration 2 for session websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e
2026-05-09 17:06:43 | DEBUG | - | LLM usage: prompt=7938 completion=67 cached=7808
2026-05-09 17:06:43 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] State RUN took 11649.8ms -> event ok
2026-05-09 17:06:43 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] State SAVE took 3.4ms -> event ok
2026-05-09 17:06:43 | INFO  | - | Response to websocket:anon-0ae5838a2377: 杭州现在的天气:

- 🌤️ **晴间多云**
- 🌡️ 气温 **26°C**
- 💧 湿度 **23%**
- 🌬️ 风速 **西北风 13km/h**

天气不错,比较干燥,注意补水哦!
2026-05-09 17:06:43 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] State RESPOND took 0.3ms -> event ok
2026-05-09 17:06:43 | DEBUG | - | [turn websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e:1778317592136679900] Turn completed after 7 states
2026-05-09 17:06:44 | DEBUG | - | Token consolidation idle websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e: 8247/200000 via tiktoken, msgs=9
2026-05-09 17:07:50 | INFO  | - | Auto-compact: archived websocket:8f4b3b72-f648-4bc7-b53a-46f3a875986e (archived=2, kept=7, summary=True)

- TurnContext now carries a turn_id (session_key:time_ns)
- All state transition debug logs include [turn_id] prefix
- RuntimeError messages also include turn_id for observability
Copy link
Copy Markdown
Collaborator

@Re-bin Re-bin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice.

@Re-bin Re-bin merged commit de13e72 into main May 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants