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(
188 api_messages: &mut Vec<SharedMessage>,
189 steering: &mut VecDeque<String>,
190 prompt: &str,
191) {
192 for text in steering.drain(..) {
193 api_messages.push(Arc::new(serde_json::json!({"role": "user", "content": text})));
194 }
195 api_messages.push(Arc::new(serde_json::json!({"role": "user", "content": prompt})));
196}
197
198pub(crate) fn poll_request(
200 active: &DriverState,
201 outcome: Outcome,
202 error_kind: String,
203 session_cost_so_far: f64,
204) -> PollRequest {
205 PollRequest {
206 run_id: active.grant.run_id.clone(),
207 decision_id: uuid::Uuid::new_v4().to_string(),
208 outcome,
209 error_kind,
210 model: active.selection.model.clone(),
211 effort: active.selection.effort.clone(),
212 feedback: active.grant.feedback_enabled().then(|| {
213 if outcome == Outcome::Success {
214 active.completed_feedback
215 } else {
216 "unknown"
217 }
218 .into()
219 }),
220 session_id: Some(active.grant.session_id.clone()),
221 session_cost_so_far: Some(session_cost_so_far),
223 }
224}
225
226pub(crate) fn deadline_ms(grant: &Grant) -> Option<u64> {
228 grant.deadline().map(|d| {
229 d.saturating_duration_since(Instant::now())
230 .as_millis()
231 .min(u64::MAX as u128) as u64
232 })
233}
234
235#[derive(Debug)]
239pub(crate) enum Terminal {
240 Success,
241 Failure(Outcome, String),
242 Canceled,
244}
245
246pub(crate) fn capture_terminal(
249 event: Option<&crate::StreamEvent>,
250 canceled: bool,
251) -> Option<Terminal> {
252 use crate::{SessionEvent, StreamEvent};
253 use crate::extensions::session_driver::{classify_turn_error, Outcome as O};
254 if canceled {
255 return Some(Terminal::Canceled);
256 }
257 match event {
258 Some(StreamEvent::Session(SessionEvent::Done)) => Some(Terminal::Success),
259 Some(StreamEvent::Session(SessionEvent::Error(error))) => {
260 if matches!(error.outcome, agent_core::TurnOutcome::Canceled) {
261 return Some(Terminal::Canceled);
262 }
263 let (outcome, kind) = classify_turn_error(error);
264 Some(Terminal::Failure(outcome, kind))
265 }
266 None => Some(Terminal::Failure(O::Blocked, "unknown".into())),
267 _ => None,
268 }
269}
270
271pub(crate) fn observe_terminal(
275 driver: &mut DriverState,
276 runtime: &crate::Runtime,
277 terminal: Terminal,
278) -> Option<String> {
279 if !driver.awaiting_terminal {
280 return None;
281 }
282 driver.awaiting_terminal = false;
283 match terminal {
284 Terminal::Canceled => {
285 return Some("canceled".into());
286 }
287 Terminal::Success => {
288 driver.completed_feedback = if driver.grant.feedback_enabled() {
289 driver.feedback.finish()
290 } else {
291 "unknown"
292 };
293 driver.outcome = Some((Outcome::Success, "none".into()));
294 }
295 Terminal::Failure(Outcome::TimeCheckpoint, kind)
296 if driver.grant.time_checkpoints_enabled()
297 && runtime.turn_budget().has_time_limit() =>
298 {
299 driver.completed_feedback = "unknown";
300 driver.feedback = feedback::Tracker::default();
301 driver.outcome = Some((Outcome::TimeCheckpoint, kind));
302 }
303 Terminal::Failure(Outcome::TimeCheckpoint, _) => {
304 return Some("blocked or unexpected end of stream; explicit user action required".into());
305 }
306 Terminal::Failure(outcome, kind) => {
307 if matches!(outcome, Outcome::Blocked) {
308 return Some("blocked or unexpected end of stream; explicit user action required".into());
309 }
310 driver.outcome = Some((outcome, kind));
311 }
312 }
313 None
314}
315
316#[cfg(test)]
319mod tests {
320 use super::*;
321 use std::collections::VecDeque;
322
323 fn test_selection() -> Selection {
324 Selection {
325 model: "test/model".into(),
326 effort: "high".into(),
327 }
328 }
329
330 fn test_reply() -> Reply {
331 Reply::Start {
332 run_id: "test-run".into(),
333 models: vec![test_selection()],
334 prompt: "do the work".into(),
335 delay_ms: 2000,
336 max_duration_ms: Some(120_000),
337 feedback_version: Some(1),
338 time_checkpoint_version: Some(1),
339 context_mode: None,
340 notice: "test notice".into(),
341 max_cost_usd: None,
342 }
343 }
344
345 fn test_grant_and_state() -> (Grant, DriverState) {
346 let reply = test_reply();
347 let (grant, proposal) =
348 Grant::from_start("test-plugin", "test-session", reply).unwrap();
349 let state = DriverState {
350 selection: proposal.selection.clone(),
351 grant,
352 handler: std::sync::Arc::new(crate::extensions::runtime::tests::DummyHandler),
353 handler_generation: 0,
354 cancel: CancellationToken::new(),
355 workers: Arc::new(std::sync::Mutex::new(
356 crate::runtime::subagent::SubagentRegistry::new(),
357 )),
358 worker_epoch: 0,
359 deadline_task: None,
360 proposal: Some(Scheduled { proposal, due: Instant::now() + Duration::from_secs(2) }),
361 awaiting_terminal: false,
362 outcome: None,
363 feedback: feedback::Tracker::default(),
364 completed_feedback: "unknown",
365 steering: VecDeque::new(),
366 auto_wakes_blocked: false,
367 cost_at_arm: 0.0,
368 };
369 let grant_copy = Grant::from_start(
370 "test-plugin",
371 "test-session",
372 test_reply(),
373 )
374 .unwrap()
375 .0;
376 (grant_copy, state)
377 }
378
379 #[test]
382 fn schedule_rejects_too_short_delay() {
383 let (_, mut state) = test_grant_and_state();
384 let proposal = crate::extensions::session_driver::Proposal {
385 selection: test_selection(),
386 prompt: "x".into(),
387 delay: Duration::from_millis(500),
388 notice: String::new(),
389 };
390 assert!(schedule(&mut state, proposal).is_err());
391 }
392
393 #[test]
394 fn schedule_rejects_past_deadline() {
395 let (_, mut state) = test_grant_and_state();
396 let proposal = crate::extensions::session_driver::Proposal {
398 selection: test_selection(),
399 prompt: "x".into(),
400 delay: Duration::from_secs(300),
401 notice: String::new(),
402 };
403 assert!(schedule(&mut state, proposal).is_err());
404 }
405
406 #[test]
407 fn schedule_accepts_valid_proposal() {
408 let (_, mut state) = test_grant_and_state();
409 let proposal = crate::extensions::session_driver::Proposal {
410 selection: Selection {
411 model: "other/model".into(),
412 effort: "low".into(),
413 },
414 prompt: "continue".into(),
415 delay: Duration::from_secs(2),
416 notice: "notice".into(),
417 };
418 assert!(schedule(&mut state, proposal).is_ok());
419 assert_eq!(state.selection.model, "other/model");
420 assert!(state.proposal.is_some());
421 }
422
423 #[test]
426 fn poll_request_includes_feedback_when_enabled() {
427 let (_, state) = test_grant_and_state();
428 let req = poll_request(
429 &state,
430 crate::extensions::session_driver::Outcome::Success,
431 "none".into(),
432 1.25,
433 );
434 assert!(req.feedback.is_some());
435 assert_eq!(req.run_id, "test-run");
436 assert!(req.session_id.is_some());
437 assert_eq!(req.session_cost_so_far, Some(1.25));
439 }
440
441 #[test]
444 fn same_lifecycle_matches_same_generation() {
445 let handler: Arc<dyn crate::extensions::runtime::ExtensionHandler> =
446 Arc::new(crate::extensions::runtime::tests::DummyHandler);
447 let gen = live_generation(&handler).unwrap();
448 assert!(same_lifecycle(&handler, gen).is_ok());
449 }
450
451 #[test]
452 fn same_lifecycle_rejects_different_generation() {
453 let handler: Arc<dyn crate::extensions::runtime::ExtensionHandler> =
454 Arc::new(crate::extensions::runtime::tests::DummyHandler);
455 assert!(same_lifecycle(&handler, 999).is_err());
456 }
457
458 #[test]
461 fn history_with_steering_appends_user_messages() {
462 let base: Vec<SharedMessage> =
463 vec![Arc::new(serde_json::json!({"role":"assistant","content":"hi"}))];
464 let mut steering = VecDeque::new();
465 steering.push_back("one".into());
466 steering.push_back("two".into());
467 let result = history_with_steering(&base, &steering);
468 assert_eq!(result.len(), 3);
469 assert_eq!(result[1]["role"], "user");
470 assert_eq!(result[1]["content"], "one");
471 assert_eq!(result[2]["content"], "two");
472 }
473
474 #[test]
477 fn commit_submission_drains_steering_and_appends_prompt() {
478 let mut messages: Vec<SharedMessage> = Vec::new();
479 let mut steering = VecDeque::new();
480 steering.push_back("steer1".into());
481 steering.push_back("steer2".into());
482 commit_submission(&mut messages, &mut steering, "go");
483 assert!(steering.is_empty());
484 assert_eq!(messages.len(), 3);
485 assert_eq!(messages[0]["content"], "steer1");
486 assert_eq!(messages[1]["content"], "steer2");
487 assert_eq!(messages[2]["content"], "go");
488 }
489
490 #[test]
493 fn capture_terminal_done_is_success() {
494 let event = crate::StreamEvent::Session(crate::SessionEvent::Done);
495 match capture_terminal(Some(&event), false) {
496 Some(Terminal::Success) => {}
497 other => panic!("expected Success, got: {other:?}"),
498 }
499 }
500
501 #[test]
502 fn capture_terminal_canceled_flag() {
503 let event = crate::StreamEvent::Session(crate::SessionEvent::Done);
504 match capture_terminal(Some(&event), true) {
505 Some(Terminal::Canceled) => {}
506 other => panic!("expected Canceled, got: {other:?}"),
507 }
508 }
509
510 #[test]
511 fn capture_terminal_eof_is_blocked() {
512 match capture_terminal(None, false) {
513 Some(Terminal::Failure(Outcome::Blocked, _)) => {}
514 other => panic!("expected Blocked, got: {other:?}"),
515 }
516 }
517
518 #[test]
519 fn capture_terminal_error_with_canceled_outcome() {
520 let error = agent_core::TurnError {
521 message: "canceled".into(),
522 outcome: agent_core::TurnOutcome::Canceled,
523 };
524 let event = crate::StreamEvent::Session(crate::SessionEvent::Error(error));
525 match capture_terminal(Some(&event), false) {
526 Some(Terminal::Canceled) => {}
527 other => panic!("expected Canceled from TurnOutcome::Canceled, got: {other:?}"),
528 }
529 }
530
531 #[test]
532 fn capture_terminal_llm_event_is_none() {
533 let event = crate::StreamEvent::Llm(crate::LlmEvent::Text("hi".into()));
534 assert!(capture_terminal(Some(&event), false).is_none());
535 }
536
537 #[test]
538 fn observe_terminal_success_sets_outcome() {
539 let (_, mut state) = test_grant_and_state();
540 state.awaiting_terminal = true;
541 let runtime = tokio::runtime::Runtime::new().unwrap();
542 runtime.block_on(async {
543 let rt = crate::Runtime::new().await.unwrap();
544 let result = observe_terminal(&mut state, &rt, Terminal::Success);
545 assert!(result.is_none(), "success should not revoke");
546 assert!(state.outcome.is_some());
547 let (outcome, _) = state.outcome.clone().unwrap();
548 assert_eq!(outcome, Outcome::Success);
549 });
550 }
551
552 #[test]
553 fn observe_terminal_canceled_returns_revoke_reason() {
554 let (_, mut state) = test_grant_and_state();
555 state.awaiting_terminal = true;
556 let runtime = tokio::runtime::Runtime::new().unwrap();
557 runtime.block_on(async {
558 let rt = crate::Runtime::new().await.unwrap();
559 let result = observe_terminal(&mut state, &rt, Terminal::Canceled);
560 assert!(result.is_some());
561 assert!(result.unwrap().contains("canceled"));
562 assert!(state.outcome.is_none());
563 });
564 }
565
566 #[test]
567 fn observe_terminal_blocked_returns_revoke_reason() {
568 let (_, mut state) = test_grant_and_state();
569 state.awaiting_terminal = true;
570 let runtime = tokio::runtime::Runtime::new().unwrap();
571 runtime.block_on(async {
572 let rt = crate::Runtime::new().await.unwrap();
573 let result = observe_terminal(
574 &mut state,
575 &rt,
576 Terminal::Failure(Outcome::Blocked, "unknown".into()),
577 );
578 assert!(result.is_some());
579 assert!(result.unwrap().contains("blocked"));
580 });
581 }
582
583 #[test]
584 fn observe_terminal_provider_error_sets_outcome() {
585 let (_, mut state) = test_grant_and_state();
586 state.awaiting_terminal = true;
587 let runtime = tokio::runtime::Runtime::new().unwrap();
588 runtime.block_on(async {
589 let rt = crate::Runtime::new().await.unwrap();
590 let result = observe_terminal(
591 &mut state,
592 &rt,
593 Terminal::Failure(Outcome::ProviderError, "transient".into()),
594 );
595 assert!(result.is_none(), "provider error should not revoke");
596 let (outcome, kind) = state.outcome.clone().unwrap();
597 assert_eq!(outcome, Outcome::ProviderError);
598 assert_eq!(kind, "transient");
599 });
600 }
601
602 #[test]
603 fn observe_terminal_not_awaiting_is_noop() {
604 let (_, mut state) = test_grant_and_state();
605 state.awaiting_terminal = false;
606 let runtime = tokio::runtime::Runtime::new().unwrap();
607 runtime.block_on(async {
608 let rt = crate::Runtime::new().await.unwrap();
609 let result = observe_terminal(&mut state, &rt, Terminal::Success);
610 assert!(result.is_none());
611 assert!(state.outcome.is_none());
612 });
613 }
614
615 #[test]
616 fn observe_terminal_only_fires_once() {
617 let (_, mut state) = test_grant_and_state();
618 state.awaiting_terminal = true;
619 let runtime = tokio::runtime::Runtime::new().unwrap();
620 runtime.block_on(async {
621 let rt = crate::Runtime::new().await.unwrap();
622 let r1 = observe_terminal(&mut state, &rt, Terminal::Success);
623 assert!(r1.is_none());
624 assert!(state.outcome.is_some());
625 state.outcome = None;
627 let r2 = observe_terminal(&mut state, &rt, Terminal::Success);
628 assert!(r2.is_none());
629 assert!(state.outcome.is_none());
630 });
631 }
632}