PROTOCOL 03 / RUNTIME

Async, Events and Concurrency

1–2 sessions Foundation Eden research series

Learning outcomes

  • Explain two event loops
  • Schedule work with root.after
  • Use async and await correctly
  • Communicate through queues
  • Keep Tkinter on its thread
  • Shut workers down safely

1. Why interfaces freeze

If Adam moves while a five-second decision request runs inside a Tkinter callback, the same thread cannot repaint the window or process the Pause button. The UI appears frozen because its event loop is occupied.

A world tick, a user event and a slow model request are different types of work. They should progress independently and communicate through explicit messages.

2. Tkinter and asyncio are separate schedulers

Tkinter's main loop

root.mainloop() handles widgets, paint events and callbacks. Treat the Tk thread as the only place that modifies Tkinter or Turtle objects.

asyncio's task loop

asyncio coordinates coroutines that yield while waiting. await does not automatically create a thread, and concurrency is not the same as parallel CPU execution.

DO NOT PATCH THE FREEZE

Calling root.update() repeatedly introduces re-entrant UI work. Calling asyncio.run() from every button creates and destroys loops. Design one stable boundary instead.

3. First scheduler: root.after()

import tkinter as tk root = tk.Tk() label = tk.Label(root, text="Tick: 0") label.pack(padx=24, pady=24) tick_count = 0 def tick() -> None: """Advance the display and return control immediately.""" global tick_count tick_count += 1 label.config(text=f"Tick: {tick_count}") root.after(200, tick) root.after(200, tick) root.mainloop()

root.after() registers future work and returns. It does not sleep. The actual interval includes the time spent by other callbacks, so it is a scheduler rather than a precision clock.

4. Concurrent waiting with asyncio

import asyncio async def operation(name: str) -> str: """Simulate a three-second I/O operation.""" await asyncio.sleep(3) return f"{name} completed" async def main() -> None: results = await asyncio.gather(operation("A"), operation("B")) print(results) asyncio.run(main())

The two waits overlap because each coroutine yields control. CPU-heavy work would still block this loop and may require a process or specialised executor.

5. The safe bridge

TK THREADSubmit requestID + tick snapshot
QUEUETransfer workThread-safe boundary
ASYNC WORKERAwait operationNo widgets touched
TK THREADPoll resultroot.after()

The worker receives plain data and returns plain data. The UI periodically drains a result queue and decides whether the result is still relevant. A request created for tick 12 may be stale when it returns at tick 20.

6. Three independent rhythms

  • Simulation rhythm: when rules and needs advance.
  • Render rhythm: when the screen reflects the latest state.
  • Deliberation rhythm: when an agent needs an expensive decision.

Do not call the LLM on every visual frame or every tick. Deliberation should have a reason: a new goal, failed action, important observation or expired plan.

7. Guided laboratory

EXPERIMENT A

Create the freeze

Use a deliberate sleep once and observe the blocked window.

EXPERIMENT B

Build the clock

Replace the loop with a self-scheduling root.after callback.

EXPERIMENT C

Test coroutines

Run concurrent console tasks before integrating the UI.

EXPERIMENT D

Bridge with queues

Return a fake slow decision without touching a widget from the worker.

Protocol completion

A five-second operation does not stop the window moving or the Pause control responding. Closing the application cancels or completes pending work and terminates the worker cleanly.