Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce0e4e6820 | ||
|
|
923911f631 |
@@ -482,7 +482,7 @@ async fn structured_prompt(
|
|||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Json(body): Json<StructuredPromptRequest>,
|
Json(body): Json<StructuredPromptRequest>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match s.manager.queue_acp_prompt(id, body.prompt).await {
|
match s.manager.queue_acp_prompt(id, body.prompt, None).await {
|
||||||
Ok(QueueAcpPrompt::Dispatched) => StatusCode::NO_CONTENT.into_response(),
|
Ok(QueueAcpPrompt::Dispatched) => StatusCode::NO_CONTENT.into_response(),
|
||||||
Ok(QueueAcpPrompt::Queued) => StatusCode::ACCEPTED.into_response(),
|
Ok(QueueAcpPrompt::Queued) => StatusCode::ACCEPTED.into_response(),
|
||||||
Ok(QueueAcpPrompt::Full) => error_response(
|
Ok(QueueAcpPrompt::Full) => error_response(
|
||||||
|
|||||||
@@ -757,6 +757,7 @@ impl SessionManager {
|
|||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
text: String,
|
text: String,
|
||||||
|
row_metadata: Option<serde_json::Value>,
|
||||||
) -> Result<QueueAcpPrompt, SessionError> {
|
) -> Result<QueueAcpPrompt, SessionError> {
|
||||||
let (acp, stream) = {
|
let (acp, stream) = {
|
||||||
let sessions = self.inner.sessions.lock().await;
|
let sessions = self.inner.sessions.lock().await;
|
||||||
@@ -833,12 +834,19 @@ impl SessionManager {
|
|||||||
self.persist_prompt_queue(id).await;
|
self.persist_prompt_queue(id).await;
|
||||||
return Ok(QueueAcpPrompt::Queued);
|
return Ok(QueueAcpPrompt::Queued);
|
||||||
}
|
}
|
||||||
|
let mut data = serde_json::json!({
|
||||||
|
"content": {"type": "text", "text": text},
|
||||||
|
});
|
||||||
|
if let Some(extra) = &row_metadata {
|
||||||
|
if let (Some(base), Some(extra)) = (data.as_object_mut(), extra.as_object())
|
||||||
|
{
|
||||||
|
base.extend(extra.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||||
|
}
|
||||||
|
}
|
||||||
let row = TranscriptRow {
|
let row = TranscriptRow {
|
||||||
id: format!("user:{}", Uuid::new_v4()),
|
id: format!("user:{}", Uuid::new_v4()),
|
||||||
kind: TranscriptRowKind::User,
|
kind: TranscriptRowKind::User,
|
||||||
data: serde_json::json!({
|
data,
|
||||||
"content": {"type": "text", "text": text},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
let event = PersistedEvent::User { row: row.clone() };
|
let event = PersistedEvent::User { row: row.clone() };
|
||||||
let seq = match stream.log.append(&event) {
|
let seq = match stream.log.append(&event) {
|
||||||
@@ -1631,7 +1639,7 @@ impl SessionManager {
|
|||||||
let manager = SessionManager {
|
let manager = SessionManager {
|
||||||
inner: inner.clone(),
|
inner: inner.clone(),
|
||||||
};
|
};
|
||||||
let _ = manager.queue_acp_prompt(id, text).await;
|
let _ = manager.queue_acp_prompt(id, text, None).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2022,7 +2030,9 @@ impl SessionManager {
|
|||||||
if let Some(record) = self.inner.sessions.lock().await.get_mut(&id) {
|
if let Some(record) = self.inner.sessions.lock().await.get_mut(&id) {
|
||||||
record.acp_event_pump = Some(done);
|
record.acp_event_pump = Some(done);
|
||||||
}
|
}
|
||||||
// Drain any prompts that were queued before the daemon restarted.
|
// Drain any prompts that were queued before the daemon
|
||||||
|
// restarted. If none were queued, send a synthetic continue
|
||||||
|
// prompt so the agent resumes autonomously.
|
||||||
let queued_text = {
|
let queued_text = {
|
||||||
let stream = acp_events
|
let stream = acp_events
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -2037,7 +2047,15 @@ impl SessionManager {
|
|||||||
};
|
};
|
||||||
if let Some(text) = queued_text {
|
if let Some(text) = queued_text {
|
||||||
self.persist_prompt_queue(id).await;
|
self.persist_prompt_queue(id).await;
|
||||||
let _ = self.queue_acp_prompt(id, text).await;
|
let _ = self.queue_acp_prompt(id, text, None).await;
|
||||||
|
} else {
|
||||||
|
let _ = self
|
||||||
|
.queue_acp_prompt(
|
||||||
|
id,
|
||||||
|
"The daemon restarted. Continue where you left off.".into(),
|
||||||
|
Some(serde_json::json!({"maestro": {"type": "restart_continue"}})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = self
|
let _ = self
|
||||||
@@ -3017,7 +3035,7 @@ async fn run_blocked_work_pass(inner: &Arc<Inner>) {
|
|||||||
fn drain_prompt_queue(manager: SessionManager, id: Uuid, text: String) {
|
fn drain_prompt_queue(manager: SessionManager, id: Uuid, text: String) {
|
||||||
let fut: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> =
|
let fut: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> =
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let _ = manager.queue_acp_prompt(id, text).await;
|
let _ = manager.queue_acp_prompt(id, text, None).await;
|
||||||
});
|
});
|
||||||
tokio::spawn(fut);
|
tokio::spawn(fut);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,15 +84,27 @@ impl Fake {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn event_text(event: &maestro_daemon::sidecar::LoggedEvent) -> &str {
|
fn event_text(event: &maestro_daemon::sidecar::LoggedEvent) -> &str {
|
||||||
let maestro_daemon::sidecar::PersistedEvent::Notification(notification) = &event.event else {
|
match &event.event {
|
||||||
panic!("unexpected daemon event")
|
maestro_daemon::sidecar::PersistedEvent::Notification(notification) => {
|
||||||
};
|
match ¬ification.update {
|
||||||
match ¬ification.update {
|
SessionUpdate::AgentMessageChunk(chunk) => match &chunk.content {
|
||||||
SessionUpdate::AgentMessageChunk(chunk) => match &chunk.content {
|
ContentBlock::Text(text) => &text.text,
|
||||||
ContentBlock::Text(text) => &text.text,
|
other => panic!("unexpected content {other:?}"),
|
||||||
other => panic!("unexpected content {other:?}"),
|
},
|
||||||
},
|
other => panic!("unexpected update {other:?}"),
|
||||||
other => panic!("unexpected update {other:?}"),
|
}
|
||||||
|
}
|
||||||
|
maestro_daemon::sidecar::PersistedEvent::User { row } => row
|
||||||
|
.data
|
||||||
|
.pointer("/content/text")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.expect("user row text"),
|
||||||
|
maestro_daemon::sidecar::PersistedEvent::Turn { row } => row
|
||||||
|
.data
|
||||||
|
.pointer("/maestroTurnMeta/outcome")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.unwrap_or("[turn]"),
|
||||||
|
maestro_daemon::sidecar::PersistedEvent::Approval { .. } => "[approval]",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +271,7 @@ async fn structured_activity_tracks_turn_completion_and_failure() {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fx.manager
|
fx.manager
|
||||||
.queue_acp_prompt(session.id, "fail".into())
|
.queue_acp_prompt(session.id, "fail".into(), None)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
||||||
@@ -286,7 +298,7 @@ async fn structured_activity_tracks_turn_completion_and_failure() {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fx.manager
|
fx.manager
|
||||||
.queue_acp_prompt(session.id, "succeed".into())
|
.queue_acp_prompt(session.id, "succeed".into(), None)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
||||||
@@ -426,7 +438,7 @@ async fn queued_prompt_is_a_durable_user_transcript_row() {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fx.manager
|
fx.manager
|
||||||
.queue_acp_prompt(session.id, "show my prompt".into())
|
.queue_acp_prompt(session.id, "show my prompt".into(), None)
|
||||||
.await
|
.await
|
||||||
.expect("queue prompt"),
|
.expect("queue prompt"),
|
||||||
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
||||||
@@ -516,7 +528,7 @@ async fn cancelled_prompt_that_never_settles_fails_the_session() {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fx.manager
|
fx.manager
|
||||||
.queue_acp_prompt(session.id, "first".into())
|
.queue_acp_prompt(session.id, "first".into(), None)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
||||||
@@ -524,7 +536,7 @@ async fn cancelled_prompt_that_never_settles_fails_the_session() {
|
|||||||
assert!(fx.manager.cancel_acp_turn(session.id).await.unwrap());
|
assert!(fx.manager.cancel_acp_turn(session.id).await.unwrap());
|
||||||
let second = fx
|
let second = fx
|
||||||
.manager
|
.manager
|
||||||
.queue_acp_prompt(session.id, "second".into())
|
.queue_acp_prompt(session.id, "second".into(), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -555,7 +567,7 @@ async fn cancellation_settlement_allows_the_next_prompt_permission() {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fx.manager
|
fx.manager
|
||||||
.queue_acp_prompt(session.id, "first".into())
|
.queue_acp_prompt(session.id, "first".into(), None)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
||||||
@@ -563,7 +575,7 @@ async fn cancellation_settlement_allows_the_next_prompt_permission() {
|
|||||||
assert!(fx.manager.cancel_acp_turn(session.id).await.unwrap());
|
assert!(fx.manager.cancel_acp_turn(session.id).await.unwrap());
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fx.manager
|
fx.manager
|
||||||
.queue_acp_prompt(session.id, "second".into())
|
.queue_acp_prompt(session.id, "second".into(), None)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
maestro_daemon::session::QueueAcpPrompt::Dispatched
|
||||||
@@ -655,25 +667,18 @@ async fn recovery_loads_the_acp_session_without_duplicating_history() {
|
|||||||
revived.status(session.id).await,
|
revived.status(session.id).await,
|
||||||
Some(SessionStatus::Running)
|
Some(SessionStatus::Running)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
revived
|
|
||||||
.snapshot()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.find(|candidate| candidate.id == session.id)
|
|
||||||
.unwrap()
|
|
||||||
.activity,
|
|
||||||
ActivityState::Idle
|
|
||||||
);
|
|
||||||
let log = fake.log();
|
let log = fake.log();
|
||||||
assert!(
|
assert!(
|
||||||
log.contains(&format!("recv session/load sessionId={acp_id}")),
|
log.contains(&format!("recv session/load sessionId={acp_id}")),
|
||||||
"{log}"
|
"{log}"
|
||||||
);
|
);
|
||||||
|
// Recovery adds: a continue-prompt User row, the adapter's post-load
|
||||||
|
// notification, and a turn-completed footer — 5 events total before the
|
||||||
|
// next manual tick.
|
||||||
wait_for_async(Duration::from_secs(5), || async {
|
wait_for_async(Duration::from_secs(5), || async {
|
||||||
revived
|
revived
|
||||||
.read_acp_events(session.id, EventCursor::Since(0), 10)
|
.read_acp_events(session.id, EventCursor::Since(0), 10)
|
||||||
.is_ok_and(|events| events.len() == 3)
|
.is_ok_and(|events| events.len() == 5)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -682,7 +687,7 @@ async fn recovery_loads_the_acp_session_without_duplicating_history() {
|
|||||||
.await
|
.await
|
||||||
.expect("replay read")
|
.expect("replay read")
|
||||||
.expect("recovered event subscription");
|
.expect("recovered event subscription");
|
||||||
assert_eq!(replay.len(), 3);
|
assert_eq!(replay.len(), 5);
|
||||||
assert!(live.try_recv().is_err(), "replay duplicated onto live");
|
assert!(live.try_recv().is_err(), "replay duplicated onto live");
|
||||||
let new_pid = fake.pid();
|
let new_pid = fake.pid();
|
||||||
assert_ne!(new_pid, old_pid);
|
assert_ne!(new_pid, old_pid);
|
||||||
@@ -693,16 +698,23 @@ async fn recovery_loads_the_acp_session_without_duplicating_history() {
|
|||||||
.await
|
.await
|
||||||
.expect("live event after recovery")
|
.expect("live event after recovery")
|
||||||
.expect("broadcast open");
|
.expect("broadcast open");
|
||||||
assert_eq!(event.seq, 4);
|
assert_eq!(event.seq, 6);
|
||||||
assert_eq!(event_text(&event), "tick 1");
|
assert_eq!(event_text(&event), "tick 1");
|
||||||
|
|
||||||
let stored = revived
|
let stored = revived
|
||||||
.read_acp_events(session.id, EventCursor::Since(0), 10)
|
.read_acp_events(session.id, EventCursor::Since(0), 10)
|
||||||
.expect("recovered log");
|
.expect("recovered log");
|
||||||
assert_eq!(stored.len(), 4, "session/load replay must not be appended");
|
assert_eq!(stored.len(), 6, "session/load replay must not be appended");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stored.iter().map(event_text).collect::<Vec<_>>(),
|
stored.iter().map(event_text).collect::<Vec<_>>(),
|
||||||
["hello from fake", "tick 1", "hello after load", "tick 1"]
|
[
|
||||||
|
"hello from fake",
|
||||||
|
"tick 1",
|
||||||
|
"The daemon restarted. Continue where you left off.",
|
||||||
|
"hello after load",
|
||||||
|
"completed",
|
||||||
|
"tick 1",
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
revived.kill(session.id, false).await.expect("kill");
|
revived.kill(session.id, false).await.expect("kill");
|
||||||
|
|||||||
Reference in New Issue
Block a user