1use crate::error::Result;
6use crate::interrupt::Interrupt;
7use crate::state::State;
8use crate::stream::StreamEvent;
9use crate::timeout::ProgressHandle;
10use async_trait::async_trait;
11use serde_json::Value;
12use std::collections::HashMap;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16
17#[derive(Clone)]
19pub struct ExecutionConfig {
20 pub thread_id: String,
22 pub resume_from: Option<String>,
24 pub recursion_limit: usize,
26 pub metadata: HashMap<String, Value>,
28 pub parent_context: Option<Arc<dyn adk_core::InvocationContext>>,
37}
38
39impl ExecutionConfig {
40 pub fn new(thread_id: &str) -> Self {
42 Self {
43 thread_id: thread_id.to_string(),
44 resume_from: None,
45 recursion_limit: 50,
46 metadata: HashMap::new(),
47 parent_context: None,
48 }
49 }
50
51 #[must_use]
57 pub fn with_parent_context(mut self, parent: Arc<dyn adk_core::InvocationContext>) -> Self {
58 self.parent_context = Some(parent);
59 self
60 }
61
62 pub fn with_recursion_limit(mut self, limit: usize) -> Self {
64 self.recursion_limit = limit;
65 self
66 }
67
68 pub fn with_resume_from(mut self, checkpoint_id: &str) -> Self {
70 self.resume_from = Some(checkpoint_id.to_string());
71 self
72 }
73
74 pub fn with_metadata(mut self, key: &str, value: Value) -> Self {
76 self.metadata.insert(key.to_string(), value);
77 self
78 }
79}
80
81impl Default for ExecutionConfig {
82 fn default() -> Self {
83 Self::new(&uuid::Uuid::new_v4().to_string())
84 }
85}
86
87pub struct NodeContext {
89 pub state: State,
91 pub config: ExecutionConfig,
93 pub step: usize,
95 progress_handle: Option<ProgressHandle>,
98 children: Option<std::sync::Arc<crate::child::ChildInvoker>>,
100 parent_schema: Option<std::sync::Arc<crate::state::StateSchema>>,
102}
103
104impl NodeContext {
105 pub fn new(state: State, config: ExecutionConfig, step: usize) -> Self {
107 Self { state, config, step, progress_handle: None, children: None, parent_schema: None }
108 }
109
110 pub fn parent_schema(&self) -> Option<std::sync::Arc<crate::state::StateSchema>> {
116 self.parent_schema.clone()
117 }
118
119 pub fn set_parent_schema(&mut self, schema: std::sync::Arc<crate::state::StateSchema>) {
121 self.parent_schema = Some(schema);
122 }
123
124 pub(crate) fn child_invoker(&self) -> Option<std::sync::Arc<crate::child::ChildInvoker>> {
125 self.children.clone()
126 }
127
128 pub(crate) fn set_child_invoker(
130 &mut self,
131 invoker: std::sync::Arc<crate::child::ChildInvoker>,
132 ) {
133 self.children = Some(invoker);
134 }
135
136 pub async fn run_node(&self, child: &str, input: Value) -> Result<Value> {
153 self.run_node_with(child, input, crate::child::RunNodeOptions::default()).await
154 }
155
156 pub async fn run_node_with(
163 &self,
164 child: &str,
165 input: Value,
166 options: crate::child::RunNodeOptions,
167 ) -> Result<Value> {
168 let invoker = self.children.as_ref().ok_or_else(|| {
169 crate::error::GraphError::InvalidGraph(
170 "this node cannot invoke other nodes: no child invoker was attached".to_string(),
171 )
172 })?;
173 invoker.run(child, input, options, self).await
174 }
175
176 pub fn get(&self, key: &str) -> Option<&Value> {
178 self.state.get(key)
179 }
180
181 pub fn get_as<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
183 self.state.get(key).and_then(|v| serde_json::from_value(v.clone()).ok())
184 }
185
186 pub fn report_progress(&self) {
204 if let Some(handle) = &self.progress_handle {
205 handle.report_progress();
206 }
207 }
208
209 pub fn set_progress_handle(&mut self, handle: ProgressHandle) {
214 self.progress_handle = Some(handle);
215 }
216
217 pub fn progress_handle(&self) -> Option<&ProgressHandle> {
219 self.progress_handle.as_ref()
220 }
221}
222
223#[derive(Default)]
225pub struct NodeOutput {
226 pub updates: HashMap<String, Value>,
228 pub interrupt: Option<Interrupt>,
230 pub events: Vec<StreamEvent>,
232 pub goto: Option<Vec<String>>,
234 pub goto_parent: Option<Vec<String>>,
239}
240
241impl NodeOutput {
242 pub fn new() -> Self {
244 Self::default()
245 }
246
247 pub fn with_goto_parent<I, S>(mut self, targets: I) -> Self
293 where
294 I: IntoIterator<Item = S>,
295 S: Into<String>,
296 {
297 self.goto_parent = Some(targets.into_iter().map(Into::into).collect());
298 self
299 }
300
301 pub fn with_goto<I, S>(mut self, targets: I) -> Self
302 where
303 I: IntoIterator<Item = S>,
304 S: Into<String>,
305 {
306 self.goto = Some(targets.into_iter().map(Into::into).collect());
307 self
308 }
309
310 pub fn with_update(mut self, key: &str, value: impl Into<Value>) -> Self {
312 self.updates.insert(key.to_string(), value.into());
313 self
314 }
315
316 pub fn with_updates(mut self, updates: HashMap<String, Value>) -> Self {
318 self.updates.extend(updates);
319 self
320 }
321
322 pub fn with_interrupt(mut self, interrupt: Interrupt) -> Self {
324 self.interrupt = Some(interrupt);
325 self
326 }
327
328 pub fn with_event(mut self, event: StreamEvent) -> Self {
330 self.events.push(event);
331 self
332 }
333
334 pub fn interrupt(message: &str) -> Self {
336 Self::new().with_interrupt(crate::interrupt::interrupt(message))
337 }
338
339 pub fn interrupt_with_data(message: &str, data: Value) -> Self {
341 Self::new().with_interrupt(crate::interrupt::interrupt_with_data(message, data))
342 }
343}
344
345#[async_trait]
347pub trait Node: Send + Sync {
348 fn name(&self) -> &str;
350
351 fn description(&self) -> &str {
353 "Graph workflow node"
354 }
355
356 fn capabilities(&self) -> adk_core::AgentCapabilities {
358 adk_core::AgentCapabilities::default()
359 }
360
361 async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput>;
363
364 fn validate_against(&self, _parent: &crate::state::StateSchema) -> Result<()> {
382 Ok(())
383 }
384
385 fn validate(&self) -> Result<()> {
386 Ok(())
387 }
388
389 fn execute_stream<'a>(
396 &'a self,
397 ctx: &'a NodeContext,
398 ) -> Pin<Box<dyn futures::Stream<Item = Result<StreamEvent>> + Send + 'a>> {
399 let name = self.name().to_string();
400 Box::pin(async_stream::stream! {
401 match self.execute(ctx).await {
402 Ok(output) => {
403 for event in output.events {
404 yield Ok(event);
405 }
406 if let Some(targets) = output.goto {
410 yield Ok(StreamEvent::route_dispatched(&name, targets));
411 }
412 if let Some(interrupt) = output.interrupt {
413 let (message, data) = match interrupt {
414 crate::interrupt::Interrupt::Dynamic { message, data } => (message, data),
415 other => (other.to_string(), None),
416 };
417 yield Ok(StreamEvent::node_interrupt(&name, &message, data));
418 }
419 yield Ok(StreamEvent::Updates { node: name, updates: output.updates });
420 }
421 Err(e) => yield Err(e),
422 }
423 })
424 }
425}
426
427pub type BoxedNode = Box<dyn Node>;
429
430pub type AsyncNodeFn = Box<
432 dyn Fn(NodeContext) -> Pin<Box<dyn Future<Output = Result<NodeOutput>> + Send>> + Send + Sync,
433>;
434
435pub struct FunctionNode {
437 name: String,
438 func: AsyncNodeFn,
439}
440
441impl FunctionNode {
442 pub fn new<F, Fut>(name: &str, func: F) -> Self
444 where
445 F: Fn(NodeContext) -> Fut + Send + Sync + 'static,
446 Fut: Future<Output = Result<NodeOutput>> + Send + 'static,
447 {
448 Self { name: name.to_string(), func: Box::new(move |ctx| Box::pin(func(ctx))) }
449 }
450}
451
452#[async_trait]
453impl Node for FunctionNode {
454 fn name(&self) -> &str {
455 &self.name
456 }
457
458 async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
459 let mut ctx_owned = NodeContext::new(ctx.state.clone(), ctx.config.clone(), ctx.step);
462 if let Some(handle) = ctx.progress_handle() {
463 ctx_owned.set_progress_handle(handle.clone());
464 }
465 if let Some(invoker) = ctx.child_invoker() {
466 ctx_owned.set_child_invoker(invoker);
467 }
468 if let Some(schema) = ctx.parent_schema() {
469 ctx_owned.set_parent_schema(schema);
470 }
471 (self.func)(ctx_owned).await
472 }
473}
474
475pub struct PassthroughNode {
477 name: String,
478}
479
480impl PassthroughNode {
481 pub fn new(name: &str) -> Self {
483 Self { name: name.to_string() }
484 }
485}
486
487#[async_trait]
488impl Node for PassthroughNode {
489 fn name(&self) -> &str {
490 &self.name
491 }
492
493 async fn execute(&self, _ctx: &NodeContext) -> Result<NodeOutput> {
494 Ok(NodeOutput::new())
495 }
496}
497
498pub type AgentInputMapper = Box<dyn Fn(&State) -> adk_core::Content + Send + Sync>;
500
501pub type AgentOutputMapper =
503 Box<dyn Fn(&[adk_core::Event]) -> HashMap<String, Value> + Send + Sync>;
504
505pub type AgentGotoMapper =
509 Box<dyn Fn(&HashMap<String, Value>) -> Option<Vec<String>> + Send + Sync>;
510
511pub struct AgentNode {
513 name: String,
514 #[allow(dead_code)]
515 agent: Arc<dyn adk_core::Agent>,
516 input_mapper: AgentInputMapper,
518 output_mapper: AgentOutputMapper,
520 goto_mapper: Option<AgentGotoMapper>,
522}
523
524impl AgentNode {
525 pub fn new(agent: Arc<dyn adk_core::Agent>) -> Self {
527 let name = agent.name().to_string();
528 Self {
529 name,
530 agent,
531 input_mapper: Box::new(default_input_mapper),
532 output_mapper: Box::new(default_output_mapper),
533 goto_mapper: None,
534 }
535 }
536
537 pub fn with_input_mapper<F>(mut self, mapper: F) -> Self
539 where
540 F: Fn(&State) -> adk_core::Content + Send + Sync + 'static,
541 {
542 self.input_mapper = Box::new(mapper);
543 self
544 }
545
546 pub fn with_output_mapper<F>(mut self, mapper: F) -> Self
548 where
549 F: Fn(&[adk_core::Event]) -> HashMap<String, Value> + Send + Sync + 'static,
550 {
551 self.output_mapper = Box::new(mapper);
552 self
553 }
554
555 pub fn with_goto_mapper<F>(mut self, mapper: F) -> Self
580 where
581 F: Fn(&HashMap<String, Value>) -> Option<Vec<String>> + Send + Sync + 'static,
582 {
583 self.goto_mapper = Some(Box::new(mapper));
584 self
585 }
586}
587
588fn default_input_mapper(state: &State) -> adk_core::Content {
590 if let Some(messages) = state.get("messages")
592 && let Some(arr) = messages.as_array()
593 && let Some(last) = arr.last()
594 && let Some(content) = last.get("content").and_then(|c| c.as_str())
595 {
596 return adk_core::Content::new("user").with_text(content);
597 }
598
599 if let Some(input) = state.get("input")
601 && let Some(text) = input.as_str()
602 {
603 return adk_core::Content::new("user").with_text(text);
604 }
605
606 adk_core::Content::new("user")
607}
608
609fn default_output_mapper(events: &[adk_core::Event]) -> HashMap<String, Value> {
611 let mut updates = HashMap::new();
612
613 let mut messages = Vec::new();
615 for event in events {
616 if let Some(content) = event.content() {
617 let text = content.parts.iter().filter_map(|p| p.text()).collect::<Vec<_>>().join("");
618
619 if !text.is_empty() {
620 messages.push(serde_json::json!({
621 "role": "assistant",
622 "content": text
623 }));
624 }
625 }
626 }
627
628 if !messages.is_empty() {
629 updates.insert("messages".to_string(), serde_json::json!(messages));
630 }
631
632 updates
633}
634
635#[async_trait]
636impl Node for AgentNode {
637 fn name(&self) -> &str {
638 &self.name
639 }
640
641 fn description(&self) -> &str {
642 self.agent.description()
643 }
644
645 fn capabilities(&self) -> adk_core::AgentCapabilities {
646 self.agent.capabilities()
647 }
648
649 async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
650 use futures::StreamExt;
651
652 let content = (self.input_mapper)(&ctx.state);
654
655 let invocation_ctx = Arc::new(GraphInvocationContext::with_parent(
657 ctx.config.thread_id.clone(),
658 content,
659 self.agent.clone(),
660 ctx.config.parent_context.clone(),
661 ));
662
663 let stream = self.agent.run(invocation_ctx).await.map_err(|e| {
665 crate::error::GraphError::NodeExecutionFailed {
666 node: self.name.clone(),
667 message: e.to_string(),
668 }
669 })?;
670
671 let events: Vec<adk_core::Event> = stream.filter_map(|r| async { r.ok() }).collect().await;
672
673 let updates = (self.output_mapper)(&events);
675 let goto = self.goto_mapper.as_ref().and_then(|mapper| mapper(&updates));
676
677 let mut output = NodeOutput::new().with_updates(updates);
679 if let Some(targets) = goto {
680 output = output.with_goto(targets);
681 }
682 for event in &events {
683 if let Ok(json) = serde_json::to_value(event) {
684 output = output.with_event(StreamEvent::custom(&self.name, "agent_event", json));
685 }
686 }
687
688 Ok(output)
689 }
690
691 fn execute_stream<'a>(
692 &'a self,
693 ctx: &'a NodeContext,
694 ) -> Pin<Box<dyn futures::Stream<Item = Result<StreamEvent>> + Send + 'a>> {
695 use futures::StreamExt;
696 let name = self.name.clone();
697 let agent = self.agent.clone();
698 let input_mapper = &self.input_mapper;
699 let output_mapper = &self.output_mapper;
700 let goto_mapper = &self.goto_mapper;
701 let parent_context = ctx.config.parent_context.clone();
702 let thread_id = ctx.config.thread_id.clone();
703 let content = (input_mapper)(&ctx.state);
704
705 Box::pin(async_stream::stream! {
706 tracing::debug!("AgentNode::execute_stream called for {}", name);
707 let invocation_ctx = Arc::new(GraphInvocationContext::with_parent(
708 thread_id,
709 content,
710 agent.clone(),
711 parent_context,
712 ));
713
714 let stream = match agent.run(invocation_ctx).await {
715 Ok(s) => s,
716 Err(e) => {
717 yield Err(crate::error::GraphError::NodeExecutionFailed {
718 node: name.clone(),
719 message: e.to_string(),
720 });
721 return;
722 }
723 };
724
725 tokio::pin!(stream);
726 let mut all_events = Vec::new();
727
728 while let Some(result) = stream.next().await {
729 match result {
730 Ok(event) => {
731 if let Some(content) = event.content() {
733 let text: String = content.parts.iter().filter_map(|p| p.text()).collect();
734 if !text.is_empty() {
735 yield Ok(StreamEvent::Message {
736 node: name.clone(),
737 content: text,
738 is_final: false,
739 });
740 }
741 }
742 all_events.push(event);
743 }
744 Err(e) => {
745 yield Err(crate::error::GraphError::NodeExecutionFailed {
746 node: name.clone(),
747 message: e.to_string(),
748 });
749 return;
750 }
751 }
752 }
753
754 for event in &all_events {
756 if let Ok(json) = serde_json::to_value(event) {
757 yield Ok(StreamEvent::custom(&name, "agent_event", json));
758 }
759 }
760
761 let updates = (output_mapper)(&all_events);
765 if let Some(targets) = goto_mapper.as_ref().and_then(|mapper| mapper(&updates)) {
767 yield Ok(StreamEvent::route_dispatched(&name, targets));
768 }
769 yield Ok(StreamEvent::Updates { node: name.clone(), updates });
770 })
771 }
772}
773
774struct GraphInvocationContext {
776 invocation_id: String,
777 user_content: adk_core::Content,
778 agent: Arc<dyn adk_core::Agent>,
779 session: Arc<GraphSession>,
780 run_config: adk_core::RunConfig,
781 ended: std::sync::atomic::AtomicBool,
782 parent: Option<Arc<dyn adk_core::InvocationContext>>,
788 user_id: String,
790 app_name: String,
791 branch: String,
792}
793
794const STANDALONE_USER_ID: &str = "graph_user";
796const STANDALONE_APP_NAME: &str = "graph_app";
798
799impl GraphInvocationContext {
800 fn with_parent(
801 session_id: String,
802 user_content: adk_core::Content,
803 agent: Arc<dyn adk_core::Agent>,
804 parent: Option<Arc<dyn adk_core::InvocationContext>>,
805 ) -> Self {
806 let invocation_id = uuid::Uuid::new_v4().to_string();
807 let session = Arc::new(GraphSession::new(session_id));
808 session.append_content(user_content.clone());
810
811 let (user_id, app_name, branch, run_config) = match parent.as_ref() {
814 Some(parent) => (
815 parent.user_id().to_string(),
816 parent.app_name().to_string(),
817 match parent.branch() {
818 "" => agent.name().to_string(),
819 existing => format!("{existing}.{}", agent.name()),
820 },
821 parent.run_config().clone(),
822 ),
823 None => (
824 STANDALONE_USER_ID.to_string(),
825 STANDALONE_APP_NAME.to_string(),
826 "main".to_string(),
827 adk_core::RunConfig::default(),
828 ),
829 };
830
831 Self {
832 invocation_id,
833 user_content,
834 agent,
835 session,
836 run_config,
837 ended: std::sync::atomic::AtomicBool::new(false),
838 parent,
839 user_id,
840 app_name,
841 branch,
842 }
843 }
844}
845
846impl adk_core::ReadonlyContext for GraphInvocationContext {
848 fn invocation_id(&self) -> &str {
849 &self.invocation_id
850 }
851
852 fn agent_name(&self) -> &str {
853 self.agent.name()
854 }
855
856 fn user_id(&self) -> &str {
857 &self.user_id
858 }
859
860 fn app_name(&self) -> &str {
861 &self.app_name
862 }
863
864 fn session_id(&self) -> &str {
865 &self.session.id
866 }
867
868 fn branch(&self) -> &str {
869 &self.branch
870 }
871
872 fn user_content(&self) -> &adk_core::Content {
873 &self.user_content
874 }
875}
876
877#[async_trait]
879impl adk_core::CallbackContext for GraphInvocationContext {
880 fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
881 self.parent.as_ref().and_then(|parent| parent.artifacts())
882 }
883
884 fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
885 self.parent.as_ref().and_then(|parent| parent.shared_state())
886 }
887}
888
889#[async_trait]
891impl adk_core::InvocationContext for GraphInvocationContext {
892 fn agent(&self) -> Arc<dyn adk_core::Agent> {
893 self.agent.clone()
894 }
895
896 fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
897 self.parent.as_ref().and_then(|parent| parent.memory())
898 }
899
900 fn session(&self) -> &dyn adk_core::Session {
901 self.session.as_ref()
902 }
903
904 fn run_config(&self) -> &adk_core::RunConfig {
905 &self.run_config
906 }
907
908 fn end_invocation(&self) {
909 self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
910 if let Some(parent) = &self.parent {
911 parent.end_invocation();
912 }
913 }
914
915 fn ended(&self) -> bool {
916 self.ended.load(std::sync::atomic::Ordering::SeqCst)
917 || self.parent.as_ref().is_some_and(|parent| parent.ended())
918 }
919
920 fn is_cancelled(&self) -> bool {
921 self.parent.as_ref().is_some_and(|parent| parent.is_cancelled())
922 }
923
924 fn user_scopes(&self) -> Vec<String> {
925 self.parent.as_ref().map(|parent| parent.user_scopes()).unwrap_or_default()
926 }
927
928 fn request_metadata(&self) -> std::collections::HashMap<String, Value> {
929 self.parent.as_ref().map(|parent| parent.request_metadata()).unwrap_or_default()
930 }
931
932 async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
933 match &self.parent {
934 Some(parent) => parent.get_secret(name).await,
935 None => Ok(None),
936 }
937 }
938
939 async fn get_secret_for(
940 &self,
941 request: &adk_core::SecretRequest,
942 ) -> adk_core::Result<Option<String>> {
943 match &self.parent {
944 Some(parent) => parent.get_secret_for(request).await,
945 None => Ok(None),
946 }
947 }
948}
949
950struct GraphSession {
952 id: String,
953 state: GraphState,
954 history: std::sync::RwLock<Vec<adk_core::Content>>,
955}
956
957impl GraphSession {
958 fn new(id: String) -> Self {
959 Self { id, state: GraphState::new(), history: std::sync::RwLock::new(Vec::new()) }
960 }
961
962 fn append_content(&self, content: adk_core::Content) {
963 if let Ok(mut h) = self.history.write() {
964 h.push(content);
965 }
966 }
967}
968
969impl adk_core::Session for GraphSession {
970 fn id(&self) -> &str {
971 &self.id
972 }
973
974 fn app_name(&self) -> &str {
975 "graph_app"
976 }
977
978 fn user_id(&self) -> &str {
979 "graph_user"
980 }
981
982 fn state(&self) -> &dyn adk_core::State {
983 &self.state
984 }
985
986 fn conversation_history(&self) -> Vec<adk_core::Content> {
987 self.history.read().ok().map(|h| h.clone()).unwrap_or_default()
988 }
989
990 fn append_to_history(&self, content: adk_core::Content) {
991 self.append_content(content);
992 }
993}
994
995struct GraphState {
997 data: std::sync::RwLock<std::collections::HashMap<String, serde_json::Value>>,
998}
999
1000impl GraphState {
1001 fn new() -> Self {
1002 Self { data: std::sync::RwLock::new(std::collections::HashMap::new()) }
1003 }
1004}
1005
1006impl adk_core::State for GraphState {
1007 fn get(&self, key: &str) -> Option<serde_json::Value> {
1008 self.data.read().ok()?.get(key).cloned()
1009 }
1010
1011 fn set(&mut self, key: String, value: serde_json::Value) {
1012 if let Err(msg) = adk_core::validate_state_key(&key) {
1013 tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
1014 return;
1015 }
1016 if let Ok(mut data) = self.data.write() {
1017 data.insert(key, value);
1018 }
1019 }
1020
1021 fn all(&self) -> std::collections::HashMap<String, serde_json::Value> {
1022 self.data.read().ok().map(|d| d.clone()).unwrap_or_default()
1023 }
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028 use super::*;
1029
1030 #[tokio::test]
1031 async fn test_function_node() {
1032 let node = FunctionNode::new("test", |_ctx| async {
1033 Ok(NodeOutput::new().with_update("result", serde_json::json!("success")))
1034 });
1035
1036 assert_eq!(node.name(), "test");
1037
1038 let ctx = NodeContext::new(State::new(), ExecutionConfig::default(), 0);
1039 let output = node.execute(&ctx).await.unwrap();
1040
1041 assert_eq!(output.updates.get("result"), Some(&serde_json::json!("success")));
1042 }
1043
1044 #[tokio::test]
1045 async fn test_passthrough_node() {
1046 let node = PassthroughNode::new("pass");
1047 let ctx = NodeContext::new(State::new(), ExecutionConfig::default(), 0);
1048 let output = node.execute(&ctx).await.unwrap();
1049
1050 assert!(output.updates.is_empty());
1051 assert!(output.interrupt.is_none());
1052 }
1053
1054 #[test]
1055 fn test_node_output_builder() {
1056 let output = NodeOutput::new().with_update("a", 1).with_update("b", "hello");
1057
1058 assert_eq!(output.updates.get("a"), Some(&serde_json::json!(1)));
1059 assert_eq!(output.updates.get("b"), Some(&serde_json::json!("hello")));
1060 }
1061}