worker: separate subworkers from workspace workers
This commit is contained in:
@@ -342,7 +342,7 @@ async fn feature_flags_default_to_core_tool_surface_only() {
|
||||
assert_eq!(names, vec!["Bash", "Edit", "Glob", "Grep", "Read", "Write"]);
|
||||
assert!(!names.iter().any(|name| name == "TaskCreate"));
|
||||
assert!(!names.iter().any(|name| name == "WebSearch"));
|
||||
assert!(!names.iter().any(|name| name == "SpawnWorker"));
|
||||
assert!(!names.iter().any(|name| name == "SubWorkerSpawn"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -386,7 +386,7 @@ permission = "write"
|
||||
assert!(names.iter().any(|name| name == "TaskUpdate"));
|
||||
assert!(names.iter().any(|name| name == "WebSearch"));
|
||||
assert!(names.iter().any(|name| name == "WebFetch"));
|
||||
assert!(!names.iter().any(|name| name == "SpawnWorker"));
|
||||
assert!(!names.iter().any(|name| name == "SubWorkerSpawn"));
|
||||
assert!(!names.iter().any(|name| name == "MemoryRead"));
|
||||
}
|
||||
|
||||
@@ -394,34 +394,34 @@ permission = "write"
|
||||
async fn project_role_tool_surfaces_keep_task_disabled_and_workers_role_scoped() {
|
||||
struct Case {
|
||||
role: &'static str,
|
||||
workers_enabled: bool,
|
||||
sub_worker_enabled: bool,
|
||||
}
|
||||
|
||||
let cases = [
|
||||
Case {
|
||||
role: "orchestrator",
|
||||
workers_enabled: true,
|
||||
sub_worker_enabled: true,
|
||||
},
|
||||
Case {
|
||||
role: "coder",
|
||||
workers_enabled: false,
|
||||
sub_worker_enabled: false,
|
||||
},
|
||||
Case {
|
||||
role: "intake",
|
||||
workers_enabled: false,
|
||||
sub_worker_enabled: false,
|
||||
},
|
||||
Case {
|
||||
role: "reviewer",
|
||||
workers_enabled: false,
|
||||
sub_worker_enabled: false,
|
||||
},
|
||||
Case {
|
||||
role: "companion",
|
||||
workers_enabled: false,
|
||||
sub_worker_enabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let delegation = if case.workers_enabled {
|
||||
let delegation = if case.sub_worker_enabled {
|
||||
r#"
|
||||
[[delegation_scope.allow]]
|
||||
target = "/tmp"
|
||||
@@ -446,8 +446,8 @@ max_tokens = 100
|
||||
[feature.task]
|
||||
enabled = false
|
||||
|
||||
[feature.workers]
|
||||
enabled = {workers_enabled}
|
||||
[feature.sub_worker]
|
||||
enabled = {sub_worker_enabled}
|
||||
|
||||
[[scope.allow]]
|
||||
target = "./"
|
||||
@@ -455,7 +455,7 @@ permission = "write"
|
||||
{delegation}
|
||||
"#,
|
||||
role = case.role,
|
||||
workers_enabled = case.workers_enabled,
|
||||
sub_worker_enabled = case.sub_worker_enabled,
|
||||
delegation = delegation,
|
||||
);
|
||||
let client = MockClient::new(simple_text_events());
|
||||
@@ -474,16 +474,16 @@ permission = "write"
|
||||
case.role
|
||||
);
|
||||
assert_eq!(
|
||||
names.iter().any(|name| name == "SpawnWorker"),
|
||||
case.workers_enabled,
|
||||
"{} role Worker tool exposure mismatch: {names:?}",
|
||||
names.iter().any(|name| name == "SubWorkerSpawn"),
|
||||
case.sub_worker_enabled,
|
||||
"{} role SubWorker tool exposure mismatch: {names:?}",
|
||||
case.role
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workers_feature_requires_delegation_scope() {
|
||||
async fn sub_worker_feature_requires_delegation_scope() {
|
||||
let manifest = r#"
|
||||
[worker]
|
||||
name = "worker-management-feature-test"
|
||||
@@ -496,7 +496,7 @@ model_id = "test-model"
|
||||
[engine]
|
||||
max_tokens = 100
|
||||
|
||||
[feature.workers]
|
||||
[feature.sub_worker]
|
||||
enabled = true
|
||||
|
||||
[[scope.allow]]
|
||||
@@ -510,7 +510,7 @@ permission = "write"
|
||||
assert!(result.is_err());
|
||||
let message = result.err().unwrap().to_string();
|
||||
assert!(
|
||||
message.contains("[feature.workers].enabled = true requires non-empty"),
|
||||
message.contains("[feature.sub_worker].enabled = true requires non-empty"),
|
||||
"unexpected error: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Integration tests for the `SpawnWorker` tool.
|
||||
//! Integration tests for the `SubWorkerSpawn` tool.
|
||||
//!
|
||||
//! These tests exercise the tool's worker-allocation delegation, subprocess
|
||||
//! launch, socket handoff, and `spawned_workers.json` write through an injected
|
||||
@@ -24,7 +24,7 @@ use tokio::net::UnixListener;
|
||||
use worker::runtime::dir::{RuntimeDir, SpawnedWorkerRecord};
|
||||
use worker::runtime::worker_allocation::{self, LockFileGuard};
|
||||
use worker::spawn::registry::SpawnedWorkerRegistry;
|
||||
use worker::spawn::tool::spawn_worker_tool_with_runtime_command;
|
||||
use worker::spawn::tool::sub_worker_spawn_tool_with_runtime_command;
|
||||
|
||||
/// Serialises tests that mutate `YOI_RUNTIME_DIR` across the
|
||||
/// thread-pooled test harness.
|
||||
@@ -203,7 +203,7 @@ fn which_sh() -> String {
|
||||
}
|
||||
|
||||
/// Tests don't exercise the model — they intercept the spawned
|
||||
/// child via a mock socket — but `spawn_worker_tool` needs a value to
|
||||
/// child via a mock socket — but `sub_worker_spawn_tool` needs a value to
|
||||
/// embed in the overlay TOML. Any well-formed `ModelManifest` works.
|
||||
fn dummy_model() -> ModelManifest {
|
||||
ModelManifest {
|
||||
@@ -289,7 +289,7 @@ async fn spawn_worker_launches_runtime_in_workspace_and_process_cwd() {
|
||||
let received = accept_one_method(listener);
|
||||
|
||||
let registry = SpawnedWorkerRegistry::new(spawner_rd);
|
||||
let def = spawn_worker_tool_with_runtime_command(
|
||||
let def = sub_worker_spawn_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
@@ -349,7 +349,7 @@ async fn spawn_worker_omitted_cwd_preserves_spawner_cwd() {
|
||||
let received = accept_one_method(listener);
|
||||
|
||||
let registry = SpawnedWorkerRegistry::new(spawner_rd);
|
||||
let def = spawn_worker_tool_with_runtime_command(
|
||||
let def = sub_worker_spawn_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
@@ -400,7 +400,7 @@ async fn spawn_worker_delegates_scope_and_sends_run() {
|
||||
|
||||
let registry = SpawnedWorkerRegistry::new(spawner_rd.clone());
|
||||
let spawner_scope = shared_scope_for(allow_root.path());
|
||||
let def = spawn_worker_tool_with_runtime_command(
|
||||
let def = sub_worker_spawn_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket.clone(),
|
||||
runtime_base.clone(),
|
||||
@@ -493,7 +493,7 @@ async fn spawn_worker_requires_explicit_delegation_even_with_direct_scope() {
|
||||
assert!(direct.is_writable(&allow_root.path().join("direct.txt")));
|
||||
|
||||
let registry = SpawnedWorkerRegistry::new(spawner_rd.clone());
|
||||
let def = spawn_worker_tool_with_runtime_command(
|
||||
let def = sub_worker_spawn_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
@@ -560,7 +560,7 @@ async fn spawn_worker_rejects_child_non_recursive_scope_under_parent_non_recursi
|
||||
let manifest = dummy_manifest_with_scopes(direct_scope, delegation_scope);
|
||||
|
||||
let registry = SpawnedWorkerRegistry::new(spawner_rd.clone());
|
||||
let def = spawn_worker_tool_with_runtime_command(
|
||||
let def = sub_worker_spawn_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
@@ -612,7 +612,7 @@ async fn spawn_worker_rejects_scope_outside_spawner() {
|
||||
|
||||
let registry = SpawnedWorkerRegistry::new(spawner_rd);
|
||||
let spawner_scope = shared_scope_for(allow_root.path());
|
||||
let def = spawn_worker_tool_with_runtime_command(
|
||||
let def = sub_worker_spawn_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
@@ -686,7 +686,7 @@ async fn spawn_worker_rolls_back_reservation_when_socket_never_appears() {
|
||||
|
||||
let registry = SpawnedWorkerRegistry::new(spawner_rd);
|
||||
let spawner_scope = shared_scope_for(allow_root.path());
|
||||
let def = spawn_worker_tool_with_runtime_command(
|
||||
let def = sub_worker_spawn_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Integration tests for the worker-comm tools (`SendToWorker`,
|
||||
//! `ReadWorkerOutput`, `StopWorker`).
|
||||
//! Integration tests for the worker-comm tools (`SubWorkerSend`,
|
||||
//! `SubWorkerReadOutput`, `SubWorkerStop`).
|
||||
//!
|
||||
//! The real child Worker binary is not started. Instead each test stands
|
||||
//! up a mock `UnixListener` that speaks the socket protocol directly:
|
||||
@@ -25,7 +25,9 @@ use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use worker::runtime::dir::{RuntimeDir, SpawnedWorkerRecord};
|
||||
use worker::runtime::worker_allocation::{self, LockFileGuard};
|
||||
use worker::spawn::comm_tools::{read_worker_output_tool, send_to_worker_tool, stop_worker_tool};
|
||||
use worker::spawn::comm_tools::{
|
||||
sub_worker_read_output_tool, sub_worker_send_tool, sub_worker_stop_tool,
|
||||
};
|
||||
use worker::spawn::registry::SpawnedWorkerRegistry;
|
||||
|
||||
/// Serialises env-mutating tests. The test harness runs tasks across
|
||||
@@ -148,7 +150,7 @@ fn accept_one_method(listener: UnixListener) -> JoinHandle<Option<Method>> {
|
||||
}
|
||||
|
||||
/// Accept one connection, send the protocol's connect-time snapshot,
|
||||
/// read one `Method`, then write `response` back. Used by `SendToWorker`
|
||||
/// read one `Method`, then write `response` back. Used by `SubWorkerSend`
|
||||
/// tests to mock the real controller's `TurnStart` acknowledgement (or
|
||||
/// its `AlreadyRunning` rejection).
|
||||
fn accept_method_and_respond(
|
||||
@@ -171,7 +173,7 @@ fn accept_method_and_respond(
|
||||
|
||||
/// Pretend to be a spawned Worker whose connect-time snapshot carries a
|
||||
/// fixed set of assistant items. Sends `Event::Snapshot` immediately on
|
||||
/// every accept — the real Worker does the same, so `ReadWorkerOutput`'s
|
||||
/// every accept — the real Worker does the same, so `SubWorkerReadOutput`'s
|
||||
/// `fetch_history` just consumes the first non-Alert event.
|
||||
fn serve_history(listener: UnixListener, items: Vec<Item>) -> JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
@@ -249,7 +251,7 @@ fn assistant(text: &str) -> Item {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SendToWorker
|
||||
// SubWorkerSend
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
@@ -257,11 +259,11 @@ async fn send_to_worker_delivers_run_method() {
|
||||
let (tmp, registry, _rd) = setup_registry().await;
|
||||
let (socket, listener) = bind_mock_socket(tmp.path(), "child").await;
|
||||
// Mock the controller's accept path: after reading the method,
|
||||
// ack with `TurnStart` so `SendToWorker`'s confirmation loop succeeds.
|
||||
// ack with `TurnStart` so `SubWorkerSend`'s confirmation loop succeeds.
|
||||
let received = accept_method_and_respond(listener, Event::TurnStart { turn: 1 });
|
||||
register_child(®istry, "child", &socket, tmp.path()).await;
|
||||
|
||||
let def = send_to_worker_tool(registry);
|
||||
let def = sub_worker_send_tool(registry);
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "child", "message": "hello there" }).to_string();
|
||||
let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
|
||||
@@ -284,7 +286,7 @@ async fn send_to_worker_delivers_run_method() {
|
||||
#[tokio::test]
|
||||
async fn send_to_worker_errors_on_unknown_worker() {
|
||||
let (_tmp, registry, _rd) = setup_registry().await;
|
||||
let def = send_to_worker_tool(registry);
|
||||
let def = sub_worker_send_tool(registry);
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "nope", "message": "hi" }).to_string();
|
||||
let err = tool.execute(&input, Default::default()).await.unwrap_err();
|
||||
@@ -306,7 +308,7 @@ async fn send_to_worker_errors_when_worker_already_running() {
|
||||
);
|
||||
register_child(®istry, "child", &socket, tmp.path()).await;
|
||||
|
||||
let def = send_to_worker_tool(registry);
|
||||
let def = sub_worker_send_tool(registry);
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "child", "message": "hi" }).to_string();
|
||||
let err = tool.execute(&input, Default::default()).await.unwrap_err();
|
||||
@@ -323,7 +325,7 @@ async fn send_to_worker_errors_when_worker_already_running() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReadWorkerOutput
|
||||
// SubWorkerReadOutput
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
@@ -339,7 +341,7 @@ async fn read_worker_output_returns_new_assistant_text_then_empty_on_second_call
|
||||
];
|
||||
let _server = serve_history(listener, items);
|
||||
|
||||
let def = read_worker_output_tool(registry);
|
||||
let def = sub_worker_read_output_tool(registry);
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "child" }).to_string();
|
||||
|
||||
@@ -370,7 +372,7 @@ async fn read_worker_output_reports_stopped_on_dead_socket() {
|
||||
let dead_socket = tmp.path().join("dead.sock");
|
||||
register_child(®istry, "child", &dead_socket, tmp.path()).await;
|
||||
|
||||
let def = read_worker_output_tool(registry);
|
||||
let def = sub_worker_read_output_tool(registry);
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "child" }).to_string();
|
||||
let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
|
||||
@@ -378,7 +380,7 @@ async fn read_worker_output_reports_stopped_on_dead_socket() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// StopWorker
|
||||
// SubWorkerStop
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
@@ -408,7 +410,7 @@ async fn stop_worker_sends_shutdown_and_releases_scope() {
|
||||
|
||||
// Seed workers.json with a restored top-level `spawner` allocation whose
|
||||
// scope_deny contains the delegated child path plus the live child
|
||||
// allocation — mimics a parent resumed after SpawnWorker.
|
||||
// allocation — mimics a parent resumed after SubWorkerSpawn.
|
||||
{
|
||||
let mut g = LockFileGuard::open(&lock_path).unwrap();
|
||||
let rule = ScopeRule {
|
||||
@@ -451,7 +453,7 @@ async fn stop_worker_sends_shutdown_and_releases_scope() {
|
||||
let received = accept_one_method(listener);
|
||||
register_child(®istry, "child", &socket, tmp.path()).await;
|
||||
|
||||
let def = stop_worker_tool(registry.clone());
|
||||
let def = sub_worker_stop_tool(registry.clone());
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "child" }).to_string();
|
||||
let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
|
||||
@@ -492,11 +494,11 @@ async fn stop_worker_succeeds_even_when_child_unreachable() {
|
||||
}
|
||||
|
||||
// No live listener — socket never bound. Registered record points
|
||||
// at a dead path. StopWorker should still clean up local bookkeeping.
|
||||
// at a dead path. SubWorkerStop should still clean up local bookkeeping.
|
||||
let dead_socket = tmp.path().join("dead.sock");
|
||||
register_child(®istry, "child", &dead_socket, tmp.path()).await;
|
||||
|
||||
let def = stop_worker_tool(registry.clone());
|
||||
let def = sub_worker_stop_tool(registry.clone());
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "child" }).to_string();
|
||||
let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
|
||||
@@ -550,7 +552,7 @@ async fn restored_registry_uses_worker_state_without_runtime_file() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let def = send_to_worker_tool(restored.clone());
|
||||
let def = sub_worker_send_tool(restored.clone());
|
||||
let (_meta, tool) = def();
|
||||
let input = json!({ "name": "child", "message": "after restart" }).to_string();
|
||||
tool.execute(&input, Default::default()).await.unwrap();
|
||||
@@ -562,7 +564,7 @@ async fn restored_registry_uses_worker_state_without_runtime_file() {
|
||||
other => panic!("expected Run, got {other:?}"),
|
||||
}
|
||||
|
||||
let def = stop_worker_tool(restored.clone());
|
||||
let def = sub_worker_stop_tool(restored.clone());
|
||||
let (_meta, tool) = def();
|
||||
tool.execute(&json!({ "name": "child" }).to_string(), Default::default())
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user