77A standalone interactive CLI that uses the OpenAI SDK for LLM calls with
88tool_use. Demonstrates **parallel states**, **compound states**,
99**HistoryState**, **eventless transitions**, **In() guards**,
10- **done.state**, **error.execution**, and **raise_()** — all working
11- together in a practical application.
10+ **done.state**, **error.execution**, **invoke**, and **raise_()** — all
11+ working together in a practical application.
12+
13+ .. warning::
14+
15+ This example grants an LLM the ability to read files, list directories,
16+ and execute shell commands — which can be very useful for exploring a
17+ codebase, running tests, or automating tasks. However, the actual behavior
18+ depends on the prompts you send and the model you use, and unintended
19+ actions (e.g., deleting files or exposing credentials) are possible.
20+
21+ **Use at your own risk.** This code is provided for educational and
22+ demonstration purposes only. The authors and contributors of
23+ python-statemachine accept no liability for any damage or data loss.
24+ Consider running it in an isolated environment (e.g., a container or
25+ virtual machine) and avoid using elevated privileges.
1226
1327Usage::
1428
@@ -329,17 +343,17 @@ class AIShell(StateChart):
329343 """An agentic coding assistant as a StateChart.
330344
331345 Demonstrates parallel states, compound states, HistoryState, eventless
332- transitions, In() guards, done.state, error.execution, and raise_() —
333- all in a practical application.
346+ transitions, In() guards, done.state, error.execution, invoke, and
347+ raise_() — all in a practical application.
334348
335349 States::
336350
337351 session (Parallel)
338352 ├── conversation (Compound)
339353 │ ├── idle (initial)
340354 │ ├── processing (Compound)
341- │ │ ├── thinking (initial) ← API call + spinner
342- │ │ ├── using_tool
355+ │ │ ├── thinking (initial, invoke ) ← API call + spinner
356+ │ │ ├── using_tool (invoke) ← tool execution
343357 │ │ ├── done (final)
344358 │ │ └── h = HistoryState(deep) ← for error retry
345359 │ ├── responding
@@ -367,12 +381,11 @@ class processing(State.Compound):
367381 done = State ("Done" , final = True )
368382 h = HistoryState (type = "deep" )
369383
370- # Internal events route thinking results
371- use_tool = thinking .to (using_tool )
372- text_ready = thinking .to (done )
373-
374- # Tool execution → think again (via raise_())
375- think_again = using_tool .to (thinking )
384+ # Invoke results route automatically
385+ done_invoke_thinking = thinking .to (
386+ using_tool , cond = "has_tool_calls"
387+ ) | thinking .to (done )
388+ done_invoke_using_tool = using_tool .to (thinking )
376389
377390 responding = State ("Responding" )
378391 recovering = State ("Recovering" )
@@ -414,6 +427,7 @@ def __init__(self):
414427 self .messages : list = [{"role" : "system" , "content" : SYSTEM_PROMPT }]
415428 self ._last_text : str = ""
416429 self ._retries : int = 0
430+ self ._ready = threading .Event ()
417431 super ().__init__ ()
418432
419433 # --- Guards ---
@@ -431,19 +445,23 @@ def cannot_retry(self, **kwargs) -> bool:
431445 return not self .can_retry ()
432446
433447 def is_active_context (self , ** kwargs ) -> bool :
434- return len (self .messages ) >= 3
448+ return len (self .messages ) >= 5
435449
436450 def is_deep_context (self , ** kwargs ) -> bool :
437- return len (self .messages ) >= 5
451+ return len (self .messages ) >= 20
438452
439453 # --- Callbacks ---
440454
441455 def on_user_message (self , text , ** kwargs ):
442456 """Append the user's message to conversation history."""
443457 self .messages .append ({"role" : "user" , "content" : text })
444458
445- def on_enter_thinking (self , ** kwargs ):
446- """Call the OpenAI API with a spinner animation. Route to tool use or done."""
459+ def has_tool_calls (self , data = None , ** kwargs ) -> bool :
460+ """Guard: check if the API response contains tool calls."""
461+ return bool (getattr (data , "tool_calls" , None ))
462+
463+ def on_invoke_thinking (self , ** kwargs ):
464+ """Call the OpenAI API with a spinner animation. Returns the message."""
447465 with Spinner ():
448466 response = self .client .chat .completions .create (
449467 model = "gpt-4o-mini" ,
@@ -452,21 +470,16 @@ def on_enter_thinking(self, **kwargs):
452470 )
453471
454472 message = response .choices [0 ].message
455- tool_calls = message .tool_calls
456-
457- # Store the assistant response in history
458473 self .messages .append (message )
459474
460- if tool_calls :
461- self .raise_ ("use_tool" , tool_calls = list (tool_calls ))
462- else :
475+ if not message .tool_calls :
463476 self ._last_text = message .content or ""
464- # Enter the final child → triggers done.state.processing automatically
465- self .raise_ ("text_ready" )
466477
467- def on_enter_using_tool (self , tool_calls , ** kwargs ):
468- """Execute tool calls and send results back for another thinking round."""
469- for call in tool_calls :
478+ return message
479+
480+ def on_invoke_using_tool (self , data , ** kwargs ):
481+ """Execute tool calls from the API response."""
482+ for call in data .tool_calls :
470483 args = json .loads (call .function .arguments )
471484 print (f" [tool] { call .function .name } ({ json .dumps (args )} )" )
472485 result = execute_tool (call .function .name , args , sm = self )
@@ -477,7 +490,6 @@ def on_enter_using_tool(self, tool_calls, **kwargs):
477490 "content" : result ,
478491 }
479492 )
480- self .raise_ ("think_again" )
481493
482494 def on_enter_responding (self , ** kwargs ):
483495 """Print the assistant's text response."""
@@ -486,8 +498,9 @@ def on_enter_responding(self, **kwargs):
486498 self ._last_text = ""
487499
488500 def on_enter_idle (self , ** kwargs ):
489- """Reset retry counter when returning to idle."""
501+ """Reset retry counter and signal readiness when returning to idle."""
490502 self ._retries = 0
503+ self ._ready .set ()
491504
492505 def on_enter_recovering (self , ** kwargs ):
493506 """Handle API errors with retry logic (via error.execution)."""
@@ -540,6 +553,8 @@ def main():
540553 sys .exit (f"Error initializing: { e } " )
541554
542555 while not sm .is_terminated :
556+ sm ._ready .wait ()
557+ sm ._ready .clear ()
543558 try :
544559 text = input ("> " )
545560 except (EOFError , KeyboardInterrupt ):
0 commit comments