Trace a Tool-free Turn
Exercise objective
Without modifying source code, trace one CLI message through final Session persistence. You should be able to explain the order of all eight TurnState enum values and why DONE has no state handler.
Prerequisites
- Complete one real DeepSeek request.
- Read the chapters on the AgentLoop state machine and AgentRunner.
- Be able to launch the current source checkout as
python -m nanobotfrom your IDE.
Source to read
Read only these locations, in order:
nanobot/cli/commands.py::agentnanobot/agent/loop.py::AgentLoop.from_confignanobot/agent/loop.py::process_directnanobot/agent/loop.py::_process_messagenanobot/agent/loop.py::_state_restorethrough_state_respondnanobot/agent/runner.py::runnanobot/session/manager.py::save
Predict before running
Answer these questions first:
- Does the CLI’s one-shot mode start
MessageBus.consume_inbound()? - Where is the Session lock acquired?
- How many Runner iterations occur when there is no Tool Call?
- Does
SAVEoccur before or afterRESPOND? - What is the default Session key?
Procedure
1. Set breakpoints
Suggested breakpoints:
- before
AgentLoop.process_direct()calls_process_message; - before
_process_message()calls a state handler; - in
AgentRunner._request_model(); - in
_state_save(); - in
SessionManager.save().
2. Run with a fixed Session
Assume you set this variable in the earlier chapter:
$nanobotLab = Join-Path $env:TEMP "nanobot-learning"From example/nanobot/code-EN, start the debugger with the equivalent of:
python -m nanobot agent `
--config "$nanobotLab\config.json" `
--workspace "$nanobotLab\workspace" `
--session "learning:no-tool" `
--message "Do not use any tools. Reply with TURN_OK only."If the model still attempts a Tool Call, do not change the code. Try a more deterministic message and record that a real model’s choice is not a hard guarantee.
3. Record four field groups at every state
| Field group | What to observe |
|---|---|
ctx.state | The current state |
ctx.session.key | Whether it is learning:no-tool |
ctx.history / ctx.initial_messages | When Session history becomes model input for this Turn |
ctx.all_messages / ctx.final_content | What the persistence boundary receives after the Runner returns |
ctx.stop_reason / ctx.outbound | When the run result becomes an OutboundMessage |
4. Inspect the trace
Before the Turn finishes, inspect ctx.trace:
- state order;
duration_ms;- event;
- error.
The normal path contains seven handler records. DONE is the loop’s exit condition, so no _state_done() executes.
5. Inspect the persistence file
Do not guess its filename. List it first:
Get-ChildItem -LiteralPath "$nanobotLab\workspace\sessions" -File |
Sort-Object LastWriteTime -Descending |
Select-Object -First 3 Name, LastWriteTimeOpen the newest JSONL file and confirm that:
- the first line is metadata;
- later lines contain at least one user and one assistant message;
- it contains no credentials;
- the Session key is correct.
Expected observations
The main path should be:
RESTORE → COMPACT → COMMAND → BUILD → RUN → SAVE → RESPOND → DONEYou should also observe:
- CLI one-shot mode calls
process_direct()directly and does not depend on AgentLoop’s bus-consumer loop; process_direct()still reuses_session_locks;- BUILD produces the complete messages sent to the Provider;
- a one-iteration text response does not enter Tool execution;
- SAVE persists the Turn before RESPOND constructs the response;
- the
finallyblock inprocess_direct()resets Runtime status to idle.
Completion criteria
- Draw the actual call chain with at least seven functions.
- Save an observation table of states and key fields.
- Explain the shared core and distinct entry points of the CLI direct path and Gateway bus path.
- Locate this Turn’s user and assistant records in Session JSONL.
Verification
Send another message to the same Session:
python -m nanobot agent `
--config "$nanobotLab\config.json" `
--workspace "$nanobotLab\workspace" `
--session "learning:no-tool" `
--message "What marker did the previous message ask you to return? Reply with the marker only."If the response uses the previous Turn, Session replay is working. Then prove from the BUILD messages how that history entered model context.
Review questions
- Why does the direct path still need a Session lock?
- Can RUN duration in the trace be treated as exactly equal to Provider latency?
- Do
ctx.messagesandsession.messagesserve the same purpose? - If the process crashes before SAVE, which data might exist only in memory or a checkpoint?