feat: weak companion progress notify
This commit is contained in:
@@ -125,6 +125,10 @@ impl PendingRun {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_auto_run_notification(status: PodStatus, auto_run: bool) -> bool {
|
||||
auto_run && status == PodStatus::Idle
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PodController — actor that owns a Pod
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -774,7 +778,7 @@ async fn controller_loop<C, St>(
|
||||
pending = Some(PendingRun::Run(input));
|
||||
}
|
||||
|
||||
Method::Notify { message } => {
|
||||
Method::Notify { message, auto_run } => {
|
||||
// Client-side live echo is delivered as `Event::SystemItem`
|
||||
// once the interceptor commits the corresponding
|
||||
// `LogEntry::SystemItem` entry — drained out of the
|
||||
@@ -784,10 +788,10 @@ async fn controller_loop<C, St>(
|
||||
// RUNNING / Paused: the buffer push is the entire
|
||||
// operation; an in-flight turn (or the next
|
||||
// Resume/Run) will drain it at its next
|
||||
// pending_history_appends. IDLE: auto-start a turn so the LLM
|
||||
// sees the buffered notification(s) without a human
|
||||
// Run.
|
||||
if shared_state.get_status() == PodStatus::Idle {
|
||||
// pending_history_appends. IDLE: only `auto_run`
|
||||
// notifications stage RunForNotification; weak progress
|
||||
// notices stay queued until an explicit run/resume.
|
||||
if should_auto_run_notification(shared_state.get_status(), auto_run) {
|
||||
pending = Some(PendingRun::RunForNotification(protocol::InvokeKind::Notify));
|
||||
}
|
||||
}
|
||||
@@ -1145,7 +1149,7 @@ where
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
Some(Method::Notify { message }) => {
|
||||
Some(Method::Notify { message, .. }) => {
|
||||
// Live echo arrives via `Event::SystemItem` once
|
||||
// the in-flight turn's next `pending_history_appends`
|
||||
// drains this entry through the interceptor.
|
||||
@@ -1337,6 +1341,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notification_auto_run_gate_only_allows_idle_auto_run() {
|
||||
assert!(should_auto_run_notification(PodStatus::Idle, true));
|
||||
assert!(!should_auto_run_notification(PodStatus::Idle, false));
|
||||
assert!(!should_auto_run_notification(PodStatus::Running, true));
|
||||
assert!(!should_auto_run_notification(PodStatus::Paused, true));
|
||||
}
|
||||
|
||||
struct DriveTurnEnv {
|
||||
// Held to keep the channel alive; without this `method_rx.recv()`
|
||||
// would observe channel-closed and confuse the select! arm.
|
||||
|
||||
@@ -913,7 +913,14 @@ where
|
||||
}
|
||||
|
||||
async fn send_peer_notify(socket_path: &Path, message: String) -> io::Result<()> {
|
||||
connect_and_send(socket_path, &Method::Notify { message }).await
|
||||
connect_and_send(
|
||||
socket_path,
|
||||
&Method::Notify {
|
||||
message,
|
||||
auto_run: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn json_content<T: Serialize>(value: &T) -> Result<String, ToolError> {
|
||||
@@ -1395,7 +1402,8 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
let method = reader.next::<Method>().await.unwrap().unwrap();
|
||||
if let Method::Notify { message } = method {
|
||||
if let Method::Notify { message, auto_run } = method {
|
||||
assert!(auto_run);
|
||||
tx.send(message).await.unwrap();
|
||||
} else {
|
||||
panic!("expected Notify, got {method:?}");
|
||||
|
||||
@@ -1025,6 +1025,7 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||
handle
|
||||
.send(Method::Notify {
|
||||
message: "turn finished".into(),
|
||||
auto_run: true,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1105,6 +1106,62 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_while_idle_with_auto_run_false_waits_for_explicit_run() {
|
||||
let client = MockClient::new(simple_text_events());
|
||||
let client_for_assert = client.clone();
|
||||
let pod = make_pod(client).await;
|
||||
let handle = spawn_controller(pod).await;
|
||||
|
||||
handle
|
||||
.send(Method::Notify {
|
||||
message: "progress snapshot".into(),
|
||||
auto_run: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
assert_eq!(handle.shared_state.get_status(), PodStatus::Idle);
|
||||
assert!(
|
||||
client_for_assert.captured_requests().is_empty(),
|
||||
"weak Notify must not stage RunForNotification while idle"
|
||||
);
|
||||
|
||||
handle.send(Method::run_text("continue")).await.unwrap();
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
loop {
|
||||
if !client_for_assert.captured_requests().is_empty() {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
tokio::time::Instant::now() < deadline,
|
||||
"explicit run did not reach the mock LLM"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
wait_for_status(&handle, PodStatus::Idle).await;
|
||||
let requests = client_for_assert.captured_requests();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
1,
|
||||
"explicit run should drain the queued notification"
|
||||
);
|
||||
let notify_in_request = requests[0].items.iter().any(|i| {
|
||||
i.as_text()
|
||||
.is_some_and(|t| t.contains("[Notification]") && t.contains("progress snapshot"))
|
||||
});
|
||||
assert!(
|
||||
notify_in_request,
|
||||
"queued weak notification must be history-backed on the next explicit run; got items: {:?}",
|
||||
requests[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|i| i.as_text())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pod_event_turn_ended_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||
let client = MockClient::new(simple_text_events());
|
||||
@@ -1259,6 +1316,7 @@ async fn notify_while_running_does_not_emit_already_running_error() {
|
||||
handle
|
||||
.send(Method::Notify {
|
||||
message: "ping".into(),
|
||||
auto_run: true,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user