2026-04-28 01:26:21 +02:00
use crate ::ai_agents ::AiAgentStreamEvent ;
pub ( crate ) fn parse_line < F > (
line : Result < String , std ::io ::Error > ,
emit : & mut F ,
) -> Option < serde_json ::Value >
where
F : FnMut ( AiAgentStreamEvent ) ,
{
let line = match line {
Ok ( line ) = > line ,
Err ( error ) = > {
emit ( AiAgentStreamEvent ::Error {
message : format ! ( " Read error: {error} " ) ,
} ) ;
return None ;
}
} ;
let trimmed = line . trim ( ) ;
if trimmed . is_empty ( ) {
return None ;
}
serde_json ::from_str ::< serde_json ::Value > ( trimmed ) . ok ( )
}
pub ( crate ) fn dispatch_event < F > ( json : & serde_json ::Value , emit : & mut F )
where
F : FnMut ( AiAgentStreamEvent ) ,
{
if json [ " type " ] . as_str ( ) = = Some ( " session " ) {
emit_session_event ( json , emit ) ;
}
match json [ " type " ] . as_str ( ) . unwrap_or_default ( ) {
" message " | " text " = > emit_text ( json , emit ) ,
" reasoning " = > emit_reasoning ( json , emit ) ,
" tool_use " | " tool " = > emit_tool_start ( json , emit ) ,
" tool_result " | " tool_done " = > emit_tool_done ( json , emit ) ,
" error " = > emit_error_event ( json , emit ) ,
_ = > { }
}
}
pub ( crate ) fn session_id ( json : & serde_json ::Value ) -> Option < & str > {
json [ " sessionID " ]
. as_str ( )
. or_else ( | | json [ " session_id " ] . as_str ( ) )
. or_else ( | | json [ " session " ] [ " id " ] . as_str ( ) )
}
pub ( crate ) fn format_error ( stderr_output : String , status : String ) -> String {
let lower = stderr_output . to_ascii_lowercase ( ) ;
if is_auth_error ( & lower ) {
return " OpenCode CLI is not authenticated or has no provider configured. Run `opencode auth login` or configure a provider in OpenCode, then retry. " . into ( ) ;
}
if stderr_output . trim ( ) . is_empty ( ) {
format! ( " opencode exited with status {status} " )
} else {
stderr_output . lines ( ) . take ( 3 ) . collect ::< Vec < _ > > ( ) . join ( " \n " )
}
}
fn emit_session_event < F > ( json : & serde_json ::Value , emit : & mut F )
where
F : FnMut ( AiAgentStreamEvent ) ,
{
if let Some ( session_id ) = session_id ( json ) {
emit ( AiAgentStreamEvent ::Init {
session_id : session_id . to_string ( ) ,
} ) ;
}
}
fn emit_text < F > ( json : & serde_json ::Value , emit : & mut F )
where
F : FnMut ( AiAgentStreamEvent ) ,
{
if let Some ( text ) = text_value ( json ) {
emit ( AiAgentStreamEvent ::TextDelta {
text : text . to_string ( ) ,
} ) ;
}
}
fn emit_reasoning < F > ( json : & serde_json ::Value , emit : & mut F )
where
F : FnMut ( AiAgentStreamEvent ) ,
{
if let Some ( text ) = text_value ( json ) {
emit ( AiAgentStreamEvent ::ThinkingDelta {
text : text . to_string ( ) ,
} ) ;
}
}
fn emit_tool_start < F > ( json : & serde_json ::Value , emit : & mut F )
where
F : FnMut ( AiAgentStreamEvent ) ,
{
let tool_id = tool_id ( json ) . unwrap_or ( " tool " ) . to_string ( ) ;
2026-04-28 22:55:49 +02:00
let tool_name = tool_name ( json ) . unwrap_or ( " tool " ) . to_string ( ) ;
let input = tool_input ( json ) ;
2026-04-28 01:26:21 +02:00
emit ( AiAgentStreamEvent ::ToolStart {
tool_name ,
tool_id ,
input ,
} ) ;
}
fn emit_tool_done < F > ( json : & serde_json ::Value , emit : & mut F )
where
F : FnMut ( AiAgentStreamEvent ) ,
{
let tool_id = tool_id ( json ) . unwrap_or ( " tool " ) . to_string ( ) ;
2026-04-28 22:55:49 +02:00
let output = tool_output ( json ) ;
2026-04-28 01:26:21 +02:00
emit ( AiAgentStreamEvent ::ToolDone { tool_id , output } ) ;
}
fn emit_error_event < F > ( json : & serde_json ::Value , emit : & mut F )
where
F : FnMut ( AiAgentStreamEvent ) ,
{
if let Some ( message ) = message_value ( json ) {
emit ( AiAgentStreamEvent ::Error {
message : message . to_string ( ) ,
} ) ;
}
}
fn tool_id ( json : & serde_json ::Value ) -> Option < & str > {
2026-04-28 22:55:49 +02:00
first_string_field (
json ,
& [ " id " , " toolID " , " tool_id " , " toolCallID " , " toolCallId " ] ,
)
2026-04-28 01:26:21 +02:00
}
fn text_value ( json : & serde_json ::Value ) -> Option < & str > {
2026-04-28 22:55:49 +02:00
first_string_field ( json , & [ " text " , " content " , " message " ] )
2026-04-28 01:26:21 +02:00
}
fn message_value ( json : & serde_json ::Value ) -> Option < & str > {
2026-04-28 22:55:49 +02:00
first_string_field ( json , & [ " message " , " error " , " text " ] )
}
fn tool_name ( json : & serde_json ::Value ) -> Option < & str > {
first_string_field ( json , & [ " name " , " tool " , " toolName " ] )
}
fn tool_input ( json : & serde_json ::Value ) -> Option < String > {
first_json_field ( json , & [ " input " , " args " ] ) . map ( | input | input . to_string ( ) )
}
fn tool_output ( json : & serde_json ::Value ) -> Option < String > {
first_json_field ( json , & [ " output " , " result " ] ) . map ( display_json_value )
}
fn first_string_field < ' a > ( json : & ' a serde_json ::Value , keys : & [ & str ] ) -> Option < & ' a str > {
keys . iter ( )
. find_map ( | key | json [ * key ] . as_str ( ) . or_else ( | | json [ " part " ] [ * key ] . as_str ( ) ) )
}
fn first_json_field < ' a > (
json : & ' a serde_json ::Value ,
keys : & [ & str ] ,
) -> Option < & ' a serde_json ::Value > {
keys . iter ( )
. find_map ( | key | json . get ( * key ) . or_else ( | | json [ " part " ] . get ( * key ) ) )
}
fn display_json_value ( value : & serde_json ::Value ) -> String {
value
2026-04-28 01:26:21 +02:00
. as_str ( )
2026-04-28 22:55:49 +02:00
. map ( | output | output . to_string ( ) )
. unwrap_or_else ( | | value . to_string ( ) )
2026-04-28 01:26:21 +02:00
}
fn is_auth_error ( lower : & str ) -> bool {
[ " auth " , " login " , " sign in " , " api key " , " provider " ]
. iter ( )
. any ( | pattern | lower . contains ( pattern ) )
}
#[ cfg(test) ]
#[ path = " opencode_events_tests.rs " ]
mod tests ;