runtime: route notifications through worker inbox
This commit is contained in:
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkerInputKind {
|
||||
User,
|
||||
System,
|
||||
Notify,
|
||||
Compact,
|
||||
ListRewindTargets,
|
||||
RegisterPeer,
|
||||
@@ -38,15 +38,35 @@ impl WorkerInput {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system(content: impl Into<String>) -> Self {
|
||||
pub fn notify(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind: WorkerInputKind::System,
|
||||
kind: WorkerInputKind::Notify,
|
||||
content: content.into(),
|
||||
segments: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::WorkerInput;
|
||||
|
||||
#[test]
|
||||
fn notify_is_an_operation_and_legacy_system_kind_is_rejected() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(WorkerInput::notify("message")).unwrap(),
|
||||
serde_json::json!({ "kind": "notify", "content": "message" })
|
||||
);
|
||||
assert!(
|
||||
serde_json::from_value::<WorkerInput>(serde_json::json!({
|
||||
"kind": "system",
|
||||
"content": "message"
|
||||
}))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledgement returned after input is accepted into the Worker.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WorkerInteractionAck {
|
||||
|
||||
@@ -2392,9 +2392,9 @@ fn input_protocol_event(input: &WorkerInput) -> protocol::Event {
|
||||
}]
|
||||
}),
|
||||
},
|
||||
WorkerInputKind::System => protocol::Event::SystemItem {
|
||||
WorkerInputKind::Notify => protocol::Event::SystemItem {
|
||||
item: serde_json::json!({
|
||||
"kind": "embedded_worker_system_input",
|
||||
"kind": "embedded_worker_notification",
|
||||
"content": input.content.clone(),
|
||||
}),
|
||||
},
|
||||
@@ -3148,7 +3148,7 @@ mod tests {
|
||||
fn create_worker_rejects_system_initial_input_without_persisting_worker() {
|
||||
let runtime = runtime_with_backend();
|
||||
let mut request = task_request("system initial input");
|
||||
request.initial_input = Some(WorkerInput::system("role/system belongs in config bundle"));
|
||||
request.initial_input = Some(WorkerInput::notify("role/system belongs in config bundle"));
|
||||
|
||||
let error = runtime.create_worker(request).unwrap_err();
|
||||
assert!(matches!(
|
||||
@@ -3390,7 +3390,7 @@ mod tests {
|
||||
.send_input(&detail.worker_ref, WorkerInput::user("hello"))
|
||||
.unwrap();
|
||||
runtime
|
||||
.send_input(&detail.worker_ref, WorkerInput::system("note"))
|
||||
.send_input(&detail.worker_ref, WorkerInput::notify("note"))
|
||||
.unwrap();
|
||||
|
||||
let observations = runtime
|
||||
@@ -3550,7 +3550,7 @@ mod tests {
|
||||
.send_input(&worker.worker_ref, WorkerInput::user("first"))
|
||||
.unwrap();
|
||||
runtime
|
||||
.send_input(&worker.worker_ref, WorkerInput::system("second"))
|
||||
.send_input(&worker.worker_ref, WorkerInput::notify("second"))
|
||||
.unwrap();
|
||||
runtime
|
||||
.stop_worker(&worker.worker_ref, Some("finished".to_string()))
|
||||
|
||||
@@ -793,6 +793,14 @@ fn method_starts_turn(method: &Method) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn accepted_notify_run_state(status: WorkerStatus, auto_run: bool) -> WorkerExecutionRunState {
|
||||
match status {
|
||||
WorkerStatus::Running => WorkerExecutionRunState::Busy,
|
||||
WorkerStatus::Idle if auto_run => WorkerExecutionRunState::Busy,
|
||||
WorkerStatus::Idle | WorkerStatus::Paused => WorkerExecutionRunState::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
fn accepted_run_state_for_method(method: &Method) -> WorkerExecutionRunState {
|
||||
match method {
|
||||
Method::Run { .. }
|
||||
@@ -1098,6 +1106,29 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
if input.kind == WorkerInputKind::Notify {
|
||||
let status = worker.shared_state.get_status();
|
||||
let accepted_run_state = accepted_notify_run_state(status, true);
|
||||
let claimed_here = status == WorkerStatus::Idle
|
||||
&& busy
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok();
|
||||
let result = self.send_method(
|
||||
WorkerExecutionOperation::Input,
|
||||
worker,
|
||||
Method::Notify {
|
||||
message: input.content,
|
||||
auto_run: true,
|
||||
},
|
||||
accepted_run_state,
|
||||
);
|
||||
if claimed_here && result.outcome != crate::execution::WorkerExecutionOutcome::Accepted
|
||||
{
|
||||
busy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if worker.shared_state.get_status() != WorkerStatus::Idle
|
||||
|| busy
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
@@ -1115,10 +1146,9 @@ where
|
||||
.segments
|
||||
.unwrap_or_else(|| vec![Segment::text(input.content.trim().to_string())]),
|
||||
},
|
||||
WorkerInputKind::System => Method::Notify {
|
||||
message: input.content,
|
||||
auto_run: true,
|
||||
},
|
||||
WorkerInputKind::Notify => {
|
||||
unreachable!("Notify input is dispatched before the turn-start busy guard")
|
||||
}
|
||||
WorkerInputKind::Compact => Method::Compact,
|
||||
WorkerInputKind::ListRewindTargets => Method::ListRewindTargets,
|
||||
WorkerInputKind::RegisterPeer => Method::RegisterPeer {
|
||||
@@ -1159,6 +1189,28 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
if let Method::Notify { auto_run, .. } = &method {
|
||||
let auto_run = *auto_run;
|
||||
let status = worker.shared_state.get_status();
|
||||
let accepted_run_state = accepted_notify_run_state(status, auto_run);
|
||||
let claimed_here = status == WorkerStatus::Idle
|
||||
&& auto_run
|
||||
&& busy
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok();
|
||||
let result = self.send_method(
|
||||
WorkerExecutionOperation::ProtocolMethod,
|
||||
worker,
|
||||
method,
|
||||
accepted_run_state,
|
||||
);
|
||||
if claimed_here && result.outcome != crate::execution::WorkerExecutionOutcome::Accepted
|
||||
{
|
||||
busy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let starts_turn = method_starts_turn(&method);
|
||||
if starts_turn
|
||||
&& (worker.shared_state.get_status() != WorkerStatus::Idle
|
||||
@@ -1298,6 +1350,26 @@ mod tests {
|
||||
use manifest::{Scope, WorkerManifest};
|
||||
use session_store::WorkerMetadataStore;
|
||||
|
||||
#[test]
|
||||
fn notify_run_state_allows_running_worker_inbox_delivery() {
|
||||
assert_eq!(
|
||||
accepted_notify_run_state(WorkerStatus::Running, true),
|
||||
WorkerExecutionRunState::Busy
|
||||
);
|
||||
assert_eq!(
|
||||
accepted_notify_run_state(WorkerStatus::Idle, true),
|
||||
WorkerExecutionRunState::Busy
|
||||
);
|
||||
assert_eq!(
|
||||
accepted_notify_run_state(WorkerStatus::Idle, false),
|
||||
WorkerExecutionRunState::Idle
|
||||
);
|
||||
assert_eq!(
|
||||
accepted_notify_run_state(WorkerStatus::Paused, true),
|
||||
WorkerExecutionRunState::Idle
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockClient {
|
||||
responses: Arc<Vec<Vec<LlmEvent>>>,
|
||||
|
||||
Reference in New Issue
Block a user