@@ -16,7 +16,8 @@ use agentkit_acp::{
1616 } ,
1717} ;
1818use agentkit_core:: {
19- CancellationController , FinishReason , Item , ItemKind , Part , SessionId , ToolOutput , Usage ,
19+ CancellationController , Delta , FinishReason , Item , ItemKind , Part , PartId , PartKind , SessionId ,
20+ ToolOutput , Usage ,
2021} ;
2122use agentkit_loop:: {
2223 AgentEvent , LoopDriver , LoopError , LoopInterrupt , LoopObserver , LoopStep , ModelSession ,
@@ -38,6 +39,7 @@ use super::{
3839
3940const PAGE_SIZE : usize = 100 ;
4041static NEXT_ERROR_MESSAGE_ID : AtomicU64 = AtomicU64 :: new ( 1 ) ;
42+ static NEXT_THOUGHT_MESSAGE_ID : AtomicU64 = AtomicU64 :: new ( 1 ) ;
4143
4244fn available_commands_update ( session_id : wire:: SessionId ) -> wire:: UpdateSessionNotification {
4345 wire:: UpdateSessionNotification :: new (
@@ -131,7 +133,9 @@ impl AcpSessionUpdateSink for ConnectionSink {
131133#[ derive( Default ) ]
132134struct CurrentReplacementMessages {
133135 agent : Option < wire:: MessageId > ,
134- thought : Option < wire:: MessageId > ,
136+ thoughts : Vec < wire:: MessageId > ,
137+ thought_parts : HashMap < PartId , wire:: MessageId > ,
138+ pending_thought : Option < wire:: MessageId > ,
135139 replacement : Option < ReplacementGeneration > ,
136140}
137141
@@ -179,6 +183,40 @@ impl<S> ResponseReplacementSink<S> {
179183 }
180184 }
181185
186+ // AgentKit's ACP v2 adapter groups every reasoning part between tool boundaries
187+ // under one message ID. Carry the current PartId across its synchronous observer-to-sink
188+ // call so clients can render each reasoning part as a separate thought block.
189+ fn prepare_content_delta ( & self , delta : & Delta ) {
190+ let mut current = self
191+ . current
192+ . lock ( )
193+ . unwrap_or_else ( |error| error. into_inner ( ) ) ;
194+ current. pending_thought = match delta {
195+ Delta :: BeginPart {
196+ part_id,
197+ kind : PartKind :: Reasoning ,
198+ } => {
199+ current
200+ . thought_parts
201+ . entry ( part_id. clone ( ) )
202+ . or_insert_with ( || {
203+ let sequence = NEXT_THOUGHT_MESSAGE_ID . fetch_add ( 1 , Ordering :: Relaxed ) ;
204+ wire:: MessageId :: new ( format ! ( "kit-thought-{sequence}" ) )
205+ } ) ;
206+ None
207+ }
208+ Delta :: AppendText { part_id, .. } => current. thought_parts . get ( part_id) . cloned ( ) ,
209+ _ => None ,
210+ } ;
211+ }
212+
213+ fn clear_pending_thought ( & self ) {
214+ self . current
215+ . lock ( )
216+ . unwrap_or_else ( |error| error. into_inner ( ) )
217+ . pending_thought = None ;
218+ }
219+
182220 fn rewrite_and_track ( & self , notification : & mut wire:: UpdateSessionNotification ) {
183221 let mut current = self
184222 . current
@@ -192,10 +230,14 @@ impl<S> ResponseReplacementSink<S> {
192230 current. agent = Some ( chunk. message_id . clone ( ) ) ;
193231 }
194232 wire:: SessionUpdate :: AgentThoughtChunk ( chunk) => {
195- if let Some ( replacement) = current. replacement . as_mut ( ) {
233+ if let Some ( message_id) = current. pending_thought . take ( ) {
234+ chunk. message_id = message_id;
235+ } else if let Some ( replacement) = current. replacement . as_mut ( ) {
196236 chunk. message_id = replacement. message_id ( "thought" ) ;
197237 }
198- current. thought = Some ( chunk. message_id . clone ( ) ) ;
238+ if !current. thoughts . contains ( & chunk. message_id ) {
239+ current. thoughts . push ( chunk. message_id . clone ( ) ) ;
240+ }
199241 }
200242 _ => { }
201243 }
@@ -211,17 +253,19 @@ impl<S> ResponseReplacementSink<S> {
211253
212254impl < S : AcpSessionUpdateSink > ResponseReplacementSink < S > {
213255 fn clear_current ( & self , session_id : & wire:: SessionId ) -> Vec < Result < ( ) , AcpRuntimeError > > {
214- let ( agent, thought ) = {
256+ let ( agent, thoughts ) = {
215257 let mut current = self
216258 . current
217259 . lock ( )
218260 . unwrap_or_else ( |error| error. into_inner ( ) ) ;
219261 let agent = current. agent . take ( ) ;
220- let thought = current. thought . take ( ) ;
262+ let thoughts = std:: mem:: take ( & mut current. thoughts ) ;
263+ current. thought_parts . clear ( ) ;
264+ current. pending_thought = None ;
221265 current. replacement = Some ( ReplacementGeneration :: new ( ) ) ;
222- ( agent, thought )
266+ ( agent, thoughts )
223267 } ;
224- let mut results = Vec :: with_capacity ( 2 ) ;
268+ let mut results = Vec :: with_capacity ( usize :: from ( agent . is_some ( ) ) + thoughts . len ( ) ) ;
225269 if let Some ( message_id) = agent {
226270 results. push ( self . inner . update ( wire:: UpdateSessionNotification :: new (
227271 session_id. clone ( ) ,
@@ -230,7 +274,7 @@ impl<S: AcpSessionUpdateSink> ResponseReplacementSink<S> {
230274 ) ,
231275 ) ) ) ;
232276 }
233- if let Some ( message_id) = thought {
277+ for message_id in thoughts {
234278 results. push ( self . inner . update ( wire:: UpdateSessionNotification :: new (
235279 session_id. clone ( ) ,
236280 wire:: SessionUpdate :: AgentThought (
@@ -349,7 +393,11 @@ where
349393 ) {
350394 self . sink . reset ( ) ;
351395 }
396+ if let AgentEvent :: ContentDelta ( delta) = & event. event {
397+ self . sink . prepare_content_delta ( delta) ;
398+ }
352399 self . inner . handle_event ( event) ;
400+ self . sink . clear_pending_thought ( ) ;
353401 }
354402}
355403
@@ -1971,6 +2019,62 @@ mod tests {
19712019 assert ! ( usage. cost. is_none( ) ) ;
19722020 }
19732021
2022+ #[ test]
2023+ fn reasoning_parts_use_separate_thought_message_ids ( ) {
2024+ let integration = AcpIntegration :: default ( ) ;
2025+ let recording = RecordingSink :: default ( ) ;
2026+ let sink = ResponseReplacementSink :: new ( recording. clone ( ) ) ;
2027+ let session_id = wire:: SessionId :: new ( "thought-session" ) ;
2028+ let loop_session_id = SessionId :: new ( "thought-loop" ) ;
2029+ let _handle = integration
2030+ . bind_session ( AcpSessionBinding :: new (
2031+ session_id. clone ( ) ,
2032+ loop_session_id. clone ( ) ,
2033+ sink. clone ( ) ,
2034+ ) )
2035+ . unwrap ( ) ;
2036+ let observer = ResponseReplacementObserver :: new ( integration, sink, session_id) ;
2037+ let emit = |delta| {
2038+ observer. handle_event ( ObservedEvent {
2039+ session_id : Arc :: new ( loop_session_id. clone ( ) ) ,
2040+ event : AgentEvent :: ContentDelta ( delta) ,
2041+ } ) ;
2042+ } ;
2043+
2044+ let first = PartId :: new ( "reasoning-1" ) ;
2045+ let second = PartId :: new ( "reasoning-2" ) ;
2046+ emit ( Delta :: BeginPart {
2047+ part_id : first. clone ( ) ,
2048+ kind : PartKind :: Reasoning ,
2049+ } ) ;
2050+ for chunk in [ "first" , " continued" ] {
2051+ emit ( Delta :: AppendText {
2052+ part_id : first. clone ( ) ,
2053+ chunk : chunk. into ( ) ,
2054+ } ) ;
2055+ }
2056+ emit ( Delta :: BeginPart {
2057+ part_id : second. clone ( ) ,
2058+ kind : PartKind :: Reasoning ,
2059+ } ) ;
2060+ emit ( Delta :: AppendText {
2061+ part_id : second,
2062+ chunk : "second" . into ( ) ,
2063+ } ) ;
2064+
2065+ let updates = recording. updates . lock ( ) . unwrap ( ) ;
2066+ let ids = updates
2067+ . iter ( )
2068+ . map ( |notification| match & notification. update {
2069+ wire:: SessionUpdate :: AgentThoughtChunk ( chunk) => chunk. message_id . clone ( ) ,
2070+ update => panic ! ( "expected thought chunk, got {update:?}" ) ,
2071+ } )
2072+ . collect :: < Vec < _ > > ( ) ;
2073+ assert_eq ! ( ids. len( ) , 3 ) ;
2074+ assert_eq ! ( ids[ 0 ] , ids[ 1 ] ) ;
2075+ assert_ne ! ( ids[ 0 ] , ids[ 2 ] ) ;
2076+ }
2077+
19742078 #[ test]
19752079 fn response_replacement_clears_and_remaps_message_ids_in_new_chunk_order ( ) {
19762080 let integration = AcpIntegration :: default ( ) ;
0 commit comments