@@ -963,6 +963,21 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
963963 } ) ,
964964 required_permission: PermissionMode :: DangerFullAccess ,
965965 } ,
966+ ToolSpec {
967+ name: "WorkerObserveCompletion" ,
968+ description: "Report session completion to the worker, classifying finish_reason into Finished or Failed (provider-degraded). Use after the opencode session completes to advance the worker to its terminal state." ,
969+ input_schema: json!( {
970+ "type" : "object" ,
971+ "properties" : {
972+ "worker_id" : { "type" : "string" } ,
973+ "finish_reason" : { "type" : "string" } ,
974+ "tokens_output" : { "type" : "integer" , "minimum" : 0 }
975+ } ,
976+ "required" : [ "worker_id" , "finish_reason" , "tokens_output" ] ,
977+ "additionalProperties" : false
978+ } ) ,
979+ required_permission: PermissionMode :: DangerFullAccess ,
980+ } ,
966981 ToolSpec {
967982 name: "TeamCreate" ,
968983 description: "Create a team of sub-agents for parallel task execution." ,
@@ -1229,6 +1244,10 @@ fn execute_tool_with_enforcer(
12291244 }
12301245 "WorkerRestart" => from_value :: < WorkerIdInput > ( input) . and_then ( run_worker_restart) ,
12311246 "WorkerTerminate" => from_value :: < WorkerIdInput > ( input) . and_then ( run_worker_terminate) ,
1247+ "WorkerObserveCompletion" => {
1248+ from_value :: < WorkerObserveCompletionInput > ( input)
1249+ . and_then ( run_worker_observe_completion)
1250+ }
12321251 "TeamCreate" => from_value :: < TeamCreateInput > ( input) . and_then ( run_team_create) ,
12331252 "TeamDelete" => from_value :: < TeamDeleteInput > ( input) . and_then ( run_team_delete) ,
12341253 "CronCreate" => from_value :: < CronCreateInput > ( input) . and_then ( run_cron_create) ,
@@ -1490,6 +1509,18 @@ fn run_worker_terminate(input: WorkerIdInput) -> Result<String, String> {
14901509 to_pretty_json ( worker)
14911510}
14921511
1512+ #[ allow( clippy:: needless_pass_by_value) ]
1513+ fn run_worker_observe_completion (
1514+ input : WorkerObserveCompletionInput ,
1515+ ) -> Result < String , String > {
1516+ let worker = global_worker_registry ( ) . observe_completion (
1517+ & input. worker_id ,
1518+ & input. finish_reason ,
1519+ input. tokens_output ,
1520+ ) ?;
1521+ to_pretty_json ( worker)
1522+ }
1523+
14931524#[ allow( clippy:: needless_pass_by_value) ]
14941525fn run_team_create ( input : TeamCreateInput ) -> Result < String , String > {
14951526 let task_ids: Vec < String > = input
@@ -2224,6 +2255,13 @@ struct WorkerIdInput {
22242255 worker_id : String ,
22252256}
22262257
2258+ #[ derive( Debug , Deserialize ) ]
2259+ struct WorkerObserveCompletionInput {
2260+ worker_id : String ,
2261+ finish_reason : String ,
2262+ tokens_output : u64 ,
2263+ }
2264+
22272265#[ derive( Debug , Deserialize ) ]
22282266struct WorkerObserveInput {
22292267 worker_id : String ,
@@ -5739,6 +5777,63 @@ mod tests {
57395777 ) ;
57405778 }
57415779
5780+ #[ test]
5781+ fn worker_observe_completion_success_finish_sets_finished_status ( ) {
5782+ let created = execute_tool (
5783+ "WorkerCreate" ,
5784+ & json ! ( { "cwd" : "/tmp/observe-completion-test" , "trusted_roots" : [ "/tmp" ] } ) ,
5785+ )
5786+ . expect ( "WorkerCreate should succeed" ) ;
5787+ let output: serde_json:: Value = serde_json:: from_str ( & created) . expect ( "json" ) ;
5788+ let worker_id = output[ "worker_id" ] . as_str ( ) . expect ( "worker_id" ) . to_string ( ) ;
5789+
5790+ let completed = execute_tool (
5791+ "WorkerObserveCompletion" ,
5792+ & json ! ( {
5793+ "worker_id" : worker_id,
5794+ "finish_reason" : "end_turn" ,
5795+ "tokens_output" : 512
5796+ } ) ,
5797+ )
5798+ . expect ( "WorkerObserveCompletion should succeed" ) ;
5799+ let completed_output: serde_json:: Value = serde_json:: from_str ( & completed) . expect ( "json" ) ;
5800+ assert_eq ! ( completed_output[ "status" ] , "finished" ) ;
5801+ assert_eq ! ( completed_output[ "prompt_in_flight" ] , false ) ;
5802+ }
5803+
5804+ #[ test]
5805+ fn worker_observe_completion_degraded_provider_sets_failed_status ( ) {
5806+ let created = execute_tool (
5807+ "WorkerCreate" ,
5808+ & json ! ( { "cwd" : "/tmp/observe-degraded-test" , "trusted_roots" : [ "/tmp" ] } ) ,
5809+ )
5810+ . expect ( "WorkerCreate should succeed" ) ;
5811+ let output: serde_json:: Value = serde_json:: from_str ( & created) . expect ( "json" ) ;
5812+ let worker_id = output[ "worker_id" ] . as_str ( ) . expect ( "worker_id" ) . to_string ( ) ;
5813+
5814+ // finish=unknown + 0 tokens = degraded provider classification
5815+ let failed = execute_tool (
5816+ "WorkerObserveCompletion" ,
5817+ & json ! ( {
5818+ "worker_id" : worker_id,
5819+ "finish_reason" : "unknown" ,
5820+ "tokens_output" : 0
5821+ } ) ,
5822+ )
5823+ . expect ( "WorkerObserveCompletion should succeed" ) ;
5824+ let failed_output: serde_json:: Value = serde_json:: from_str ( & failed) . expect ( "json" ) ;
5825+ assert_eq ! (
5826+ failed_output[ "status" ] , "failed" ,
5827+ "finish=unknown + 0 tokens should classify as provider failure"
5828+ ) ;
5829+ assert_eq ! ( failed_output[ "prompt_in_flight" ] , false ) ;
5830+ // last_error should be set with provider failure message
5831+ assert ! (
5832+ !failed_output[ "last_error" ] . is_null( ) ,
5833+ "last_error should be populated for provider failure"
5834+ ) ;
5835+ }
5836+
57425837 #[ test]
57435838 fn worker_tools_detect_misdelivery_and_arm_prompt_replay ( ) {
57445839 let created = execute_tool (
0 commit comments