mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 17:14:49 +08:00
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.
This commit is contained in:
@@ -963,6 +963,21 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
}),
|
}),
|
||||||
required_permission: PermissionMode::DangerFullAccess,
|
required_permission: PermissionMode::DangerFullAccess,
|
||||||
},
|
},
|
||||||
|
ToolSpec {
|
||||||
|
name: "WorkerObserveCompletion",
|
||||||
|
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.",
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"worker_id": { "type": "string" },
|
||||||
|
"finish_reason": { "type": "string" },
|
||||||
|
"tokens_output": { "type": "integer", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"required": ["worker_id", "finish_reason", "tokens_output"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}),
|
||||||
|
required_permission: PermissionMode::DangerFullAccess,
|
||||||
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "TeamCreate",
|
name: "TeamCreate",
|
||||||
description: "Create a team of sub-agents for parallel task execution.",
|
description: "Create a team of sub-agents for parallel task execution.",
|
||||||
@@ -1229,6 +1244,10 @@ fn execute_tool_with_enforcer(
|
|||||||
}
|
}
|
||||||
"WorkerRestart" => from_value::<WorkerIdInput>(input).and_then(run_worker_restart),
|
"WorkerRestart" => from_value::<WorkerIdInput>(input).and_then(run_worker_restart),
|
||||||
"WorkerTerminate" => from_value::<WorkerIdInput>(input).and_then(run_worker_terminate),
|
"WorkerTerminate" => from_value::<WorkerIdInput>(input).and_then(run_worker_terminate),
|
||||||
|
"WorkerObserveCompletion" => {
|
||||||
|
from_value::<WorkerObserveCompletionInput>(input)
|
||||||
|
.and_then(run_worker_observe_completion)
|
||||||
|
}
|
||||||
"TeamCreate" => from_value::<TeamCreateInput>(input).and_then(run_team_create),
|
"TeamCreate" => from_value::<TeamCreateInput>(input).and_then(run_team_create),
|
||||||
"TeamDelete" => from_value::<TeamDeleteInput>(input).and_then(run_team_delete),
|
"TeamDelete" => from_value::<TeamDeleteInput>(input).and_then(run_team_delete),
|
||||||
"CronCreate" => from_value::<CronCreateInput>(input).and_then(run_cron_create),
|
"CronCreate" => from_value::<CronCreateInput>(input).and_then(run_cron_create),
|
||||||
@@ -1490,6 +1509,18 @@ fn run_worker_terminate(input: WorkerIdInput) -> Result<String, String> {
|
|||||||
to_pretty_json(worker)
|
to_pretty_json(worker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn run_worker_observe_completion(
|
||||||
|
input: WorkerObserveCompletionInput,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let worker = global_worker_registry().observe_completion(
|
||||||
|
&input.worker_id,
|
||||||
|
&input.finish_reason,
|
||||||
|
input.tokens_output,
|
||||||
|
)?;
|
||||||
|
to_pretty_json(worker)
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
fn run_team_create(input: TeamCreateInput) -> Result<String, String> {
|
fn run_team_create(input: TeamCreateInput) -> Result<String, String> {
|
||||||
let task_ids: Vec<String> = input
|
let task_ids: Vec<String> = input
|
||||||
@@ -2224,6 +2255,13 @@ struct WorkerIdInput {
|
|||||||
worker_id: String,
|
worker_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct WorkerObserveCompletionInput {
|
||||||
|
worker_id: String,
|
||||||
|
finish_reason: String,
|
||||||
|
tokens_output: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct WorkerObserveInput {
|
struct WorkerObserveInput {
|
||||||
worker_id: String,
|
worker_id: String,
|
||||||
@@ -5739,6 +5777,63 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worker_observe_completion_success_finish_sets_finished_status() {
|
||||||
|
let created = execute_tool(
|
||||||
|
"WorkerCreate",
|
||||||
|
&json!({"cwd": "/tmp/observe-completion-test", "trusted_roots": ["/tmp"]}),
|
||||||
|
)
|
||||||
|
.expect("WorkerCreate should succeed");
|
||||||
|
let output: serde_json::Value = serde_json::from_str(&created).expect("json");
|
||||||
|
let worker_id = output["worker_id"].as_str().expect("worker_id").to_string();
|
||||||
|
|
||||||
|
let completed = execute_tool(
|
||||||
|
"WorkerObserveCompletion",
|
||||||
|
&json!({
|
||||||
|
"worker_id": worker_id,
|
||||||
|
"finish_reason": "end_turn",
|
||||||
|
"tokens_output": 512
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("WorkerObserveCompletion should succeed");
|
||||||
|
let completed_output: serde_json::Value = serde_json::from_str(&completed).expect("json");
|
||||||
|
assert_eq!(completed_output["status"], "finished");
|
||||||
|
assert_eq!(completed_output["prompt_in_flight"], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worker_observe_completion_degraded_provider_sets_failed_status() {
|
||||||
|
let created = execute_tool(
|
||||||
|
"WorkerCreate",
|
||||||
|
&json!({"cwd": "/tmp/observe-degraded-test", "trusted_roots": ["/tmp"]}),
|
||||||
|
)
|
||||||
|
.expect("WorkerCreate should succeed");
|
||||||
|
let output: serde_json::Value = serde_json::from_str(&created).expect("json");
|
||||||
|
let worker_id = output["worker_id"].as_str().expect("worker_id").to_string();
|
||||||
|
|
||||||
|
// finish=unknown + 0 tokens = degraded provider classification
|
||||||
|
let failed = execute_tool(
|
||||||
|
"WorkerObserveCompletion",
|
||||||
|
&json!({
|
||||||
|
"worker_id": worker_id,
|
||||||
|
"finish_reason": "unknown",
|
||||||
|
"tokens_output": 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("WorkerObserveCompletion should succeed");
|
||||||
|
let failed_output: serde_json::Value = serde_json::from_str(&failed).expect("json");
|
||||||
|
assert_eq!(
|
||||||
|
failed_output["status"], "failed",
|
||||||
|
"finish=unknown + 0 tokens should classify as provider failure"
|
||||||
|
);
|
||||||
|
assert_eq!(failed_output["prompt_in_flight"], false);
|
||||||
|
// last_error should be set with provider failure message
|
||||||
|
assert!(
|
||||||
|
!failed_output["last_error"].is_null(),
|
||||||
|
"last_error should be populated for provider failure"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn worker_tools_detect_misdelivery_and_arm_prompt_replay() {
|
fn worker_tools_detect_misdelivery_and_arm_prompt_replay() {
|
||||||
let created = execute_tool(
|
let created = execute_tool(
|
||||||
|
|||||||
Reference in New Issue
Block a user