1use std::sync::{Arc, Mutex};
2
3use serde_json::Value;
4
5use crate::budget::{BudgetEnforcementBoundary, BudgetEvaluator};
6use crate::events::{RunEvent, RunEventPayload, ToolStatus};
7use crate::result::{PendingToolApproval, RunResult, RunResumeContext, RunState};
8use crate::run_config::INITIAL_BUDGET_USAGE_METADATA_KEY;
9use crate::tools::ToolLifecycleEvent;
10use crate::types::{
11 last_assistant_output, AgentResult, AgentStatus, CompletionReason, ToolDirective,
12};
13
14use super::helpers::terminal_event;
15use super::session_blocking::block_on_session;
16use super::support::{
17 apply_cancellation_precedence, apply_optional_output_validation, apply_output_guardrails,
18 capture_event, effective_event_store, extract_handoff, output_type_validation_error,
19 SingleRunOutcome,
20};
21use super::{effective_session_id, NormalizedInput, Runner};
22
23impl Runner {
24 pub async fn resume(&self, state: RunState) -> Result<RunResult, String> {
25 Box::pin(self.resume_with_optional_input(state, None)).await
26 }
27
28 pub async fn resume_with_input(
29 &self,
30 state: RunState,
31 input: impl Into<NormalizedInput>,
32 ) -> Result<RunResult, String> {
33 Box::pin(self.resume_with_optional_input(state, Some(input.into()))).await
34 }
35
36 async fn resume_with_optional_input(
37 &self,
38 state: RunState,
39 input: Option<NormalizedInput>,
40 ) -> Result<RunResult, String> {
41 let (source, approved_ids, approval_consumption) = state.into_inner();
42 let Some(resume_context) = source.resume_context().cloned() else {
43 return Err("run state does not include resume context".to_string());
44 };
45 let origin_runner = resume_context.runner.clone();
46 if let Some(result) = Box::pin(origin_runner.resume_approved_tool_call(
47 &source,
48 &resume_context,
49 &approved_ids,
50 &approval_consumption,
51 input.as_ref(),
52 ))
53 .await
54 {
55 return result;
56 }
57 let mut config = resume_context.config;
58 config.initial_messages = Some(source.result().messages.clone());
59 config.initial_shared_state = source.result().shared_state.clone();
60 set_initial_budget_usage(&mut config, source.budget_usage())?;
61 let result = origin_runner
62 .run_with_config(
63 &resume_context.agent,
64 input.unwrap_or(resume_context.input),
65 config,
66 )
67 .await
68 .map_err(|error| error.to_string())?;
69 Ok(result)
70 }
71
72 async fn resume_approved_tool_call(
73 &self,
74 source: &RunResult,
75 resume_context: &RunResumeContext,
76 approved_ids: &[String],
77 approval_consumption: &Arc<Mutex<std::collections::BTreeSet<String>>>,
78 resume_input: Option<&NormalizedInput>,
79 ) -> Option<Result<RunResult, String>> {
80 let approval = match select_approved_tool_context(
81 resume_context.pending_tool_approval.as_ref(),
82 approved_ids,
83 ) {
84 Ok(Some(approval)) => approval.clone(),
85 Ok(None) => return None,
86 Err(error) => return Some(Err(error)),
87 };
88 if !approval_snapshot_matches_result(source.result(), &approval) {
89 return Some(Err(
90 "approved tool call does not match the captured interruption".to_string(),
91 ));
92 }
93 if resume_input.is_some() {
94 return Some(Err(
95 "input cannot be provided when resuming an approved tool call".to_string(),
96 ));
97 }
98 let cancellation_token = resume_context
99 .config
100 .cancellation_token
101 .as_ref()
102 .or(self.default_run_config.cancellation_token.as_ref());
103 if cancellation_token.is_some_and(crate::runtime::CancellationToken::is_cancelled) {
104 let mut cancelled = source.result().clone();
105 cancelled.status = AgentStatus::Failed;
106 cancelled.completion_reason = Some(CompletionReason::Cancelled);
107 cancelled.completion_tool_name = None;
108 cancelled.partial_output = cancelled
109 .partial_output
110 .or_else(|| last_assistant_output(&cancelled.cycles));
111 cancelled.error = Some(
112 cancellation_token
113 .and_then(crate::runtime::CancellationToken::reason)
114 .unwrap_or_else(|| "run cancelled".to_string()),
115 );
116 cancelled.budget_exhaustion = None;
117 cancelled.final_answer = None;
118 cancelled.wait_reason = None;
119 return Some(self.finalize_approval_terminal(
120 source,
121 resume_context,
122 &approval.interruption_id,
123 cancelled,
124 source.new_items().to_vec(),
125 cancellation_token,
126 None,
127 Vec::new(),
128 ));
129 }
130 {
131 let mut consumed = approval_consumption
132 .lock()
133 .unwrap_or_else(std::sync::PoisonError::into_inner);
134 if !consumed.insert(approval.interruption_id.clone()) {
135 return Some(Err("approval_already_consumed".to_string()));
136 }
137 }
138 let resumed_run_id = format!("run_{}", uuid::Uuid::new_v4().simple());
139 let budget_limits = resume_context
140 .config
141 .budget_limits
142 .clone()
143 .or_else(|| self.default_run_config.budget_limits.clone());
144 let host_cost_meter = resume_context
145 .config
146 .host_cost_meter
147 .clone()
148 .or_else(|| self.default_run_config.host_cost_meter.clone());
149 let mut budget_evaluator = match budget_limits.filter(|limits| limits.has_limits()) {
150 Some(limits) => {
151 match BudgetEvaluator::new(limits, host_cost_meter, source.budget_usage().cloned())
152 {
153 Ok(evaluator) => Some(Box::new(evaluator)),
154 Err(error) => return Some(Err(error)),
155 }
156 }
157 None => None,
158 };
159 let mut context = approval.context.clone();
160 context.shared_state = source.result().shared_state.clone();
161 let call = approval.call.clone();
162 let lifecycle_observations = Arc::new(Mutex::new(Vec::<ToolLifecycleEvent>::new()));
163 let lifecycle_observations_for_callback = lifecycle_observations.clone();
164 let options = approval
165 .options
166 .clone()
167 .lifecycle_callback(Arc::new(move |event| {
168 lifecycle_observations_for_callback
169 .lock()
170 .unwrap_or_else(std::sync::PoisonError::into_inner)
171 .push(event);
172 }));
173 let execution = approval
174 .orchestrator
175 .run_one_with_approval_and_metadata_deferred(
176 call.clone(),
177 &mut context,
178 options,
179 |_call, _requirement, _context, _metadata| None,
180 )
181 .await
182 .map_err(|error| error.to_string());
183 let mut execution = match execution {
184 Ok(execution) => execution,
185 Err(error) => {
186 let persistence = persist_approval_lifecycle_events(
187 self,
188 source,
189 resume_context,
190 &resumed_run_id,
191 approval.cycle_index,
192 &lifecycle_observations,
193 );
194 return Some(persistence.and(Err(error)));
195 }
196 };
197 let tool_result = execution.result().clone();
198 let mut tool_result = approval.hook_manager.apply_after_tool_call(
199 &approval.task,
200 approval.cycle_index,
201 &call,
202 &context,
203 tool_result,
204 );
205 let behavior_reason = crate::runtime::tool_call_runner::apply_tool_use_behavior(
206 &approval.task,
207 &call,
208 &mut tool_result,
209 );
210 execution.replace_result(tool_result);
211 let tool_result = execution.complete();
212 let resume_tool_events = match persist_approval_lifecycle_events(
213 self,
214 source,
215 resume_context,
216 &resumed_run_id,
217 approval.cycle_index,
218 &lifecycle_observations,
219 ) {
220 Ok(events) => events,
221 Err(error) => return Some(Err(error)),
222 };
223 let mut agent_result = source.result().clone();
224 agent_result.shared_state = context.shared_state.clone();
225 if let Some(cycle) = agent_result
226 .cycles
227 .iter_mut()
228 .find(|cycle| cycle.index == approval.cycle_index)
229 {
230 if let Some(existing) = cycle.tool_results.iter_mut().find(|existing| {
231 existing.tool_call_id == call.id
232 && existing
233 .metadata
234 .get("approval_interruption_id")
235 .and_then(Value::as_str)
236 == Some(approval.interruption_id.as_str())
237 }) {
238 *existing = tool_result.clone();
239 } else {
240 cycle.tool_results.push(tool_result.clone());
241 }
242 }
243 let tool_message = tool_result.to_message();
244 agent_result.messages.retain(|message| {
245 !(message.role == crate::types::MessageRole::Tool
246 && message.tool_call_id.as_deref() == Some(call.id.as_str()))
247 });
248 agent_result.messages.push(tool_message.clone());
249 if let Some(session) = resume_context.config.session.as_ref() {
250 let session_items =
251 crate::sessions::SessionItem::from_message(&tool_message).map(|item| vec![item]);
252 let Some(session_items) = session_items else {
253 return Some(Err(
254 "approved resume messages cannot be persisted to session".to_string(),
255 ));
256 };
257 if let Err(error) = block_on_session(session.add_items(session_items)) {
258 return Some(Err(error));
259 }
260 }
261 let mut new_items = source
262 .new_items()
263 .iter()
264 .filter(|message| {
265 !(message.role == crate::types::MessageRole::Tool
266 && message.tool_call_id.as_deref() == Some(call.id.as_str()))
267 })
268 .cloned()
269 .collect::<Vec<_>>();
270 new_items.push(tool_message);
271
272 let mut resume_budget_events = Vec::new();
273 if let Some(evaluator) = &mut budget_evaluator {
274 let observed_exhaustion = evaluator.tool_batch_complete(false);
275 let snapshot = evaluator.snapshot();
276 let cancelled =
277 cancellation_token.is_some_and(crate::runtime::CancellationToken::is_cancelled);
278 let exhaustion = (!cancelled).then_some(observed_exhaustion).flatten();
279 agent_result.budget_usage = Some(snapshot.clone());
280 agent_result.budget_exhaustion = exhaustion.clone();
281 if exhaustion.is_some() {
282 agent_result.status = AgentStatus::Failed;
283 agent_result.completion_reason = Some(CompletionReason::BudgetExhausted);
284 agent_result.completion_tool_name = None;
285 agent_result.partial_output = last_assistant_output(&agent_result.cycles);
286 agent_result.final_answer = None;
287 agent_result.wait_reason = None;
288 agent_result.error = Some("Run budget exhausted.".to_string());
289 }
290 let payload = match exhaustion.clone() {
291 Some(budget_exhaustion) => RunEventPayload::BudgetExhausted {
292 enforcement_boundary: BudgetEnforcementBoundary::ToolBatchComplete,
293 budget_usage: snapshot,
294 budget_exhaustion,
295 },
296 None => RunEventPayload::BudgetSnapshot {
297 enforcement_boundary: BudgetEnforcementBoundary::ToolBatchComplete,
298 budget_usage: snapshot,
299 },
300 };
301 let mut budget_event = RunEvent::new(
302 &resumed_run_id,
303 source.trace_id(),
304 source.agent_name(),
305 Some(approval.cycle_index),
306 payload,
307 );
308 let session_id = effective_session_id(&self.default_run_config, &resume_context.config);
309 if let Some(session_id) = session_id.as_deref() {
310 budget_event = budget_event.with_session_id(session_id);
311 }
312 let (event_store, event_store_fail_closed) =
313 effective_event_store(&self.default_run_config, &resume_context.config);
314 if let Err(error) = capture_event(
315 None,
316 None,
317 event_store.as_ref(),
318 event_store_fail_closed,
319 budget_event.clone(),
320 ) {
321 return Some(Err(error));
322 }
323 resume_budget_events.push(budget_event);
324 }
325
326 if agent_result.completion_reason != Some(CompletionReason::BudgetExhausted)
327 && tool_result.directive == ToolDirective::Continue
328 {
329 let mut config = resume_context.config.clone();
330 config.initial_messages = Some(agent_result.messages.clone());
331 config.initial_shared_state = agent_result.shared_state.clone();
332 config.trace_id = Some(source.trace_id().to_string());
333 if let Err(error) =
334 set_initial_budget_usage(&mut config, agent_result.budget_usage.as_ref())
335 {
336 return Some(Err(error));
337 }
338 let mut prior_events = Vec::new();
339 if !resume_budget_events.is_empty() {
340 prior_events.extend_from_slice(source.events());
341 }
342 prior_events.extend(resume_tool_events);
343 prior_events.extend(resume_budget_events);
344 let result = self
345 .run_with_config_and_run_id(
346 &resume_context.agent,
347 NormalizedInput::from(source.input().to_string()),
348 config,
349 resumed_run_id,
350 )
351 .await
352 .map(move |result| {
353 let mut events = prior_events;
354 events.extend_from_slice(result.events());
355 let mut metadata = result.metadata().clone();
356 metadata.insert("resumed".to_string(), Value::Bool(true));
357 metadata.insert(
358 "approved_interruption_id".to_string(),
359 Value::String(approval.interruption_id.clone()),
360 );
361 result.with_events(events).with_metadata(metadata)
362 });
363 return Some(result);
364 }
365
366 if agent_result.completion_reason != Some(CompletionReason::BudgetExhausted) {
367 let completion_reason = behavior_reason.unwrap_or(match tool_result.directive {
368 ToolDirective::Finish => CompletionReason::ToolFinish,
369 ToolDirective::WaitUser => CompletionReason::WaitUser,
370 ToolDirective::Continue => unreachable!(),
371 });
372 agent_result.completion_reason = Some(completion_reason);
373 agent_result.completion_tool_name = Some(call.name.clone());
374 agent_result.error = None;
375 match tool_result.directive {
376 ToolDirective::Finish => {
377 agent_result.status = AgentStatus::Completed;
378 agent_result.partial_output = None;
379 agent_result.final_answer =
380 Some(crate::runtime::extract_final_message(&tool_result));
381 agent_result.wait_reason = None;
382 }
383 ToolDirective::WaitUser => {
384 agent_result.status = AgentStatus::WaitUser;
385 agent_result.partial_output = last_assistant_output(&agent_result.cycles);
386 agent_result.final_answer = None;
387 agent_result.wait_reason =
388 Some(crate::runtime::extract_wait_reason(&tool_result));
389 }
390 ToolDirective::Continue => unreachable!(),
391 }
392 }
393 let guardrail_context = context
394 .run_context
395 .clone()
396 .unwrap_or_else(|| crate::RunContext {
397 run_id: source.run_id().to_string(),
398 agent_name: resume_context.agent.name().to_string(),
399 metadata: source.metadata().clone(),
400 ..crate::RunContext::default()
401 });
402 agent_result =
403 apply_output_guardrails(&resume_context.agent, &guardrail_context, agent_result);
404 agent_result = apply_cancellation_precedence(agent_result, cancellation_token);
405 let output_type_validation_error =
406 output_type_validation_error(&resume_context.agent, &agent_result);
407 let (validated_result, output_validation_error) = apply_optional_output_validation(
408 &resume_context.agent,
409 &guardrail_context,
410 agent_result,
411 output_type_validation_error,
412 );
413 agent_result = validated_result;
414 let resumed = match self.finalize_approval_terminal(
415 source,
416 resume_context,
417 &approval.interruption_id,
418 agent_result,
419 new_items,
420 cancellation_token,
421 Some(resumed_run_id),
422 resume_tool_events
423 .into_iter()
424 .chain(resume_budget_events)
425 .collect(),
426 ) {
427 Ok(resumed) => resumed,
428 Err(error) => return Some(Err(error)),
429 };
430 if let Some(error) = output_validation_error {
431 return Some(Err(error));
432 }
433 let Some(handoff) = extract_handoff(resumed.result()) else {
434 return Some(Ok(resumed));
435 };
436 let event_collector = Arc::new(Mutex::new(resumed.events().to_vec()));
437 let initial_outcome = SingleRunOutcome {
438 result: resumed,
439 handoff: Some(handoff),
440 };
441 let runner = self.clone();
442 let agent = resume_context.agent.clone();
443 let input = resume_context.input.clone();
444 let config = resume_context.config.clone();
445 Some(
446 tokio::task::spawn_blocking(move || {
447 runner.run_agent_chain_with_initial(
448 &agent,
449 input,
450 config,
451 Some(event_collector),
452 None,
453 None,
454 Some(initial_outcome),
455 None,
456 )
457 })
458 .await
459 .map_err(|error| format!("resume handoff task failed: {error}"))
460 .and_then(|result| result),
461 )
462 }
463
464 #[allow(clippy::too_many_arguments)] fn finalize_approval_terminal(
466 &self,
467 source: &RunResult,
468 resume_context: &RunResumeContext,
469 interruption_id: &str,
470 agent_result: AgentResult,
471 new_items: Vec<crate::types::Message>,
472 cancellation_token: Option<&crate::runtime::CancellationToken>,
473 resumed_run_id: Option<String>,
474 additional_events: Vec<RunEvent>,
475 ) -> Result<RunResult, String> {
476 let resumed_run_id =
477 resumed_run_id.unwrap_or_else(|| format!("run_{}", uuid::Uuid::new_v4().simple()));
478 let mut events = source.events().to_vec();
479 events.extend(additional_events);
480 let mut resumed = RunResult::new(
481 resume_context.agent.name().to_string(),
482 agent_result,
483 source
484 .resolved_model()
485 .cloned()
486 .expect("interrupted runs have a resolved model"),
487 )
488 .with_ids(&resumed_run_id, source.trace_id())
489 .with_input(source.input())
490 .with_new_items(new_items)
491 .with_events(events)
492 .with_metadata({
493 let mut metadata = source.metadata().clone();
494 metadata.insert("resumed".to_string(), Value::Bool(true));
495 metadata.insert(
496 "approved_interruption_id".to_string(),
497 Value::String(interruption_id.to_string()),
498 );
499 metadata
500 })
501 .with_resume_context(resume_context.clone());
502 let event_collector = Arc::new(Mutex::new(resumed.events().to_vec()));
503 let session_id = effective_session_id(&self.default_run_config, &resume_context.config);
504 let (event_store, event_store_fail_closed) =
505 effective_event_store(&self.default_run_config, &resume_context.config);
506 capture_event(
507 Some(&event_collector),
508 None,
509 event_store.as_ref(),
510 event_store_fail_closed,
511 terminal_event(
512 resumed.result(),
513 resumed.run_id(),
514 resumed.trace_id(),
515 resume_context.agent.name(),
516 session_id.as_deref(),
517 cancellation_token,
518 ),
519 )?;
520 let events = event_collector
521 .lock()
522 .map(|events| events.clone())
523 .unwrap_or_default();
524 resumed = resumed.with_events(events);
525 Ok(resumed)
526 }
527}
528
529fn persist_approval_lifecycle_events(
530 runner: &Runner,
531 source: &RunResult,
532 resume_context: &RunResumeContext,
533 resumed_run_id: &str,
534 cycle_index: u32,
535 observations: &Arc<Mutex<Vec<ToolLifecycleEvent>>>,
536) -> Result<Vec<RunEvent>, String> {
537 let observations = std::mem::take(
538 &mut *observations
539 .lock()
540 .unwrap_or_else(std::sync::PoisonError::into_inner),
541 );
542 let session_id = effective_session_id(&runner.default_run_config, &resume_context.config);
543 let (event_store, event_store_fail_closed) =
544 effective_event_store(&runner.default_run_config, &resume_context.config);
545 let mut events = Vec::with_capacity(observations.len());
546 for observation in observations {
547 let mut event = approval_lifecycle_run_event(
548 observation,
549 resumed_run_id,
550 source.trace_id(),
551 source.agent_name(),
552 cycle_index,
553 );
554 if let Some(session_id) = session_id.as_deref() {
555 event = event.with_session_id(session_id);
556 }
557 capture_event(
558 None,
559 None,
560 event_store.as_ref(),
561 event_store_fail_closed,
562 event.clone(),
563 )?;
564 events.push(event);
565 }
566 Ok(events)
567}
568
569fn approval_lifecycle_run_event(
570 observation: ToolLifecycleEvent,
571 run_id: &str,
572 trace_id: &str,
573 agent_name: &str,
574 cycle_index: u32,
575) -> RunEvent {
576 match observation {
577 ToolLifecycleEvent::Planned {
578 call,
579 tool_metadata,
580 } => RunEvent::tool_call_planned(
581 run_id,
582 trace_id,
583 agent_name,
584 cycle_index,
585 call.id,
586 call.name,
587 Value::Object(call.arguments.into_iter().collect()),
588 )
589 .with_tool_metadata(tool_metadata.as_ref()),
590 ToolLifecycleEvent::Started {
591 call,
592 tool_metadata,
593 } => RunEvent::tool_call_started(
594 run_id,
595 trace_id,
596 agent_name,
597 cycle_index,
598 call.id,
599 call.name,
600 Value::Object(call.arguments.into_iter().collect()),
601 )
602 .with_tool_metadata(tool_metadata.as_ref()),
603 ToolLifecycleEvent::Completed {
604 call,
605 result,
606 execution_started,
607 duration_ms,
608 tool_metadata,
609 } => {
610 let status = match result.status {
611 crate::types::ToolResultStatus::Success => ToolStatus::Success,
612 crate::types::ToolResultStatus::Error => ToolStatus::Error,
613 crate::types::ToolResultStatus::WaitResponse => ToolStatus::WaitResponse,
614 crate::types::ToolResultStatus::Running => ToolStatus::Running,
615 crate::types::ToolResultStatus::PendingCompress => ToolStatus::PendingCompress,
616 };
617 RunEvent::new(
618 run_id,
619 trace_id,
620 agent_name,
621 Some(cycle_index),
622 RunEventPayload::ToolCallCompleted {
623 tool_call_id: result.tool_call_id.clone(),
624 tool_name: call.name,
625 status,
626 directive: result.directive,
627 error_code: result.error_code.clone(),
628 execution_started,
629 duration_ms,
630 },
631 )
632 .with_tool_metadata(tool_metadata.as_ref())
633 .with_metadata(
634 "tool_arguments",
635 Value::Object(call.arguments.into_iter().collect()),
636 )
637 .with_metadata(
638 "metadata",
639 Value::Object(result.metadata.into_iter().collect()),
640 )
641 .with_metadata("content", Value::String(result.content))
642 }
643 }
644}
645
646fn set_initial_budget_usage(
647 config: &mut crate::run_config::RunConfig,
648 usage: Option<&crate::budget::BudgetUsageSnapshot>,
649) -> Result<(), String> {
650 match usage {
651 Some(usage) => {
652 let value = serde_json::to_value(usage)
653 .map_err(|error| format!("failed to serialize resumed budget usage: {error}"))?;
654 config
655 .metadata
656 .insert(INITIAL_BUDGET_USAGE_METADATA_KEY.to_string(), value);
657 }
658 None => {
659 config.metadata.remove(INITIAL_BUDGET_USAGE_METADATA_KEY);
660 }
661 }
662 Ok(())
663}
664
665fn select_approved_tool_context<'a>(
666 pending: Option<&'a PendingToolApproval>,
667 approved_ids: &[String],
668) -> Result<Option<&'a PendingToolApproval>, String> {
669 if approved_ids.is_empty() {
670 return Ok(None);
671 }
672 let pending = pending.ok_or_else(|| {
673 "approved tool call is missing its captured interruption context".to_string()
674 })?;
675 if !approved_ids.iter().any(|id| id == &pending.interruption_id) {
676 return Err("approved tool call is missing its captured interruption context".to_string());
677 }
678 Ok(Some(pending))
679}
680
681fn approval_snapshot_matches_result(result: &AgentResult, approval: &PendingToolApproval) -> bool {
682 result.cycles.iter().any(|cycle| {
683 cycle.index == approval.cycle_index
684 && cycle.tool_calls.iter().any(|call| call == &approval.call)
685 && cycle.tool_results.iter().any(|tool_result| {
686 tool_result.tool_call_id == approval.call.id
687 && tool_result
688 .metadata
689 .get("approval_interruption_id")
690 .and_then(Value::as_str)
691 == Some(approval.interruption_id.as_str())
692 && tool_result
693 .metadata
694 .get("tool_name")
695 .and_then(Value::as_str)
696 == Some(approval.call.name.as_str())
697 && tool_result.metadata.get("arguments")
698 == Some(&Value::Object(
699 approval.call.arguments.clone().into_iter().collect(),
700 ))
701 })
702 })
703}
704
705#[cfg(test)]
706mod tests {
707 use super::select_approved_tool_context;
708
709 #[test]
710 fn approved_id_without_captured_context_fails_closed() {
711 let error = match select_approved_tool_context(None, &["approval_1".to_string()]) {
712 Ok(_) => panic!("missing context must fail"),
713 Err(error) => error,
714 };
715 assert_eq!(
716 error,
717 "approved tool call is missing its captured interruption context"
718 );
719 }
720
721 #[test]
722 fn conversational_resume_without_approved_id_needs_no_approval_context() {
723 assert!(select_approved_tool_context(None, &[])
724 .expect("conversational resume")
725 .is_none());
726 }
727}