1use std::collections::VecDeque;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11
12use crate::extensions::feedback;
13use crate::extensions::runtime::{ExtensionHandler, ExtensionHealth};
14use crate::extensions::session_driver::{
15 Grant, Outcome, PollRequest, PrepareError, Proposal, Reply, Selection,
16};
17use crate::{CancellationToken, Runtime, SharedMessage};
18
19type Workers = Arc<std::sync::Mutex<crate::runtime::subagent::SubagentRegistry>>;
20type Manager = Arc<tokio::sync::RwLock<crate::extensions::manager::ExtensionManager>>;
21
22pub(crate) fn cancel_workers(workers: &Workers, epoch: u64) {
23 workers
24 .lock()
25 .unwrap_or_else(|p| p.into_inner())
26 .cancel_spawn_epoch(epoch);
27}
28
29pub(crate) struct Task<T>(pub tokio::task::JoinHandle<T>);
32impl<T> Drop for Task<T> {
33 fn drop(&mut self) {
34 self.0.abort();
35 }
36}
37
38pub(crate) struct Scheduled {
41 pub proposal: Proposal,
42 pub due: Instant,
43}
44
45pub(crate) struct DriverPending {
48 pub generation: u64,
49 pub session_id: String,
50 pub task: Task<TaskResult>,
51}
52
53pub(crate) enum TaskResult {
54 Command {
55 owner: String,
56 #[allow(dead_code)] command: String,
58 handler: Arc<dyn ExtensionHandler>,
59 handler_generation: Option<u64>,
60 result: Result<serde_json::Value, String>,
61 report: crate::extensions::invoke_output::InvokeOutputReport,
62 },
63 Poll(Result<Reply, String>),
64 Prepared {
65 proposal: Proposal,
66 result: Box<Result<Runtime, PrepareError>>,
67 },
68 Started(Result<super::actor::ActiveStream, String>),
69}
70
71pub(crate) struct DriverState {
75 pub grant: Grant,
76 pub handler: Arc<dyn ExtensionHandler>,
77 pub handler_generation: u64,
78 pub cancel: CancellationToken,
79 pub workers: Workers,
80 pub worker_epoch: u64,
81 #[allow(dead_code)] pub deadline_task: Option<Task<()>>,
83 pub proposal: Option<Scheduled>,
84 pub selection: Selection,
85 pub awaiting_terminal: bool,
86 pub outcome: Option<(Outcome, String)>,
87 pub feedback: feedback::Tracker,
88 pub completed_feedback: &'static str,
89 pub steering: VecDeque<String>,
93 pub auto_wakes_blocked: bool,
94 #[allow(dead_code)] pub cost_at_arm: f64,
96}
97
98impl Drop for DriverState {
99 fn drop(&mut self) {
100 self.cancel.cancel();
101 cancel_workers(&self.workers, self.worker_epoch);
102 }
103}
104
105pub(crate) const STEERING_MAX_MESSAGES: usize = 16;
108pub(crate) const STEERING_MAX_BYTES: usize = 256 * 1024;
109
110pub(crate) fn live_generation(handler: &Arc<dyn ExtensionHandler>) -> Result<u64, String> {
113 handler
114 .lifecycle_snapshot()
115 .filter(|s| {
116 matches!(
117 s.health,
118 ExtensionHealth::Running | ExtensionHealth::Degraded
119 )
120 })
121 .map(|s| s.generation)
122 .ok_or_else(|| "owning extension is not live or its lifecycle is unavailable".into())
123}
124
125pub(crate) fn same_lifecycle(
126 handler: &Arc<dyn ExtensionHandler>,
127 generation: u64,
128) -> Result<(), String> {
129 if live_generation(handler)? == generation {
130 Ok(())
131 } else {
132 Err("owning extension process restarted or transport was lost".into())
133 }
134}
135
136pub(crate) fn same_handler(
137 manager: &Manager,
138 owner: &str,
139 handler: &Arc<dyn ExtensionHandler>,
140 generation: u64,
141) -> Result<(), String> {
142 let current = manager
143 .try_read()
144 .map_err(|_| "extension lifecycle busy".to_string())?
145 .session_driver_handler(owner)?;
146 if Arc::ptr_eq(¤t, handler) {
147 same_lifecycle(handler, generation)
148 } else {
149 Err("owning extension was replaced".into())
150 }
151}
152
153pub(crate) fn schedule(active: &mut DriverState, proposal: Proposal) -> Result<(), String> {
154 if proposal.delay < Duration::from_secs(1) || proposal.delay > Duration::from_secs(300) {
155 return Err("invalid proposal delay".into());
156 }
157 let due = Instant::now() + proposal.delay;
158 if active
159 .grant
160 .deadline()
161 .is_some_and(|deadline| due >= deadline)
162 {
163 return Err("proposal would run past the grant deadline".into());
164 }
165 active.selection = proposal.selection.clone();
166 active.proposal = Some(Scheduled { proposal, due });
167 Ok(())
168}
169
170pub(crate) fn history_with_steering(
173 api_messages: &[SharedMessage],
174 steering: &VecDeque<String>,
175) -> Vec<SharedMessage> {
176 let mut history = api_messages.to_vec();
177 for text in steering {
178 history.push(Arc::new(serde_json::json!({"role": "user", "content": text})));
179 }
180 history
181}
182
183pub(crate) fn commit_submission(
185 api_messages: &mut Vec<SharedMessage>,
186 steering: &mut VecDeque<String>,
187 prompt: &str,
188 abort_context: &mut Option<String>,
189) {
190 for text in steering.drain(..) {
191 api_messages.push(Arc::new(serde_json::json!({"role": "user", "content": text})));
192 }
193 let text = if let Some(context) = abort_context.take() {
194 format!("{context}\n\n{prompt}")
195 } else {
196 prompt.to_string()
197 };
198 api_messages.push(Arc::new(serde_json::json!({"role": "user", "content": text})));
199}
200
201pub(crate) fn poll_request(
203 active: &DriverState,
204 outcome: Outcome,
205 error_kind: String,
206 session_cost_so_far: f64,
207) -> PollRequest {
208 PollRequest {
209 run_id: active.grant.run_id.clone(),
210 decision_id: uuid::Uuid::new_v4().to_string(),
211 outcome,
212 error_kind,
213 model: active.selection.model.clone(),
214 effort: active.selection.effort.clone(),
215 feedback: active.grant.feedback_enabled().then(|| {
216 if outcome == Outcome::Success {
217 active.completed_feedback
218 } else {
219 "unknown"
220 }
221 .into()
222 }),
223 session_id: Some(active.grant.session_id.clone()),
224 session_cost_so_far: Some(session_cost_so_far),
226 }
227}
228
229pub(crate) fn deadline_ms(grant: &Grant) -> Option<u64> {
231 grant.deadline().map(|d| {
232 d.saturating_duration_since(Instant::now())
233 .as_millis()
234 .min(u64::MAX as u128) as u64
235 })
236}
237
238#[derive(Debug)]
242pub(crate) enum Terminal {
243 Success,
244 Failure(Outcome, String),
245 Canceled,
247}
248
249pub(crate) fn capture_terminal(
252 event: Option<&crate::StreamEvent>,
253 canceled: bool,
254) -> Option<Terminal> {
255 use crate::{SessionEvent, StreamEvent};
256 use crate::extensions::session_driver::{classify_turn_error, Outcome as O};
257 if canceled {
258 return Some(Terminal::Canceled);
259 }
260 match event {
261 Some(StreamEvent::Session(SessionEvent::Done)) => Some(Terminal::Success),
262 Some(StreamEvent::Session(SessionEvent::Error(error))) => {
263 if matches!(error.outcome, agent_core::TurnOutcome::Canceled) {
264 return Some(Terminal::Canceled);
265 }
266 let (outcome, kind) = classify_turn_error(error);
267 Some(Terminal::Failure(outcome, kind))
268 }
269 None => Some(Terminal::Failure(O::Blocked, "unknown".into())),
270 _ => None,
271 }
272}
273
274pub(crate) fn observe_terminal(
278 driver: &mut DriverState,
279 runtime: &crate::Runtime,
280 terminal: Terminal,
281) -> Option<String> {
282 if !driver.awaiting_terminal {
283 return None;
284 }
285 driver.awaiting_terminal = false;
286 match terminal {
287 Terminal::Canceled => {
288 return Some("canceled".into());
289 }
290 Terminal::Success => {
291 driver.completed_feedback = if driver.grant.feedback_enabled() {
292 driver.feedback.finish()
293 } else {
294 "unknown"
295 };
296 driver.outcome = Some((Outcome::Success, "none".into()));
297 }
298 Terminal::Failure(Outcome::TimeCheckpoint, kind)
299 if driver.grant.time_checkpoints_enabled()
300 && !runtime.turn_budget().max_elapsed.is_zero() =>
301 {
302 driver.completed_feedback = "unknown";
303 driver.feedback = feedback::Tracker::default();
304 driver.outcome = Some((Outcome::TimeCheckpoint, kind));
305 }
306 Terminal::Failure(Outcome::TimeCheckpoint, _) => {
307 return Some("blocked or unexpected end of stream; explicit user action required".into());
308 }
309 Terminal::Failure(outcome, kind) => {
310 if matches!(outcome, Outcome::Blocked) {
311 return Some("blocked or unexpected end of stream; explicit user action required".into());
312 }
313 driver.outcome = Some((outcome, kind));
314 }
315 }
316 None
317}
318
319#[cfg(test)]
322mod tests {
323 use super::*;
324 use std::collections::VecDeque;
325
326 fn test_selection() -> Selection {
327 Selection {
328 model: "test/model".into(),
329 effort: "high".into(),
330 }
331 }
332
333 fn test_reply() -> Reply {
334 Reply::Start {
335 run_id: "test-run".into(),
336 models: vec![test_selection()],
337 prompt: "do the work".into(),
338 delay_ms: 2000,
339 max_duration_ms: Some(120_000),
340 feedback_version: Some(1),
341 time_checkpoint_version: Some(1),
342 context_mode: None,
343 notice: "test notice".into(),
344 max_cost_usd: None,
345 }
346 }
347
348 fn test_grant_and_state() -> (Grant, DriverState) {
349 let reply = test_reply();
350 let (grant, proposal) =
351 Grant::from_start("test-plugin", "test-session", reply).unwrap();
352 let state = DriverState {
353 selection: proposal.selection.clone(),
354 grant,
355 handler: std::sync::Arc::new(crate::extensions::runtime::tests::DummyHandler),
356 handler_generation: 0,
357 cancel: CancellationToken::new(),
358 workers: Arc::new(std::sync::Mutex::new(
359 crate::runtime::subagent::SubagentRegistry::new(),
360 )),
361 worker_epoch: 0,
362 deadline_task: None,
363 proposal: Some(Scheduled { proposal, due: Instant::now() + Duration::from_secs(2) }),
364 awaiting_terminal: false,
365 outcome: None,
366 feedback: feedback::Tracker::default(),
367 completed_feedback: "unknown",
368 steering: VecDeque::new(),
369 auto_wakes_blocked: false,
370 cost_at_arm: 0.0,
371 };
372 let grant_copy = Grant::from_start(
373 "test-plugin",
374 "test-session",
375 test_reply(),
376 )
377 .unwrap()
378 .0;
379 (grant_copy, state)
380 }
381
382 #[test]
385 fn schedule_rejects_too_short_delay() {
386 let (_, mut state) = test_grant_and_state();
387 let proposal = crate::extensions::session_driver::Proposal {
388 selection: test_selection(),
389 prompt: "x".into(),
390 delay: Duration::from_millis(500),
391 notice: String::new(),
392 };
393 assert!(schedule(&mut state, proposal).is_err());
394 }
395
396 #[test]
397 fn schedule_rejects_past_deadline() {
398 let (_, mut state) = test_grant_and_state();
399 let proposal = crate::extensions::session_driver::Proposal {
401 selection: test_selection(),
402 prompt: "x".into(),
403 delay: Duration::from_secs(300),
404 notice: String::new(),
405 };
406 assert!(schedule(&mut state, proposal).is_err());
407 }
408
409 #[test]
410 fn schedule_accepts_valid_proposal() {
411 let (_, mut state) = test_grant_and_state();
412 let proposal = crate::extensions::session_driver::Proposal {
413 selection: Selection {
414 model: "other/model".into(),
415 effort: "low".into(),
416 },
417 prompt: "continue".into(),
418 delay: Duration::from_secs(2),
419 notice: "notice".into(),
420 };
421 assert!(schedule(&mut state, proposal).is_ok());
422 assert_eq!(state.selection.model, "other/model");
423 assert!(state.proposal.is_some());
424 }
425
426 #[test]
429 fn poll_request_includes_feedback_when_enabled() {
430 let (_, state) = test_grant_and_state();
431 let req = poll_request(
432 &state,
433 crate::extensions::session_driver::Outcome::Success,
434 "none".into(),
435 1.25,
436 );
437 assert!(req.feedback.is_some());
438 assert_eq!(req.run_id, "test-run");
439 assert!(req.session_id.is_some());
440 assert_eq!(req.session_cost_so_far, Some(1.25));
442 }
443
444 #[test]
447 fn same_lifecycle_matches_same_generation() {
448 let handler: Arc<dyn crate::extensions::runtime::ExtensionHandler> =
449 Arc::new(crate::extensions::runtime::tests::DummyHandler);
450 let gen = live_generation(&handler).unwrap();
451 assert!(same_lifecycle(&handler, gen).is_ok());
452 }
453
454 #[test]
455 fn same_lifecycle_rejects_different_generation() {
456 let handler: Arc<dyn crate::extensions::runtime::ExtensionHandler> =
457 Arc::new(crate::extensions::runtime::tests::DummyHandler);
458 assert!(same_lifecycle(&handler, 999).is_err());
459 }
460
461 #[test]
464 fn history_with_steering_appends_user_messages() {
465 let base: Vec<SharedMessage> =
466 vec![Arc::new(serde_json::json!({"role":"assistant","content":"hi"}))];
467 let mut steering = VecDeque::new();
468 steering.push_back("one".into());
469 steering.push_back("two".into());
470 let result = history_with_steering(&base, &steering);
471 assert_eq!(result.len(), 3);
472 assert_eq!(result[1]["role"], "user");
473 assert_eq!(result[1]["content"], "one");
474 assert_eq!(result[2]["content"], "two");
475 }
476
477 #[test]
480 fn commit_submission_drains_steering_and_appends_prompt() {
481 let mut messages: Vec<SharedMessage> = Vec::new();
482 let mut steering = VecDeque::new();
483 steering.push_back("steer1".into());
484 steering.push_back("steer2".into());
485 let mut abort = None;
486 commit_submission(&mut messages, &mut steering, "go", &mut abort);
487 assert!(steering.is_empty());
488 assert_eq!(messages.len(), 3);
489 assert_eq!(messages[0]["content"], "steer1");
490 assert_eq!(messages[1]["content"], "steer2");
491 assert_eq!(messages[2]["content"], "go");
492 }
493
494 #[test]
495 fn commit_submission_prepends_abort_context() {
496 let mut messages: Vec<SharedMessage> = Vec::new();
497 let mut steering = VecDeque::new();
498 let mut abort = Some("aborted context".into());
499 commit_submission(&mut messages, &mut steering, "go", &mut abort);
500 assert!(abort.is_none());
501 assert_eq!(messages.len(), 1);
502 assert!(messages[0]["content"].as_str().unwrap().contains("aborted context"));
503 assert!(messages[0]["content"].as_str().unwrap().contains("go"));
504 }
505
506 #[test]
509 fn capture_terminal_done_is_success() {
510 let event = crate::StreamEvent::Session(crate::SessionEvent::Done);
511 match capture_terminal(Some(&event), false) {
512 Some(Terminal::Success) => {}
513 other => panic!("expected Success, got: {other:?}"),
514 }
515 }
516
517 #[test]
518 fn capture_terminal_canceled_flag() {
519 let event = crate::StreamEvent::Session(crate::SessionEvent::Done);
520 match capture_terminal(Some(&event), true) {
521 Some(Terminal::Canceled) => {}
522 other => panic!("expected Canceled, got: {other:?}"),
523 }
524 }
525
526 #[test]
527 fn capture_terminal_eof_is_blocked() {
528 match capture_terminal(None, false) {
529 Some(Terminal::Failure(Outcome::Blocked, _)) => {}
530 other => panic!("expected Blocked, got: {other:?}"),
531 }
532 }
533
534 #[test]
535 fn capture_terminal_error_with_canceled_outcome() {
536 let error = agent_core::TurnError {
537 message: "canceled".into(),
538 outcome: agent_core::TurnOutcome::Canceled,
539 };
540 let event = crate::StreamEvent::Session(crate::SessionEvent::Error(error));
541 match capture_terminal(Some(&event), false) {
542 Some(Terminal::Canceled) => {}
543 other => panic!("expected Canceled from TurnOutcome::Canceled, got: {other:?}"),
544 }
545 }
546
547 #[test]
548 fn capture_terminal_llm_event_is_none() {
549 let event = crate::StreamEvent::Llm(crate::LlmEvent::Text("hi".into()));
550 assert!(capture_terminal(Some(&event), false).is_none());
551 }
552
553 #[test]
554 fn observe_terminal_success_sets_outcome() {
555 let (_, mut state) = test_grant_and_state();
556 state.awaiting_terminal = true;
557 let runtime = tokio::runtime::Runtime::new().unwrap();
558 runtime.block_on(async {
559 let rt = crate::Runtime::new().await.unwrap();
560 let result = observe_terminal(&mut state, &rt, Terminal::Success);
561 assert!(result.is_none(), "success should not revoke");
562 assert!(state.outcome.is_some());
563 let (outcome, _) = state.outcome.clone().unwrap();
564 assert_eq!(outcome, Outcome::Success);
565 });
566 }
567
568 #[test]
569 fn observe_terminal_canceled_returns_revoke_reason() {
570 let (_, mut state) = test_grant_and_state();
571 state.awaiting_terminal = true;
572 let runtime = tokio::runtime::Runtime::new().unwrap();
573 runtime.block_on(async {
574 let rt = crate::Runtime::new().await.unwrap();
575 let result = observe_terminal(&mut state, &rt, Terminal::Canceled);
576 assert!(result.is_some());
577 assert!(result.unwrap().contains("canceled"));
578 assert!(state.outcome.is_none());
579 });
580 }
581
582 #[test]
583 fn observe_terminal_blocked_returns_revoke_reason() {
584 let (_, mut state) = test_grant_and_state();
585 state.awaiting_terminal = true;
586 let runtime = tokio::runtime::Runtime::new().unwrap();
587 runtime.block_on(async {
588 let rt = crate::Runtime::new().await.unwrap();
589 let result = observe_terminal(
590 &mut state,
591 &rt,
592 Terminal::Failure(Outcome::Blocked, "unknown".into()),
593 );
594 assert!(result.is_some());
595 assert!(result.unwrap().contains("blocked"));
596 });
597 }
598
599 #[test]
600 fn observe_terminal_provider_error_sets_outcome() {
601 let (_, mut state) = test_grant_and_state();
602 state.awaiting_terminal = true;
603 let runtime = tokio::runtime::Runtime::new().unwrap();
604 runtime.block_on(async {
605 let rt = crate::Runtime::new().await.unwrap();
606 let result = observe_terminal(
607 &mut state,
608 &rt,
609 Terminal::Failure(Outcome::ProviderError, "transient".into()),
610 );
611 assert!(result.is_none(), "provider error should not revoke");
612 let (outcome, kind) = state.outcome.clone().unwrap();
613 assert_eq!(outcome, Outcome::ProviderError);
614 assert_eq!(kind, "transient");
615 });
616 }
617
618 #[test]
619 fn observe_terminal_not_awaiting_is_noop() {
620 let (_, mut state) = test_grant_and_state();
621 state.awaiting_terminal = false;
622 let runtime = tokio::runtime::Runtime::new().unwrap();
623 runtime.block_on(async {
624 let rt = crate::Runtime::new().await.unwrap();
625 let result = observe_terminal(&mut state, &rt, Terminal::Success);
626 assert!(result.is_none());
627 assert!(state.outcome.is_none());
628 });
629 }
630
631 #[test]
632 fn observe_terminal_only_fires_once() {
633 let (_, mut state) = test_grant_and_state();
634 state.awaiting_terminal = true;
635 let runtime = tokio::runtime::Runtime::new().unwrap();
636 runtime.block_on(async {
637 let rt = crate::Runtime::new().await.unwrap();
638 let r1 = observe_terminal(&mut state, &rt, Terminal::Success);
639 assert!(r1.is_none());
640 assert!(state.outcome.is_some());
641 state.outcome = None;
643 let r2 = observe_terminal(&mut state, &rt, Terminal::Success);
644 assert!(r2.is_none());
645 assert!(state.outcome.is_none());
646 });
647 }
648}