Skip to main content

pe_tools/
tool_node.rs

1//! ToolNode — built-in graph node that handles tool execution.
2//!
3//! Reads [`ToolCall`] entries from the last [`AiMessage`](pe_core::message::AiMessage) in state,
4//! executes all tool calls in parallel via `tokio::spawn`, and appends
5//! [`ToolMessage`](pe_core::message::ToolMessage) results back to state.
6//!
7//! Used in the standard ReAct pattern:
8//! ```text
9//! chat (LLM) → tools_condition → tool_node → chat (loop)
10//! ```
11
12use crate::interceptor::ToolCallInterceptor;
13use crate::registry::ToolRegistry;
14use pe_core::error::PeError;
15use pe_core::message::{Message, ToolCall};
16use pe_core::node::{NodeContext, NodeFn, NodeFuture, NodeResult};
17use pe_core::state::{CoreState, CoreStateUpdate};
18use std::marker::PhantomData;
19use std::sync::Arc;
20
21/// Built-in graph node that handles tool execution.
22///
23/// Reads `ToolCall` entries from the last `AiMessage` in `state.messages()`.
24/// Executes all tool calls in parallel (`tokio::spawn` per call).
25/// Appends `ToolMessage` results to state via `StateUpdate`.
26///
27/// # Error handling
28///
29/// When `handle_tool_errors` is `true` (default), tool failures become
30/// `ToolMessage`s with error text — the LLM sees the error and can retry.
31/// When `false`, the first tool error propagates as `NodeResult::Error`.
32///
33/// Unknown tool names always produce an error `ToolMessage` regardless
34/// of the `handle_tool_errors` setting.
35///
36/// # Example
37///
38/// ```ignore
39/// let tool_node = ToolNode::<MyState>::new(Arc::new(registry));
40///
41/// graph.add_node("tools", tool_node);
42/// graph.add_conditional_edge("chat", tools_condition::<MyState>);
43/// graph.add_edge("tools", "chat");
44/// ```
45pub struct ToolNode<S: CoreState> {
46    registry: Arc<ToolRegistry>,
47    handle_tool_errors: bool,
48    interceptor: Option<Arc<dyn ToolCallInterceptor>>,
49    _phantom: PhantomData<S>,
50}
51
52impl<S: CoreState> ToolNode<S>
53where
54    S::Update: CoreStateUpdate,
55{
56    /// Create a new ToolNode with the given registry.
57    /// Defaults to `handle_tool_errors = true`.
58    pub fn new(registry: Arc<ToolRegistry>) -> Self {
59        Self {
60            registry,
61            handle_tool_errors: true,
62            interceptor: None,
63            _phantom: PhantomData,
64        }
65    }
66
67    /// Set whether tool errors become ToolMessages (true) or propagate (false).
68    #[must_use = "builder methods return the modified builder"]
69    pub fn with_error_handling(mut self, handle: bool) -> Self {
70        self.handle_tool_errors = handle;
71        self
72    }
73
74    /// Set a [`ToolCallInterceptor`] that runs before each tool execution.
75    #[must_use = "builder methods return the modified builder"]
76    pub fn with_interceptor(mut self, interceptor: impl ToolCallInterceptor + 'static) -> Self {
77        self.interceptor = Some(Arc::new(interceptor));
78        self
79    }
80}
81
82impl<S: CoreState> NodeFn<S> for ToolNode<S>
83where
84    S::Update: CoreStateUpdate,
85{
86    fn call(&self, state: &S, ctx: &NodeContext) -> NodeFuture<S::Update> {
87        let registry = self.registry.clone();
88        let handle_errors = self.handle_tool_errors;
89        let interceptor = self.interceptor.clone();
90        let tool_observer = ctx.tool_observer.clone();
91
92        // Extract tool calls from the last AiMessage
93        let tool_calls = extract_tool_calls(state.messages());
94        if tool_calls.is_empty() {
95            // No tool calls — return empty update (graph should not route here)
96            return Box::pin(async move { NodeResult::Update(S::Update::default()) });
97        }
98
99        Box::pin(async move {
100            // Apply interceptor if set
101            let tool_calls = match &interceptor {
102                Some(int) => tool_calls.into_iter().map(|tc| int.intercept(tc)).collect(),
103                None => tool_calls,
104            };
105
106            // Execute all tool calls in parallel, with optional observer notifications
107            let tasks: Vec<_> = tool_calls
108                .into_iter()
109                .map(|tc| {
110                    let reg = registry.clone();
111                    let obs = tool_observer.clone();
112                    tokio::spawn(async move {
113                        execute_single_tool_observed(&reg, tc, handle_errors, obs.as_deref()).await
114                    })
115                })
116                .collect();
117
118            let results = futures::future::join_all(tasks).await;
119
120            // Collect results, handling both tool results and propagated errors
121            let mut messages = Vec::new();
122            for join_result in results {
123                match join_result {
124                    Ok(ToolExecResult::Message(msg)) => messages.push(msg),
125                    Ok(ToolExecResult::PropagateError(err)) => {
126                        return NodeResult::Error(err);
127                    }
128                    Err(join_err) => {
129                        // tokio::spawn panicked — internal error
130                        return NodeResult::Error(PeError::Internal {
131                            details: format!("Tool task panicked: {join_err}"),
132                        });
133                    }
134                }
135            }
136
137            // Build state update that appends the ToolMessages
138            let update = <S::Update as CoreStateUpdate>::from_messages(messages);
139            NodeResult::Update(update)
140        })
141    }
142
143    fn name(&self) -> &str {
144        "tool_node"
145    }
146}
147
148/// Result of executing a single tool call.
149enum ToolExecResult {
150    /// A ToolMessage to append to state.
151    Message(Message),
152    /// An error to propagate (when handle_tool_errors=false).
153    PropagateError(PeError),
154}
155
156/// Execute a single tool call with optional observer notifications.
157///
158/// When an observer is present, emits `on_tool_start` before execution
159/// and `on_tool_complete`/`on_tool_error` after.
160async fn execute_single_tool_observed(
161    registry: &ToolRegistry,
162    tc: ToolCall,
163    handle_errors: bool,
164    observer: Option<&dyn pe_core::node::ToolObserver>,
165) -> ToolExecResult {
166    // Summarize input for the observer (truncate large args)
167    let input_summary = summarize_tool_input(&tc);
168
169    if let Some(obs) = observer {
170        obs.on_tool_start(&tc.name, &input_summary).await;
171    }
172
173    let start = std::time::Instant::now();
174    let result = match registry.get(&tc.name) {
175        None => {
176            tracing::warn!("Tool '{}' not found in registry", tc.name);
177            if let Some(obs) = observer {
178                obs.on_tool_error(&tc.name, "Tool not found in registry")
179                    .await;
180            }
181            return ToolExecResult::Message(Message::tool(
182                format!("Tool '{}' not found in registry.", tc.name),
183                tc.id,
184            ));
185        }
186        Some(tool) => tool.execute_structured(tc.args.clone()).await,
187    };
188    let elapsed = start.elapsed();
189
190    match result {
191        Ok(tool_result) => {
192            if let Some(obs) = observer {
193                obs.on_tool_complete(&tc.name, elapsed).await;
194            }
195            let metadata = if tool_result.metadata.is_empty() {
196                None
197            } else {
198                Some(tool_result.metadata)
199            };
200            ToolExecResult::Message(Message::tool_with_metadata(
201                tool_result.output.to_string(),
202                tc.id,
203                metadata,
204                Some(elapsed.as_millis() as u64),
205            ))
206        }
207        Err(e) if handle_errors => {
208            tracing::warn!("Tool '{}' failed (handled): {}", tc.name, e);
209            if let Some(obs) = observer {
210                obs.on_tool_error(&tc.name, &e.to_string()).await;
211            }
212            ToolExecResult::Message(Message::tool_with_metadata(
213                format!("Tool error: {e}"),
214                tc.id,
215                None,
216                Some(elapsed.as_millis() as u64),
217            ))
218        }
219        Err(e) => {
220            tracing::error!("Tool '{}' failed (propagating): {}", tc.name, e);
221            if let Some(obs) = observer {
222                obs.on_tool_error(&tc.name, &e.to_string()).await;
223            }
224            ToolExecResult::PropagateError(PeError::ToolExecution {
225                tool: tc.name,
226                reason: e.to_string(),
227            })
228        }
229    }
230}
231
232/// Create a brief summary of tool input for streaming events.
233fn summarize_tool_input(tc: &ToolCall) -> String {
234    let s = tc.args.to_string();
235    if s.len() <= 100 {
236        s
237    } else {
238        format!("{}...", &s[..97])
239    }
240}
241
242/// Extract tool calls from the last AiMessage in the message list.
243fn extract_tool_calls(messages: &[Message]) -> Vec<ToolCall> {
244    messages
245        .iter()
246        .rev()
247        .find_map(|m| {
248            if let Message::Ai(ai) = m {
249                if !ai.tool_calls.is_empty() {
250                    return Some(ai.tool_calls.clone());
251                }
252            }
253            None
254        })
255        .unwrap_or_default()
256}
257
258impl<S: CoreState> std::fmt::Debug for ToolNode<S>
259where
260    S::Update: CoreStateUpdate,
261{
262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        f.debug_struct("ToolNode")
264            .field("handle_tool_errors", &self.handle_tool_errors)
265            .field("has_interceptor", &self.interceptor.is_some())
266            .finish()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::interceptor::ToolCallInterceptor;
274    use crate::tool::FunctionTool;
275    use pe_core::message::{AiMessage, MessageContent};
276    use pe_core::state::{CoreStateUpdate, ExecutionContext, StateUpdate};
277    use serde::{Deserialize, Serialize};
278    use std::collections::HashMap;
279    use std::time::{Duration, Instant};
280
281    // ---- Test state implementation ----
282
283    #[derive(Debug, Clone, Serialize, Deserialize)]
284    struct ToolTestState {
285        messages: Vec<Message>,
286        iterations: u32,
287        thread_id: String,
288        context: ExecutionContext,
289    }
290
291    #[derive(Debug, Clone, Default, Serialize, Deserialize)]
292    struct ToolTestUpdate {
293        messages: Option<Vec<Message>>,
294    }
295
296    impl StateUpdate for ToolTestUpdate {}
297
298    impl CoreStateUpdate for ToolTestUpdate {
299        fn from_messages(msgs: Vec<Message>) -> Self {
300            Self {
301                messages: Some(msgs),
302            }
303        }
304    }
305
306    impl pe_core::state::State for ToolTestState {
307        type Update = ToolTestUpdate;
308        fn apply(&mut self, update: Self::Update) {
309            if let Some(msgs) = update.messages {
310                self.messages.extend(msgs);
311            }
312        }
313    }
314
315    impl CoreState for ToolTestState {
316        fn messages(&self) -> &[Message] {
317            &self.messages
318        }
319        fn messages_mut(&mut self) -> &mut Vec<Message> {
320            &mut self.messages
321        }
322        fn iterations(&self) -> u32 {
323            self.iterations
324        }
325        fn set_iterations(&mut self, n: u32) {
326            self.iterations = n;
327        }
328        fn thread_id(&self) -> &str {
329            &self.thread_id
330        }
331        fn context(&self) -> &ExecutionContext {
332            &self.context
333        }
334        fn context_mut(&mut self) -> &mut ExecutionContext {
335            &mut self.context
336        }
337    }
338
339    fn make_ctx() -> NodeContext {
340        NodeContext {
341            step: 1,
342            recursion_limit: 25,
343            node_name: "tool_node".into(),
344            activation: pe_core::node::ActivationReason::EntryPoint,
345            metadata: HashMap::new(),
346            phase_store: pe_core::phase_store::PhaseStateStore::new(),
347            stream_sender: None,
348            tool_observer: None,
349            lobe_runtime_service_factory: None,
350        }
351    }
352
353    fn make_state_with_tool_calls(calls: Vec<ToolCall>) -> ToolTestState {
354        ToolTestState {
355            messages: vec![Message::Ai(AiMessage {
356                content: MessageContent::Text("Calling tools...".into()),
357                tool_calls: calls,
358                invalid_tool_calls: vec![],
359                usage_metadata: None,
360                response_metadata: HashMap::new(),
361                id: None,
362            })],
363            iterations: 0,
364            thread_id: "t1".into(),
365            context: ExecutionContext::new("test"),
366        }
367    }
368
369    fn make_echo_registry() -> ToolRegistry {
370        let mut reg = ToolRegistry::new();
371        reg.register(FunctionTool::new(
372            "echo",
373            "Echoes input",
374            serde_json::json!({"type": "object"}),
375            |input| Box::pin(async move { Ok(input) }),
376        ))
377        .unwrap();
378        reg
379    }
380
381    // ---- Tests ----
382
383    #[tokio::test]
384    async fn single_tool_call_produces_tool_message() {
385        let reg = make_echo_registry();
386        let node = ToolNode::<ToolTestState>::new(Arc::new(reg));
387
388        let state = make_state_with_tool_calls(vec![ToolCall {
389            id: "tc_1".into(),
390            name: "echo".into(),
391            args: serde_json::json!({"msg": "hello"}),
392        }]);
393
394        let result = node.call(&state, &make_ctx()).await;
395
396        match result {
397            NodeResult::Update(update) => {
398                let msgs = update.messages.expect("should have messages");
399                assert_eq!(msgs.len(), 1);
400                if let Message::Tool(tm) = &msgs[0] {
401                    assert_eq!(tm.tool_call_id, "tc_1");
402                    assert!(tm.content.contains("hello"));
403                } else {
404                    panic!("expected ToolMessage, got {:?}", msgs[0]);
405                }
406            }
407            other => panic!("expected Update, got {:?}", other),
408        }
409    }
410
411    #[tokio::test]
412    async fn parallel_tool_calls_all_execute() {
413        let mut reg = ToolRegistry::new();
414        // Each tool sleeps 50ms to verify parallelism
415        for name in ["tool_a", "tool_b", "tool_c"] {
416            let n = name.to_string();
417            reg.register(FunctionTool::new(
418                name,
419                format!("Tool {name}"),
420                serde_json::json!({"type": "object"}),
421                move |_| {
422                    let n = n.clone();
423                    Box::pin(async move {
424                        tokio::time::sleep(Duration::from_millis(50)).await;
425                        Ok(serde_json::json!({"from": n}))
426                    })
427                },
428            ))
429            .unwrap();
430        }
431
432        let node = ToolNode::<ToolTestState>::new(Arc::new(reg));
433        let state = make_state_with_tool_calls(vec![
434            ToolCall {
435                id: "tc_a".into(),
436                name: "tool_a".into(),
437                args: serde_json::json!({}),
438            },
439            ToolCall {
440                id: "tc_b".into(),
441                name: "tool_b".into(),
442                args: serde_json::json!({}),
443            },
444            ToolCall {
445                id: "tc_c".into(),
446                name: "tool_c".into(),
447                args: serde_json::json!({}),
448            },
449        ]);
450
451        let start = Instant::now();
452        let result = node.call(&state, &make_ctx()).await;
453        let elapsed = start.elapsed();
454
455        // 3 tools each sleeping 50ms — if parallel, total < 150ms
456        assert!(
457            elapsed < Duration::from_millis(150),
458            "Expected parallel execution (<150ms), took {:?}",
459            elapsed
460        );
461
462        match result {
463            NodeResult::Update(update) => {
464                let msgs = update.messages.expect("should have messages");
465                assert_eq!(msgs.len(), 3);
466                // All should be ToolMessages
467                for msg in &msgs {
468                    assert!(matches!(msg, Message::Tool(_)));
469                }
470            }
471            other => panic!("expected Update, got {:?}", other),
472        }
473    }
474
475    #[tokio::test]
476    async fn tool_error_handled_becomes_tool_message() {
477        let mut reg = ToolRegistry::new();
478        reg.register(FunctionTool::new(
479            "fail",
480            "Always fails",
481            serde_json::json!({"type": "object"}),
482            |_| {
483                Box::pin(async {
484                    Err(PeError::ToolExecution {
485                        tool: "fail".into(),
486                        reason: "boom".into(),
487                    })
488                })
489            },
490        ))
491        .unwrap();
492
493        let node = ToolNode::<ToolTestState>::new(Arc::new(reg)).with_error_handling(true);
494
495        let state = make_state_with_tool_calls(vec![ToolCall {
496            id: "tc_fail".into(),
497            name: "fail".into(),
498            args: serde_json::json!({}),
499        }]);
500
501        let result = node.call(&state, &make_ctx()).await;
502        match result {
503            NodeResult::Update(update) => {
504                let msgs = update.messages.expect("should have messages");
505                assert_eq!(msgs.len(), 1);
506                if let Message::Tool(tm) = &msgs[0] {
507                    assert!(tm.content.contains("Tool error"));
508                    assert!(tm.content.contains("boom"));
509                    assert_eq!(tm.tool_call_id, "tc_fail");
510                } else {
511                    panic!("expected ToolMessage");
512                }
513            }
514            other => panic!("expected Update, got {:?}", other),
515        }
516    }
517
518    #[tokio::test]
519    async fn tool_error_propagated_when_handling_disabled() {
520        let mut reg = ToolRegistry::new();
521        reg.register(FunctionTool::new(
522            "fail",
523            "Always fails",
524            serde_json::json!({"type": "object"}),
525            |_| {
526                Box::pin(async {
527                    Err(PeError::ToolExecution {
528                        tool: "fail".into(),
529                        reason: "critical failure".into(),
530                    })
531                })
532            },
533        ))
534        .unwrap();
535
536        let node = ToolNode::<ToolTestState>::new(Arc::new(reg)).with_error_handling(false);
537
538        let state = make_state_with_tool_calls(vec![ToolCall {
539            id: "tc_fail".into(),
540            name: "fail".into(),
541            args: serde_json::json!({}),
542        }]);
543
544        let result = node.call(&state, &make_ctx()).await;
545        assert!(result.is_error(), "expected Error, got {:?}", result);
546    }
547
548    #[tokio::test]
549    async fn unknown_tool_always_produces_not_found_message() {
550        let reg = ToolRegistry::new(); // empty
551        let node = ToolNode::<ToolTestState>::new(Arc::new(reg));
552
553        let state = make_state_with_tool_calls(vec![ToolCall {
554            id: "tc_missing".into(),
555            name: "nonexistent".into(),
556            args: serde_json::json!({}),
557        }]);
558
559        let result = node.call(&state, &make_ctx()).await;
560        match result {
561            NodeResult::Update(update) => {
562                let msgs = update.messages.expect("should have messages");
563                assert_eq!(msgs.len(), 1);
564                if let Message::Tool(tm) = &msgs[0] {
565                    assert!(tm.content.contains("not found"));
566                    assert_eq!(tm.tool_call_id, "tc_missing");
567                } else {
568                    panic!("expected ToolMessage");
569                }
570            }
571            other => panic!("expected Update, got {:?}", other),
572        }
573    }
574
575    #[tokio::test]
576    async fn interceptor_modifies_args_before_execution() {
577        struct InjectKeyInterceptor;
578        impl ToolCallInterceptor for InjectKeyInterceptor {
579            fn intercept(&self, mut call: ToolCall) -> ToolCall {
580                if let Some(obj) = call.args.as_object_mut() {
581                    obj.insert("injected".into(), serde_json::json!(true));
582                }
583                call
584            }
585        }
586
587        let reg = make_echo_registry();
588        let node =
589            ToolNode::<ToolTestState>::new(Arc::new(reg)).with_interceptor(InjectKeyInterceptor);
590
591        let state = make_state_with_tool_calls(vec![ToolCall {
592            id: "tc_int".into(),
593            name: "echo".into(),
594            args: serde_json::json!({"original": "data"}),
595        }]);
596
597        let result = node.call(&state, &make_ctx()).await;
598        match result {
599            NodeResult::Update(update) => {
600                let msgs = update.messages.expect("should have messages");
601                assert_eq!(msgs.len(), 1);
602                if let Message::Tool(tm) = &msgs[0] {
603                    // The echoed output should contain the injected key
604                    assert!(tm.content.contains("injected"));
605                    assert!(tm.content.contains("original"));
606                } else {
607                    panic!("expected ToolMessage");
608                }
609            }
610            other => panic!("expected Update, got {:?}", other),
611        }
612    }
613
614    #[tokio::test]
615    async fn tool_message_includes_duration_ms() {
616        let reg = make_echo_registry();
617        let node = ToolNode::<ToolTestState>::new(Arc::new(reg));
618
619        let state = make_state_with_tool_calls(vec![ToolCall {
620            id: "tc_dur".into(),
621            name: "echo".into(),
622            args: serde_json::json!({"msg": "timing"}),
623        }]);
624
625        let result = node.call(&state, &make_ctx()).await;
626        match result {
627            NodeResult::Update(update) => {
628                let msgs = update.messages.expect("should have messages");
629                assert_eq!(msgs.len(), 1);
630                if let Message::Tool(tm) = &msgs[0] {
631                    assert!(
632                        tm.duration_ms.is_some(),
633                        "ToolMessage should include duration_ms"
634                    );
635                    // Duration should be reasonable (< 5 seconds)
636                    assert!(tm.duration_ms.unwrap() < 5000);
637                } else {
638                    panic!("expected ToolMessage, got {:?}", msgs[0]);
639                }
640            }
641            other => panic!("expected Update, got {:?}", other),
642        }
643    }
644
645    #[tokio::test]
646    async fn tool_error_message_includes_duration_ms() {
647        let mut reg = ToolRegistry::new();
648        reg.register(FunctionTool::new(
649            "fail",
650            "Always fails",
651            serde_json::json!({"type": "object"}),
652            |_| {
653                Box::pin(async {
654                    Err(PeError::ToolExecution {
655                        tool: "fail".into(),
656                        reason: "boom".into(),
657                    })
658                })
659            },
660        ))
661        .unwrap();
662
663        let node = ToolNode::<ToolTestState>::new(Arc::new(reg)).with_error_handling(true);
664
665        let state = make_state_with_tool_calls(vec![ToolCall {
666            id: "tc_fail_dur".into(),
667            name: "fail".into(),
668            args: serde_json::json!({}),
669        }]);
670
671        let result = node.call(&state, &make_ctx()).await;
672        match result {
673            NodeResult::Update(update) => {
674                let msgs = update.messages.expect("should have messages");
675                assert_eq!(msgs.len(), 1);
676                if let Message::Tool(tm) = &msgs[0] {
677                    assert!(
678                        tm.duration_ms.is_some(),
679                        "Error ToolMessage should include duration_ms"
680                    );
681                } else {
682                    panic!("expected ToolMessage");
683                }
684            }
685            other => panic!("expected Update, got {:?}", other),
686        }
687    }
688
689    #[tokio::test]
690    async fn structured_tool_metadata_passes_through() {
691        // Create a tool that returns structured metadata
692        let mut reg = ToolRegistry::new();
693        reg.register(crate::tool::StructuredFunctionTool::new(
694            "search",
695            "Search things",
696            serde_json::json!({"type": "object"}),
697            |_input| {
698                Box::pin(async move {
699                    Ok(
700                        crate::tool::ToolResult::ok(serde_json::json!({"results": [1, 2, 3]}))
701                            .with_metadata("result_count", serde_json::json!(3))
702                            .with_metadata("source", serde_json::json!("index")),
703                    )
704                })
705            },
706        ))
707        .unwrap();
708
709        let node = ToolNode::<ToolTestState>::new(Arc::new(reg));
710        let state = make_state_with_tool_calls(vec![ToolCall {
711            id: "tc_meta".into(),
712            name: "search".into(),
713            args: serde_json::json!({}),
714        }]);
715
716        let result = node.call(&state, &make_ctx()).await;
717        match result {
718            NodeResult::Update(update) => {
719                let msgs = update.messages.expect("should have messages");
720                assert_eq!(msgs.len(), 1);
721                if let Message::Tool(tm) = &msgs[0] {
722                    let meta = tm.metadata.as_ref().expect("should have metadata");
723                    assert_eq!(meta["result_count"], serde_json::json!(3));
724                    assert_eq!(meta["source"], serde_json::json!("index"));
725                } else {
726                    panic!("expected ToolMessage");
727                }
728            }
729            other => panic!("expected Update, got {:?}", other),
730        }
731    }
732
733    #[tokio::test]
734    async fn no_tool_calls_returns_empty_update() {
735        let reg = make_echo_registry();
736        let node = ToolNode::<ToolTestState>::new(Arc::new(reg));
737
738        // State with AI message but no tool calls
739        let state = ToolTestState {
740            messages: vec![Message::ai("No tools needed")],
741            iterations: 0,
742            thread_id: "t1".into(),
743            context: ExecutionContext::new("test"),
744        };
745
746        let result = node.call(&state, &make_ctx()).await;
747        match result {
748            NodeResult::Update(update) => {
749                assert!(
750                    update.messages.is_none(),
751                    "empty update should have no messages"
752                );
753            }
754            other => panic!("expected Update, got {:?}", other),
755        }
756    }
757}