Merge branch 'feature/117-opencode-headless-recovery' into develop
This commit is contained in:
@ -4,10 +4,10 @@ use std::sync::{Arc, Mutex};
|
||||
use application::{AgentWakeService, WakeSessionProvider};
|
||||
use async_trait::async_trait;
|
||||
use domain::background_task::{
|
||||
BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskState,
|
||||
BackgroundTaskWakePolicy,
|
||||
BackgroundTask, BackgroundTaskKind, BackgroundTaskRendezvousLink, BackgroundTaskResult,
|
||||
BackgroundTaskState, BackgroundTaskWakePolicy,
|
||||
};
|
||||
use domain::ids::{AgentId, ProjectId, RuntimeAgentKey, SessionId, TaskId};
|
||||
use domain::ids::{AgentId, ProjectId, RendezvousId, RuntimeAgentKey, SessionId, TaskId};
|
||||
use domain::inbox::{
|
||||
AgentInbox, AgentInboxSnapshot, InboxError, InboxItem, InboxItemKind, InboxReceipt,
|
||||
InboxReceiptStatus, InboxSource,
|
||||
@ -20,6 +20,7 @@ use domain::ports::{
|
||||
};
|
||||
use domain::project::{Project, ProjectPath};
|
||||
use domain::remote::RemoteRef;
|
||||
use domain::ConversationId;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn id(n: u128) -> Uuid {
|
||||
@ -95,6 +96,37 @@ fn completed_task(
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn completed_rendezvous_task(
|
||||
project: &Project,
|
||||
owner: AgentId,
|
||||
task_id: TaskId,
|
||||
link: BackgroundTaskRendezvousLink,
|
||||
) -> BackgroundTask {
|
||||
BackgroundTask::new(
|
||||
task_id,
|
||||
project.id,
|
||||
owner,
|
||||
BackgroundTaskKind::Command {
|
||||
label: "build".to_owned(),
|
||||
},
|
||||
BackgroundTaskWakePolicy::WakeOwner,
|
||||
1,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.with_rendezvous(link)
|
||||
.transition(BackgroundTaskState::Running, 10)
|
||||
.unwrap()
|
||||
.complete(BackgroundTaskResult::Success {
|
||||
finished_at_ms: 20,
|
||||
exit_code: Some(0),
|
||||
summary: "done".to_owned(),
|
||||
stdout_tail: None,
|
||||
stderr_tail: None,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeInbox {
|
||||
queues: Mutex<HashMap<RuntimeAgentKey, VecDeque<InboxItem>>>,
|
||||
@ -147,6 +179,7 @@ impl AgentInbox for FakeInbox {
|
||||
struct SharedTurnState {
|
||||
busy: Mutex<HashMap<RuntimeAgentKey, AgentBusyState>>,
|
||||
tickets: Mutex<HashMap<RuntimeAgentKey, VecDeque<Ticket>>>,
|
||||
replies: Mutex<Vec<(TicketId, String)>>,
|
||||
}
|
||||
|
||||
impl SharedTurnState {
|
||||
@ -168,6 +201,10 @@ impl SharedTurnState {
|
||||
.map(VecDeque::len)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn replies(&self) -> Vec<(TicketId, String)> {
|
||||
self.replies.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl InputMediator for SharedTurnState {
|
||||
@ -220,6 +257,25 @@ impl AgentMailbox for SharedTurnState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_ticket(
|
||||
&self,
|
||||
agent: RuntimeAgentKey,
|
||||
ticket_id: TicketId,
|
||||
result: String,
|
||||
) -> Result<(), MailboxError> {
|
||||
let mut tickets = self.tickets.lock().unwrap();
|
||||
let queue = tickets
|
||||
.get_mut(&agent)
|
||||
.ok_or(MailboxError::NoPendingRequest(agent.agent_id))?;
|
||||
let pos = queue
|
||||
.iter()
|
||||
.position(|ticket| ticket.id == ticket_id)
|
||||
.ok_or(MailboxError::NoPendingRequest(agent.agent_id))?;
|
||||
queue.remove(pos);
|
||||
self.replies.lock().unwrap().push((ticket_id, result));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_head(&self, agent: RuntimeAgentKey, ticket_id: TicketId) {
|
||||
let mut tickets = self.tickets.lock().unwrap();
|
||||
if let Some(queue) = tickets.get_mut(&agent) {
|
||||
@ -230,6 +286,65 @@ impl AgentMailbox for SharedTurnState {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rendezvous_background_completion_resolves_original_ticket_with_business_final() {
|
||||
let project = project();
|
||||
let owner = agent(1);
|
||||
let requester = agent(2);
|
||||
let task_id = task_id(10);
|
||||
let rendezvous_ticket = ticket(99);
|
||||
let conversation_id = ConversationId::from_uuid(id(300));
|
||||
let rendezvous_id = RendezvousId::from_uuid(id(400));
|
||||
let link = BackgroundTaskRendezvousLink {
|
||||
rendezvous_id,
|
||||
ticket_id: rendezvous_ticket,
|
||||
requester_agent_id: Some(requester),
|
||||
target_agent_id: owner,
|
||||
conversation_id,
|
||||
};
|
||||
let inbox = Arc::new(FakeInbox::default());
|
||||
let turns = Arc::new(SharedTurnState::default());
|
||||
let tasks = Arc::new(FakeTaskStore::default());
|
||||
let session = Arc::new(FakeSession::with_events(vec![ReplyEvent::Final {
|
||||
content: "real business answer".to_owned(),
|
||||
}]));
|
||||
let sessions = Arc::new(FakeSessionProvider::with_session(session));
|
||||
|
||||
turns.enqueue_silent(
|
||||
runtime_key(owner),
|
||||
Ticket::from_agent(
|
||||
rendezvous_ticket,
|
||||
requester,
|
||||
conversation_id,
|
||||
"Requester",
|
||||
"original ask",
|
||||
),
|
||||
);
|
||||
turns.mark_idle(runtime_key(owner));
|
||||
tasks.insert(completed_rendezvous_task(&project, owner, task_id, link));
|
||||
inbox
|
||||
.enqueue_message(
|
||||
runtime_key(owner),
|
||||
completion_item(owner, task_id, ticket(20)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
service(inbox, turns.clone(), tasks, sessions)
|
||||
.wake_agent(
|
||||
&project,
|
||||
owner,
|
||||
WakeReason::BackgroundCompletion { task_id },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
turns.replies(),
|
||||
vec![(rendezvous_ticket, "real business answer".to_owned())]
|
||||
);
|
||||
assert_eq!(turns.ticket_depth(owner), 0);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeTaskStore {
|
||||
tasks: Mutex<HashMap<TaskId, BackgroundTask>>,
|
||||
|
||||
Reference in New Issue
Block a user