Agent Loop Cycle (Execution Lifecycle)
This document details the step-by-step lifecycle of a user prompt event as it propagates through Kesoku's asynchronous agent dispatcher and reasoning loops.
đ Lifecycle Diagram
Below is the workflow showing the path of a message from the initial user input to the final assistant response:
sequenceDiagram
autonumber
participant Chatbot as Chatbot (Discord / GChat)
participant Gateway as Broker Gateway (gateway.py)
participant Agent as Agent Dispatcher (agent.py)
participant Worker as SessionWorker (agent.py)
participant Exec as TurnExecutor (turn_executor.py)
participant LLM as LLM Backend (llm.py)
Chatbot->>Gateway: Post Message (role="user")
Gateway->>Gateway: Persist in SQLite & Broadcast
Agent->>Gateway: listen(role="user") captures message
alt SessionWorker does not exist for session_id
Agent->>Worker: Spawn new SessionWorker
Worker->>Worker: start() worker loop task
end
Agent->>Worker: enqueue(message)
Worker->>Exec: execute_turn(worker)
loop Active Turn (Worker Running)
Exec->>Worker: drain_queue_and_pivot() (Merge new inputs)
Exec->>LLM: Generate response (with interruption check)
alt LLM requests Tool Execution
Exec->>Exec: Run tools concurrently (Safe subprocesses)
Exec->>Gateway: Post Tool Results
else LLM returns Final Text Response
Exec->>Gateway: Post Assistant Response
Exec-->>Worker: Exit turn loop
end
end
âī¸ 1. Message Ingestion & Gateway Persistence
- Chatbot Ingestion: An adapter receives a chat message (e.g. a Discord user typing in a thread, or an incoming GCP Pub/Sub pull event).
- Stateless Post: The adapter resolves the channel/session mappings and calls:
await gateway.post(MessageDTO(role="user", content="...", session_id="...", ...)) - Persistence: The Gateway saves the message into the SQLite database (
messagestable) and publishes the event to active in-memory listeners.
2. Dispatcher Queue Routing (Agent)
- Master Loop: The main
Agentloop runs a continuous background listener looking for new user messages:async for msg in self.gateway.listen(role=MessageRole.USER, status=MessageStatus.PENDING): - Worker Resolution: When a message arrives, the dispatcher checks its local dictionary
self.workers(dict[str, SessionWorker]):- If no
SessionWorkerexists formsg.session_id(or if the previous worker was terminated), it instantiates a newSessionWorker, stores it, and triggers its task loop viaworker.start().
- If no
- Queue Insertion: The message is pushed into the worker's internal asynchronous queue (
worker.enqueue(msg)).
3. Asynchronous Turn Processing (SessionWorker)
- Worker Task: The
SessionWorkerruns an independent task loop (_worker_loop):async def _worker_loop(self): while self.running: msg = await self.queue.get() # ... resolve active role ... await self.executor.execute_turn(message=msg, worker=self) self.queue.task_done() - Draining and Pivoting: Inside
execute_turn, theTurnExecutorqueries the queue to see if multiple user messages arrived in rapid succession. It drains them and merges them into a single consolidated prompt. - Agent Reasoning Loop:
- Context Assembling: The executor compiles the dynamic system prompt (loading role
intro.md, AWD path, and staging paths). - LLM Inference: The executor issues a non-blocking request to the active LLM backend (
GeminiLLMorClaudeLLM). It passes anis_interruptedcallback pointing tonot worker.queue_empty(). If a user types a new message while the LLM is generating, the generation can be preemptively cancelled. - Tool Executions: If the model decides to invoke tools (e.g.
run_shell_command), the executor runs them.- Safety Note: Tool executions are atomic. Once a tool has started running, it will not be killed mid-execution. Interruption checks only happen between steps.
- Context Assembling: The executor compiles the dynamic system prompt (loading role
- Completion & Hibernation: Once the LLM returns a final text response (indicating it has finished its turn) and the message queue is empty, the worker task yields and goes to sleep, waiting for the dispatcher to enqueue new events.