Skip to content

Commit 9461522

Browse files
committed
feat(tools): expose WorkerObserveCompletion tool; add provider-degraded classification tests
observe_completion() on WorkerRegistry classifies finish_reason into Finished vs Failed (finish='unknown' + 0 tokens = provider degraded). This logic existed in the runtime but had no tool wrapper — clawhip could not call it. Added WorkerObserveCompletion as a first-class tool. Tool schema: { worker_id, finish_reason: string, tokens_output: integer } Handler: run_worker_observe_completion -> global_worker_registry().observe_completion() Tests added: - worker_observe_completion_success_finish_sets_finished_status finish=end_turn + tokens=512 -> status=finished - worker_observe_completion_degraded_provider_sets_failed_status finish=unknown + tokens=0 -> status=failed, last_error populated 89 tool tests passing, 0 failing.
1 parent c08f060 commit 9461522

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

rust/crates/tools/src/lib.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
14941525
fn 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)]
22282266
struct 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

Comments
 (0)