@@ -184,13 +184,15 @@ def __init__(
184184 max_tool_iterations : int = 10 ,
185185 optimize : bool = False ,
186186 optimization_config : OptimizationConfig | None = None ,
187+ json_events : bool = False ,
187188 ) -> None :
188189 self .project_dir = str (Path (project_dir ).resolve ())
189190 self ._provider_name = provider_name
190191 self ._model = model
191192 self ._tier = tier
192193 self ._stream = stream
193194 self ._max_iterations = max_tool_iterations
195+ self ._json_events = json_events
194196
195197 self ._provider : Any = None
196198 self ._state = SessionState (project_dir = self .project_dir )
@@ -291,14 +293,26 @@ def _agent_turn(self, user_input: str, silent: bool = False) -> str:
291293 except Exception as e : # noqa: BLE001
292294 error_msg = f"[Provider error] { e } "
293295 if not silent :
294- self ._print (error_msg )
296+ if self ._json_events :
297+ self ._emit_event (type = "error" , message = error_msg )
298+ else :
299+ self ._print (error_msg )
295300 return error_msg
296301
297302 # Update credit tracking
298303 self ._state .total_input_tokens += response .input_tokens
299304 self ._state .total_output_tokens += response .output_tokens
300305 self ._state .total_cost_usd += response .estimated_cost_usd
301306
307+ # Emit token update event
308+ if self ._json_events and not silent :
309+ self ._emit_event (
310+ type = "tokens" ,
311+ in_tokens = self ._state .total_input_tokens ,
312+ out_tokens = self ._state .total_output_tokens ,
313+ cost_usd = self ._state .total_cost_usd ,
314+ )
315+
302316 if response .content :
303317 final_response = response .content
304318
@@ -330,6 +344,14 @@ def _agent_turn(self, user_input: str, silent: bool = False) -> str:
330344 )
331345 )
332346
347+ # Emit turn-done event
348+ if self ._json_events and not silent :
349+ self ._emit_event (
350+ type = "turn_done" ,
351+ total_tokens = self ._state .session_tokens ,
352+ cost_usd = self ._state .total_cost_usd ,
353+ )
354+
333355 return final_response
334356
335357 def _call_provider (self , messages : list [Message ], silent : bool = False ) -> CompletionResponse :
@@ -362,7 +384,7 @@ def _call_provider(self, messages: list[Message], silent: bool = False) -> Compl
362384 messages = hint .messages
363385 tools = hint .tools
364386
365- use_stream = self ._stream and not silent and not tools
387+ use_stream = self ._stream and not silent and not tools and not self . _json_events
366388 if use_stream :
367389 accumulated = ""
368390 for token in provider .stream (messages , tools = None ):
@@ -375,7 +397,10 @@ def _call_provider(self, messages: list[Message], silent: bool = False) -> Compl
375397 else :
376398 response = cast (CompletionResponse , provider .complete (messages , tools = tools ))
377399 if not silent and response .content :
378- self ._print (response .content )
400+ if self ._json_events :
401+ self ._emit_event (type = "llm_chunk" , text = response .content )
402+ else :
403+ self ._print (response .content )
379404
380405 # ── Optimization post-call ───────────────────────────────────────────
381406 if self ._optimizer is not None and hint is not None :
@@ -405,7 +430,10 @@ def _execute_tool_calls(
405430 inputs = tc .get ("input" , {})
406431
407432 if not silent :
408- self ._print (f"\n [Tool: { name } ]" )
433+ if self ._json_events :
434+ self ._emit_event (type = "tool_started" , name = name , args = inputs )
435+ else :
436+ self ._print (f"\n [Tool: { name } ]" )
409437
410438 # Fire pre_tool hooks
411439 pre_ctx = HookContext (
@@ -451,9 +479,12 @@ def _execute_tool_calls(
451479 elapsed = time .time () * 1000 - start_ms
452480
453481 if not silent :
454- # Truncate long outputs for display
455- display = output [:500 ] + "..." if len (output ) > 500 else output
456- self ._print (f" → { display } " )
482+ if self ._json_events :
483+ self ._emit_event (type = "tool_finished" , name = name , result = output , is_error = error )
484+ else :
485+ # Truncate long outputs for display
486+ display = output [:500 ] + "..." if len (output ) > 500 else output
487+ self ._print (f" → { display } " )
457488
458489 tr = ToolResult (
459490 tool_name = name ,
@@ -554,13 +585,23 @@ def _on_session_end(self) -> None:
554585 def _print_banner (self ) -> None :
555586 provider = getattr (self ._provider , "provider_name" , "?" )
556587 model = getattr (self ._provider , "model" , "?" )
557- self ._print (
558- f"\n [specsmith agent — AEE-integrated]"
559- f"\n Project: { self .project_dir } "
560- f"\n Provider: { provider } /{ model } "
561- f"\n Tools: { len (self ._tools )} | Skills: { len (self ._skills )} "
562- f"\n Type /help for commands, exit to quit\n "
563- )
588+ if self ._json_events :
589+ self ._emit_event (
590+ type = "ready" ,
591+ provider = provider ,
592+ model = model ,
593+ project_dir = self .project_dir ,
594+ tools = len (self ._tools ),
595+ skills = len (self ._skills ),
596+ )
597+ else :
598+ self ._print (
599+ f"\n [specsmith agent — AEE-integrated]"
600+ f"\n Project: { self .project_dir } "
601+ f"\n Provider: { provider } /{ model } "
602+ f"\n Tools: { len (self ._tools )} | Skills: { len (self ._skills )} "
603+ f"\n Type /help for commands, exit to quit\n "
604+ )
564605
565606 def _print_status (self ) -> None :
566607 self ._print (
@@ -594,10 +635,22 @@ def _print_help(self) -> None:
594635 def _prompt (self ) -> str :
595636 """Read a line of input from stdin."""
596637 try :
597- return input ("\n > " )
638+ # In json_events mode suppress the prompt string so stdout stays pure JSON
639+ return input ("" if self ._json_events else "\n > " )
598640 except EOFError :
599641 raise
600642
601643 def _print (self , text : str = "" , end : str = "\n " , flush : bool = False ) -> None :
602- """Print to stdout."""
603- print (text , end = end , flush = flush )
644+ """Print to stdout; in json_events mode emit a system event instead."""
645+ if self ._json_events :
646+ if text .strip ():
647+ self ._emit_event (type = "system" , message = text .strip ())
648+ else :
649+ print (text , end = end , flush = flush )
650+
651+ def _emit_event (self , ** kwargs : Any ) -> None :
652+ """Emit a newline-delimited JSON event to stdout (json_events mode only)."""
653+ import json
654+ import sys
655+
656+ print (json .dumps (kwargs ), flush = True , file = sys .stdout ) # noqa: T201
0 commit comments