Track runtime tasks with structured task packets

Replace the oversized packet model with the requested JSON-friendly packet shape and thread it through the in-memory task registry. Add the RunTaskPacket tool so callers can launch packet-backed tasks directly while preserving existing task creation flows.

Constraint: The existing task system and tool surface had to keep TaskCreate behavior intact while adding packet-backed execution

Rejected: Add a second parallel packet registry | would duplicate task lifecycle state

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Keep TaskPacket aligned with the tool schema and task registry serialization when extending the packet contract

Tested: cargo build --workspace; cargo test --workspace

Not-tested: live end-to-end invocation of RunTaskPacket through an interactive CLI session
This commit is contained in:
Yeachan-Heo
2026-04-04 15:11:26 +00:00
parent 340d4e2b9f
commit dbfc9d521c
4 changed files with 203 additions and 493 deletions

View File

@@ -17,6 +17,7 @@ use runtime::{
permission_enforcer::{EnforcementResult, PermissionEnforcer},
read_file,
summary_compression::compress_summary_text,
TaskPacket,
task_registry::TaskRegistry,
team_cron_registry::{CronRegistry, TeamRegistry},
worker_boot::{WorkerReadySnapshot, WorkerRegistry},
@@ -755,6 +756,38 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
}),
required_permission: PermissionMode::DangerFullAccess,
},
ToolSpec {
name: "RunTaskPacket",
description: "Create a background task from a structured task packet.",
input_schema: json!({
"type": "object",
"properties": {
"objective": { "type": "string" },
"scope": { "type": "string" },
"repo": { "type": "string" },
"branch_policy": { "type": "string" },
"acceptance_tests": {
"type": "array",
"items": { "type": "string" }
},
"commit_policy": { "type": "string" },
"reporting_contract": { "type": "string" },
"escalation_policy": { "type": "string" }
},
"required": [
"objective",
"scope",
"repo",
"branch_policy",
"acceptance_tests",
"commit_policy",
"reporting_contract",
"escalation_policy"
],
"additionalProperties": false
}),
required_permission: PermissionMode::DangerFullAccess,
},
ToolSpec {
name: "TaskGet",
description: "Get the status and details of a background task by ID.",
@@ -1177,6 +1210,7 @@ fn execute_tool_with_enforcer(
from_value::<AskUserQuestionInput>(input).and_then(run_ask_user_question)
}
"TaskCreate" => from_value::<TaskCreateInput>(input).and_then(run_task_create),
"RunTaskPacket" => from_value::<TaskPacket>(input).and_then(run_task_packet),
"TaskGet" => from_value::<TaskIdInput>(input).and_then(run_task_get),
"TaskList" => run_task_list(input.clone()),
"TaskStop" => from_value::<TaskIdInput>(input).and_then(run_task_stop),
@@ -1285,6 +1319,24 @@ fn run_task_create(input: TaskCreateInput) -> Result<String, String> {
"status": task.status,
"prompt": task.prompt,
"description": task.description,
"task_packet": task.task_packet,
"created_at": task.created_at
}))
}
#[allow(clippy::needless_pass_by_value)]
fn run_task_packet(input: TaskPacket) -> Result<String, String> {
let registry = global_task_registry();
let task = registry
.create_from_packet(input)
.map_err(|error| error.to_string())?;
to_pretty_json(json!({
"task_id": task.task_id,
"status": task.status,
"prompt": task.prompt,
"description": task.description,
"task_packet": task.task_packet,
"created_at": task.created_at
}))
}
@@ -1298,6 +1350,7 @@ fn run_task_get(input: TaskIdInput) -> Result<String, String> {
"status": task.status,
"prompt": task.prompt,
"description": task.description,
"task_packet": task.task_packet,
"created_at": task.created_at,
"updated_at": task.updated_at,
"messages": task.messages,
@@ -1318,6 +1371,7 @@ fn run_task_list(_input: Value) -> Result<String, String> {
"status": t.status,
"prompt": t.prompt,
"description": t.description,
"task_packet": t.task_packet,
"created_at": t.created_at,
"updated_at": t.updated_at,
"team_id": t.team_id
@@ -4897,13 +4951,14 @@ mod tests {
use super::{
agent_permission_policy, allowed_tools_for_subagent, classify_lane_failure,
execute_agent_with_spawn, execute_tool, final_assistant_text, mvp_tool_specs,
permission_mode_from_plugin, persist_agent_terminal_state, push_output_block, AgentInput,
AgentJob, GlobalToolRegistry, LaneEventName, LaneFailureClass, SubagentToolExecutor,
permission_mode_from_plugin, persist_agent_terminal_state, push_output_block,
run_task_packet, AgentInput, AgentJob, GlobalToolRegistry, LaneEventName,
LaneFailureClass, SubagentToolExecutor,
};
use api::OutputContentBlock;
use runtime::{
permission_enforcer::PermissionEnforcer, ApiRequest, AssistantEvent, ConversationRuntime,
PermissionMode, PermissionPolicy, RuntimeError, Session, ToolExecutor,
PermissionMode, PermissionPolicy, RuntimeError, Session, TaskPacket, ToolExecutor,
};
use serde_json::json;
@@ -6996,6 +7051,34 @@ printf 'pwsh:%s' "$1"
assert_eq!(output["stdout"], "ok");
}
#[test]
fn run_task_packet_creates_packet_backed_task() {
let result = run_task_packet(TaskPacket {
objective: "Ship packetized runtime task".to_string(),
scope: "runtime/task system".to_string(),
repo: "claw-code-parity".to_string(),
branch_policy: "origin/main only".to_string(),
acceptance_tests: vec![
"cargo build --workspace".to_string(),
"cargo test --workspace".to_string(),
],
commit_policy: "single commit".to_string(),
reporting_contract: "print build/test result and sha".to_string(),
escalation_policy: "manual escalation".to_string(),
})
.expect("task packet should create a task");
let output: serde_json::Value = serde_json::from_str(&result).expect("json");
assert_eq!(output["status"], "created");
assert_eq!(output["prompt"], "Ship packetized runtime task");
assert_eq!(output["description"], "runtime/task system");
assert_eq!(output["task_packet"]["repo"], "claw-code-parity");
assert_eq!(
output["task_packet"]["acceptance_tests"][1],
"cargo test --workspace"
);
}
struct TestServer {
addr: SocketAddr,
shutdown: Option<std::sync::mpsc::Sender<()>>,