@@ -303,6 +303,7 @@ impl NativeDispatchState {
303303 if let Some ( observer) = & self . shutdown_entered {
304304 observer ( ) ;
305305 }
306+ let _ = self . lifecycle . recover_open_tokens ( ) ;
306307 self . dispatcher . close ( ) ;
307308 let quiesced = self . dispatcher . try_quiesce ( grace) ;
308309 let owner = self . owner ( ) ;
@@ -357,6 +358,19 @@ impl RunHandle {
357358
358359 fn cancel_native_tools ( & self ) {
359360 self . tool_cancel . cancel ( ) ;
361+ let lifecycle = {
362+ let phase = self
363+ . native_dispatch
364+ . lock ( )
365+ . unwrap_or_else ( |poisoned| poisoned. into_inner ( ) ) ;
366+ match & * phase {
367+ NativeDispatchPhase :: Ready ( state) => Some ( Arc :: clone ( & state. lifecycle ) ) ,
368+ _ => None ,
369+ }
370+ } ;
371+ if let Some ( lifecycle) = lifecycle {
372+ let _ = lifecycle. recover_open_tokens ( ) ;
373+ }
360374 }
361375
362376 fn native_dispatch_closed ( & self ) -> bool {
@@ -987,6 +1001,26 @@ impl AgentService {
9871001 . commit_step ( event_type, data, result)
9881002 }
9891003
1004+ /// Run-scoped capability engine used by `agent_runtime::tool_prepare`
1005+ /// and `agent_runtime::tool_commit`. Initializes native dispatch if needed.
1006+ pub fn capability_lifecycle (
1007+ & self ,
1008+ run_id : & str ,
1009+ ) -> Result < ( Arc < CapabilityLifecycle > , CapabilityOwner ) , RunContextError > {
1010+ let handle = self
1011+ . handle ( run_id)
1012+ . ok_or_else ( || RunContextError :: Missing {
1013+ run_id : run_id. to_string ( ) ,
1014+ } ) ?;
1015+ match self . native_dispatch_state ( run_id, & handle) ? {
1016+ Some ( state) => Ok ( ( Arc :: clone ( & state. lifecycle ) , state. capability_owner . clone ( ) ) ) ,
1017+ None => Err ( RunContextError :: InvalidMetadata {
1018+ run_id : run_id. to_string ( ) ,
1019+ reason : "native dispatch is closed" . to_string ( ) ,
1020+ } ) ,
1021+ }
1022+ }
1023+
9901024 /// Serial, validated native dispatch against the admitted registry snapshot.
9911025 ///
9921026 /// The live registry is not consulted. Durable event append uses the same
@@ -2952,6 +2986,7 @@ impl AgentService {
29522986 // observing the cancellation commits exactly this reason.
29532987 * handle. cancel_reason . lock ( ) . expect ( "cancel reason lock" ) = Some ( "requested" ) ;
29542988 handle. cancel . request ( CancellationReason :: Requested ) ;
2989+ drop ( store) ;
29552990 handle. cancel_native_tools ( ) ;
29562991 tracing:: debug!(
29572992 run_id,
@@ -4222,14 +4257,21 @@ impl DurableToolLifecycle for ServiceDurableLifecycle {
42224257 call_id : & str ,
42234258 result : & serde_json:: Value ,
42244259 ) -> Result < serde_json:: Value , LifecycleError > {
4260+ let tool_result = canonical_tool_result ( result) ?;
4261+ let event_type = if tool_result. ok {
4262+ "tool.completed"
4263+ } else {
4264+ "tool.failed"
4265+ } ;
4266+ let mut data = json ! ( {
4267+ "tool_call_id" : call_id,
4268+ "ok" : tool_result. ok,
4269+ } ) ;
4270+ if let Some ( error) = & tool_result. error {
4271+ data[ "error_code" ] = json ! ( error. code) ;
4272+ }
42254273 self . events
4226- . commit (
4227- "tool.completed" ,
4228- json ! ( {
4229- "tool_call_id" : call_id,
4230- "result" : result,
4231- } ) ,
4232- )
4274+ . commit_step ( event_type, data, Some ( & tool_result) )
42334275 . map_err ( |error| match error {
42344276 EventCommitError :: PersistFailed ( message) => {
42354277 LifecycleError :: ResultCommitFailed ( message)
@@ -4240,16 +4282,17 @@ impl DurableToolLifecycle for ServiceDurableLifecycle {
42404282 }
42414283
42424284 fn interrupt ( & self , call_id : & str ) -> Result < ( ) , LifecycleError > {
4285+ let tool_result =
4286+ ToolResult :: failure ( "interrupted_effect" , "effect interrupted by restart" ) ;
42434287 self . events
4244- . commit (
4288+ . commit_step (
42454289 "tool.failed" ,
42464290 json ! ( {
42474291 "tool_call_id" : call_id,
4248- "error" : {
4249- "code" : "interrupted" ,
4250- "message" : "execution was interrupted" ,
4251- }
4292+ "error_code" : "interrupted_effect" ,
4293+ "ok" : false ,
42524294 } ) ,
4295+ Some ( & tool_result) ,
42534296 )
42544297 . map_err ( map_event_commit_error)
42554298 }
@@ -4265,6 +4308,46 @@ fn map_event_commit_error(error: EventCommitError) -> LifecycleError {
42654308 }
42664309}
42674310
4311+ fn canonical_tool_result ( result : & JsonValue ) -> Result < ToolResult , LifecycleError > {
4312+ let ok = result
4313+ . get ( "ok" )
4314+ . and_then ( JsonValue :: as_bool)
4315+ . unwrap_or ( false ) ;
4316+ if ok {
4317+ let mut tool_result = ToolResult :: success (
4318+ result
4319+ . get ( "content" )
4320+ . and_then ( JsonValue :: as_str)
4321+ . unwrap_or ( "" )
4322+ . to_string ( ) ,
4323+ result. get ( "data" ) . cloned ( ) . unwrap_or_else ( || json ! ( { } ) ) ,
4324+ ) ;
4325+ tool_result. truncated = result
4326+ . get ( "truncated" )
4327+ . and_then ( JsonValue :: as_bool)
4328+ . unwrap_or ( false ) ;
4329+ if let Some ( artifacts) = result. get ( "artifacts" ) . and_then ( JsonValue :: as_array) {
4330+ tool_result. artifacts = artifacts
4331+ . iter ( )
4332+ . filter_map ( JsonValue :: as_str)
4333+ . map ( str:: to_string)
4334+ . collect ( ) ;
4335+ }
4336+ Ok ( tool_result)
4337+ } else {
4338+ let error = result. get ( "error" ) ;
4339+ let code = error
4340+ . and_then ( |value| value. get ( "code" ) )
4341+ . and_then ( JsonValue :: as_str)
4342+ . unwrap_or ( "tool_failed" ) ;
4343+ let message = error
4344+ . and_then ( |value| value. get ( "message" ) )
4345+ . and_then ( JsonValue :: as_str)
4346+ . unwrap_or ( "tool failed" ) ;
4347+ Ok ( ToolResult :: failure ( code, message) )
4348+ }
4349+ }
4350+
42684351struct ServiceEventCommitter {
42694352 store : Arc < RwLock < GatewayStore > > ,
42704353 persistence : Option < Arc < GatewayPersistence > > ,
0 commit comments