1use crate::agent::wire_id;
2use crate::error::InvalidError;
3use crate::limits::MAX_GRAPH_NAME_BYTES;
4use crate::query::{Consistency, Filter, Value};
5use serde::{Deserialize, Serialize};
6
7wire_id!(
8 NodeId
12);
13wire_id!(
14 EdgeId
17);
18
19impl NodeId {
20 pub fn content(label: &str, value: &[u8]) -> Self {
27 Self::from_u128(crate::hashing::content_id(&[label.as_bytes(), &[0], value]))
28 }
29}
30
31impl EdgeId {
32 pub fn content(from: NodeId, edge_type: &str, to: NodeId) -> Self {
36 Self::from_u128(crate::hashing::content_id(&[
37 &from.to_bytes(),
38 &[0],
39 edge_type.as_bytes(),
40 &[0],
41 &to.to_bytes(),
42 ]))
43 }
44}
45
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum EdgeDir {
50 #[default]
52 Out,
53 In,
55 Both,
57}
58
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum GraphReturn {
63 #[default]
65 Nodes,
66 Edges,
68 Paths,
70 Triplets,
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
77pub struct Hop {
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub edge_type: Option<String>,
80 #[serde(default, skip_serializing_if = "EdgeDir::is_out")]
81 pub dir: EdgeDir,
82 pub max: u32,
83}
84
85impl EdgeDir {
86 pub fn is_out(&self) -> bool {
88 matches!(self, EdgeDir::Out)
89 }
90}
91
92#[derive(Clone, Debug, Serialize, Deserialize)]
95pub enum GraphStart {
96 Ids(Vec<NodeId>),
97 Match(Filter),
98 Nearest { embedding: Vec<f32>, k: usize },
99}
100
101#[derive(Clone, Debug, Serialize, Deserialize)]
105pub struct GraphQuery {
106 pub v: u32,
107 pub graph: String,
108 pub start: GraphStart,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
110 pub traverse: Vec<Hop>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub node_filter: Option<Filter>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub edge_filter: Option<Filter>,
115 #[serde(default, skip_serializing_if = "GraphReturn::is_nodes")]
116 pub return_: GraphReturn,
117 pub limit: usize,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub fork: Option<String>,
120 #[serde(default, skip_serializing_if = "Consistency::is_eventual")]
121 pub consistency: Consistency,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub as_of: Option<u64>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub conversation: Option<String>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub stream: Option<String>,
137}
138
139impl GraphReturn {
140 pub fn is_nodes(&self) -> bool {
142 matches!(self, GraphReturn::Nodes)
143 }
144}
145
146#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct GraphNeighbors {
150 pub v: u32,
151 pub graph: String,
152 pub node: NodeId,
153 #[serde(default, skip_serializing_if = "EdgeDir::is_out")]
154 pub dir: EdgeDir,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub edge_type: Option<String>,
157 pub depth: u32,
158 pub limit: usize,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub as_of: Option<u64>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub conversation: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub stream: Option<String>,
172}
173
174#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
184pub enum SourceRef {
185 Message {
192 stream: u32,
193 topic: u32,
194 partition: u32,
195 offset: u64,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 generation: Option<u64>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 conversation: Option<String>,
201 },
202 Kv { namespace: String, key: String },
204 Memory { id: String },
206}
207
208#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
210pub struct ProducerInfo {
211 pub name: String,
212 pub version: String,
213}
214
215#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
217pub struct GraphNode {
218 pub id: NodeId,
219 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub labels: Vec<String>,
221 #[serde(default, skip_serializing_if = "Vec::is_empty")]
222 pub attrs: Vec<(String, Value)>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub embedding: Option<Vec<f32>>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub source: Option<SourceRef>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub producer: Option<ProducerInfo>,
231}
232
233impl GraphNode {
234 pub fn entity(label: impl Into<String>, value: impl Into<String>) -> Self {
240 let label = label.into();
241 let value = value.into();
242 let id = NodeId::content(&label, value.as_bytes());
243 Self {
244 id,
245 labels: vec![label],
246 attrs: vec![("value".to_owned(), Value::from(value))],
247 embedding: None,
248 source: None,
249 producer: None,
250 }
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
257pub struct GraphEdge {
258 pub id: EdgeId,
259 pub from: NodeId,
260 pub to: NodeId,
261 pub edge_type: String,
262 pub weight: f32,
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub attrs: Vec<(String, Value)>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub valid_from: Option<u64>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub valid_to: Option<u64>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub source: Option<SourceRef>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub producer: Option<ProducerInfo>,
282}
283
284impl GraphEdge {
285 pub fn relate(from: &GraphNode, edge_type: impl Into<String>, to: &GraphNode) -> Self {
289 let edge_type = edge_type.into();
290 Self {
291 id: EdgeId::content(from.id, &edge_type, to.id),
292 from: from.id,
293 to: to.id,
294 edge_type,
295 weight: 1.0,
296 attrs: Vec::new(),
297 valid_from: None,
298 valid_to: None,
299 source: None,
300 producer: None,
301 }
302 }
303
304 pub fn with_source(mut self, source: SourceRef) -> Self {
307 self.source = Some(source);
308 self
309 }
310
311 pub fn valid(mut self, from: Option<u64>, to: Option<u64>) -> Self {
316 self.valid_from = from;
317 self.valid_to = to;
318 self
319 }
320
321 pub fn valid_at(&self, at: u64) -> bool {
325 self.valid_from.is_none_or(|from| at >= from) && self.valid_to.is_none_or(|to| at < to)
326 }
327}
328
329#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
331pub struct Path {
332 pub nodes: Vec<NodeId>,
333 pub edges: Vec<EdgeId>,
334}
335
336#[derive(Clone, Debug, Default, Serialize, Deserialize)]
339pub struct GraphResult {
340 #[serde(default, skip_serializing_if = "Vec::is_empty")]
341 pub nodes: Vec<GraphNode>,
342 #[serde(default, skip_serializing_if = "Vec::is_empty")]
343 pub edges: Vec<GraphEdge>,
344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
345 pub paths: Vec<Path>,
346}
347
348#[derive(Clone, Debug, Default, Serialize, Deserialize)]
351pub struct GraphUpsert {
352 pub v: u32,
353 pub graph: String,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub session: Option<crate::agent::SessionRef>,
356 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 pub nodes: Vec<GraphNode>,
358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub edges: Vec<GraphEdge>,
360}
361
362#[derive(Clone, Debug, Serialize, Deserialize)]
365#[non_exhaustive]
366pub enum GraphReply {
367 Ok(GraphResult),
368 Err(GraphError),
369}
370
371#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
373#[non_exhaustive]
374pub enum GraphError {
375 #[error("graph not supported: {0}")]
376 Unsupported(String),
377 #[error("unauthorized: {0}")]
378 Unauthorized(String),
379 #[error("invalid graph name: {0}")]
381 InvalidName(String),
382 #[error("graph not found: {0}")]
383 NotFound(String),
384 #[error("traversal too large: {what} is {size}, exceeds cap {cap}")]
385 TooLarge {
386 what: String,
387 size: usize,
388 cap: usize,
389 },
390 #[error("graph backend error: {0}")]
391 Backend(String),
392 #[error("temporarily unavailable: {0}")]
393 Unavailable(String),
394 #[error("unsupported graph op version (expected {expected}, got {got})")]
395 Version { expected: u32, got: u32 },
396}
397
398pub fn validate_graph_name(name: &str) -> Result<(), InvalidError> {
402 if name.is_empty() {
403 return Err(InvalidError::new("graph name must not be empty"));
404 }
405 if name.len() > MAX_GRAPH_NAME_BYTES {
406 return Err(InvalidError::new(format!(
407 "graph name is {}B, exceeds cap {MAX_GRAPH_NAME_BYTES}B",
408 name.len()
409 )));
410 }
411 if name.bytes().any(|byte| byte.is_ascii_control()) {
412 return Err(InvalidError::new(
413 "graph name must not contain ASCII control characters",
414 ));
415 }
416 Ok(())
417}
418
419#[cfg(all(test, feature = "cbor"))]
420mod tests {
421 use super::*;
422 use crate::codes::GRAPH_OP_VERSION;
423 use crate::framing::{decode_named, encode_named};
424 use crate::query::CmpOp;
425
426 #[test]
427 fn given_a_graph_query_when_round_tripped_then_should_preserve_traversal() {
428 let query = GraphQuery {
429 v: GRAPH_OP_VERSION,
430 graph: "knowledge".to_owned(),
431 start: GraphStart::Match(Filter::pred("label", CmpOp::Eq, "Person")),
432 traverse: vec![
433 Hop {
434 edge_type: Some("works_at".to_owned()),
435 dir: EdgeDir::Out,
436 max: 1,
437 },
438 Hop {
439 edge_type: Some("located_in".to_owned()),
440 dir: EdgeDir::Out,
441 max: 1,
442 },
443 ],
444 node_filter: None,
445 edge_filter: None,
446 return_: GraphReturn::Paths,
447 limit: 100,
448 fork: None,
449 consistency: Consistency::Eventual,
450 as_of: Some(1_900_000_000_000_000),
451 conversation: None,
452 stream: None,
453 };
454 let bytes = encode_named(&query).expect("serializes");
455 let back: GraphQuery = decode_named(&bytes).expect("deserializes");
456 assert_eq!(back.graph, "knowledge");
457 assert_eq!(back.traverse.len(), 2);
458 assert_eq!(back.return_, GraphReturn::Paths);
459 assert_eq!(back.as_of, Some(1_900_000_000_000_000));
460 }
461
462 #[test]
463 fn given_a_graph_result_when_round_tripped_then_should_preserve_nodes_and_edges() {
464 let reply = GraphReply::Ok(GraphResult {
465 nodes: vec![GraphNode {
466 producer: None,
467 id: NodeId::from_u128(1),
468 labels: vec!["Person".to_owned()],
469 attrs: vec![("name".to_owned(), Value::from("Alice"))],
470 embedding: None,
471 source: None,
472 }],
473 edges: vec![GraphEdge {
474 producer: None,
475 id: EdgeId::from_u128(2),
476 from: NodeId::from_u128(1),
477 to: NodeId::from_u128(3),
478 edge_type: "works_at".to_owned(),
479 weight: 1.0,
480 attrs: Vec::new(),
481 valid_from: None,
482 valid_to: None,
483 source: None,
484 }],
485 paths: Vec::new(),
486 });
487 let bytes = encode_named(&reply).expect("serializes");
488 let back: GraphReply = decode_named(&bytes).expect("deserializes");
489 let GraphReply::Ok(result) = back else {
490 panic!("expected Ok");
491 };
492 assert_eq!(result.nodes.len(), 1);
493 assert_eq!(result.edges[0].edge_type, "works_at");
494 }
495
496 #[test]
497 fn given_a_nearest_start_when_round_tripped_then_should_preserve_the_seed() {
498 let query = GraphQuery {
499 v: GRAPH_OP_VERSION,
500 graph: "knowledge".to_owned(),
501 start: GraphStart::Nearest {
502 embedding: vec![0.1, 0.2, 0.3],
503 k: 5,
504 },
505 traverse: Vec::new(),
506 node_filter: None,
507 edge_filter: None,
508 return_: GraphReturn::Nodes,
509 limit: 10,
510 fork: None,
511 consistency: Consistency::Eventual,
512 as_of: None,
513 conversation: None,
514 stream: None,
515 };
516 let bytes = encode_named(&query).expect("serializes");
517 let back: GraphQuery = decode_named(&bytes).expect("deserializes");
518 match back.start {
519 GraphStart::Nearest { embedding, k } => {
520 assert_eq!(embedding, vec![0.1, 0.2, 0.3]);
521 assert_eq!(k, 5);
522 }
523 other => panic!("expected Nearest, got {other:?}"),
524 }
525 }
526
527 #[test]
528 fn given_a_node_id_when_round_tripped_through_a_string_then_should_be_equal() {
529 let id = NodeId::from_u128(987_654_321);
530 let parsed: NodeId = id.to_string().parse().expect("a node id parses");
531 assert_eq!(parsed, id);
532 }
533
534 #[test]
535 fn given_the_same_entity_when_addressed_twice_then_should_converge_on_one_node_id() {
536 let a = NodeId::content("Person", b"Alice");
537 let b = NodeId::content("Person", b"Alice");
538 assert_eq!(a, b, "the same entity is one node");
539 assert_ne!(a, NodeId::content("Company", b"Alice"));
541 assert_ne!(a, NodeId::content("Person", b"Bob"));
542 }
543
544 #[test]
545 fn given_the_pinned_entity_when_addressed_then_should_match_the_golden_id() {
546 assert_eq!(
549 NodeId::content("Person", b"Alice").to_string(),
550 "13NCEPHNVFHHGNK9GD3MT0W1AB"
551 );
552 }
553
554 #[test]
555 fn given_two_nodes_when_related_then_should_content_address_the_edge() {
556 let alice = GraphNode::entity("Person", "Alice");
557 let acme = GraphNode::entity("Company", "Acme");
558 let one = GraphEdge::relate(&alice, "works_at", &acme);
559 let two = GraphEdge::relate(&alice, "works_at", &acme);
560 assert_eq!(one.id, two.id, "the same relationship is one edge");
561 assert_eq!(one.from, alice.id);
562 assert_eq!(one.to, acme.id);
563 assert_ne!(one.id, GraphEdge::relate(&acme, "works_at", &alice).id);
565 }
566
567 #[test]
568 fn given_an_edge_validity_window_when_checked_then_should_hold_only_inside_it() {
569 let alice = GraphNode::entity("User", "alice");
570 let pool = GraphNode::entity("Pool", "eu-west");
571 let edge = GraphEdge::relate(&alice, "in_pool", &pool).valid(Some(100), Some(200));
572 assert!(!edge.valid_at(99), "before the window");
573 assert!(edge.valid_at(100), "the lower bound is inclusive");
574 assert!(edge.valid_at(150), "inside the window");
575 assert!(!edge.valid_at(200), "the upper bound is exclusive");
576 let open = GraphEdge::relate(&alice, "in_pool", &pool);
577 assert!(open.valid_at(0) && open.valid_at(u64::MAX));
578 assert_eq!(edge.id, open.id, "validity is not part of edge identity");
579 }
580
581 #[test]
582 fn given_an_edge_without_validity_when_serialized_then_should_omit_the_window() {
583 let edge = GraphEdge::relate(
584 &GraphNode::entity("A", "x"),
585 "rel",
586 &GraphNode::entity("B", "y"),
587 );
588 let json = serde_json::to_string(&edge).expect("serializes");
589 assert!(
590 !json.contains("valid_from") && !json.contains("valid_to"),
591 "an unset window must be omitted so a pre-bitemporal edge is byte-identical: {json}"
592 );
593 }
594
595 #[test]
596 fn given_a_node_without_a_source_when_serialized_then_should_omit_it() {
597 let node = GraphNode::entity("Person", "Alice");
598 let json = serde_json::to_string(&node).expect("serializes");
599 assert!(
600 !json.contains("source"),
601 "an unknown source must be omitted so a pre-provenance node is byte-identical: {json}"
602 );
603 }
604
605 #[test]
606 fn given_a_node_with_a_source_when_round_tripped_then_should_preserve_it_and_keep_identity() {
607 let mut node = GraphNode::entity("Component", "cache");
608 node.source = Some(SourceRef::Message {
609 generation: None,
610 stream: 7,
611 topic: 2,
612 partition: 3,
613 offset: 4096,
614 conversation: None,
615 });
616 let bytes = encode_named(&node).expect("serializes");
617 let back: GraphNode = decode_named(&bytes).expect("deserializes");
618 assert_eq!(back.source, node.source);
619 assert_eq!(
620 back.id,
621 GraphNode::entity("Component", "cache").id,
622 "source is not part of node identity"
623 );
624 }
625
626 #[test]
627 fn given_an_edge_with_a_source_when_round_tripped_then_should_preserve_it_and_keep_identity() {
628 let from = GraphNode::entity("A", "x");
629 let to = GraphNode::entity("B", "y");
630 let edge = GraphEdge::relate(&from, "rel", &to).with_source(SourceRef::Kv {
631 namespace: "ns".to_owned(),
632 key: "k".to_owned(),
633 });
634 let bytes = encode_named(&edge).expect("serializes");
635 let back: GraphEdge = decode_named(&bytes).expect("deserializes");
636 assert_eq!(back.source, edge.source);
637 assert_eq!(
638 back.id,
639 GraphEdge::relate(&from, "rel", &to).id,
640 "source is not part of edge identity"
641 );
642 }
643
644 #[test]
645 fn given_a_source_without_a_conversation_when_serialized_then_should_omit_it() {
646 let source = SourceRef::Message {
647 generation: None,
648 stream: 1,
649 topic: 1,
650 partition: 0,
651 offset: 0,
652 conversation: None,
653 };
654 let json = serde_json::to_string(&source).expect("serializes");
655 assert!(
656 !json.contains("conversation"),
657 "an unset conversation must be omitted so a pre-conversation source stays byte-identical: {json}"
658 );
659 }
660
661 #[test]
662 fn given_a_source_with_a_conversation_when_round_tripped_then_should_preserve_it() {
663 let mut node = GraphNode::entity("Ticket", "7");
664 node.source = Some(SourceRef::Message {
665 generation: None,
666 stream: 4,
667 topic: 6,
668 partition: 2,
669 offset: 99,
670 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
671 });
672 let bytes = encode_named(&node).expect("serializes");
673 let back: GraphNode = decode_named(&bytes).expect("deserializes");
674 assert_eq!(
675 back.source, node.source,
676 "the conversation survives the round trip"
677 );
678 assert_eq!(
679 back.id,
680 GraphNode::entity("Ticket", "7").id,
681 "the conversation is provenance, not identity"
682 );
683 }
684
685 #[test]
686 fn given_a_conversation_filter_on_a_traversal_when_round_tripped_then_should_preserve_it() {
687 let query = GraphQuery {
688 v: GRAPH_OP_VERSION,
689 graph: "knowledge".to_owned(),
690 start: GraphStart::Ids(vec![NodeId::from_u128(1)]),
691 traverse: Vec::new(),
692 node_filter: None,
693 edge_filter: None,
694 return_: GraphReturn::Nodes,
695 limit: 10,
696 fork: None,
697 consistency: Consistency::Eventual,
698 as_of: None,
699 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
700 stream: Some("acme".to_owned()),
701 };
702 let back: GraphQuery =
703 decode_named(&encode_named(&query).expect("serializes")).expect("deserializes");
704 assert_eq!(
705 back.conversation.as_deref(),
706 Some("01KWM3K3XEP3NP5TN850J17YBP")
707 );
708 assert_eq!(back.stream.as_deref(), Some("acme"));
709 let unfiltered = GraphQuery {
711 conversation: None,
712 stream: None,
713 ..query
714 };
715 let json = serde_json::to_string(&unfiltered).expect("serializes");
716 assert!(
717 !json.contains("conversation") && !json.contains("stream"),
718 "an unset filter is omitted: {json}"
719 );
720 }
721
722 #[test]
723 fn given_a_neighbors_read_with_and_without_a_stream_when_round_tripped_then_should_omit_it_only_when_unset()
724 {
725 let request = GraphNeighbors {
726 v: GRAPH_OP_VERSION,
727 graph: "knowledge".to_owned(),
728 node: NodeId::from_u128(1),
729 dir: EdgeDir::Out,
730 edge_type: None,
731 depth: 1,
732 limit: 10,
733 as_of: None,
734 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
735 stream: Some("acme".to_owned()),
736 };
737 let back: GraphNeighbors =
738 decode_named(&encode_named(&request).expect("serializes")).expect("deserializes");
739 assert_eq!(back.stream.as_deref(), Some("acme"));
740 let bare = GraphNeighbors {
741 conversation: None,
742 stream: None,
743 ..request
744 };
745 let bytes = encode_named(&bare).expect("serializes");
746 let back: GraphNeighbors = decode_named(&bytes).expect("deserializes");
747 assert_eq!(back.stream, None);
748 assert!(
749 !serde_json::to_string(&bare)
750 .expect("serializes")
751 .contains("stream"),
752 "an unset stream is omitted"
753 );
754 }
755
756 #[test]
757 fn given_a_max_element_reply_with_source_when_encoded_then_should_fit_one_frame() {
758 use crate::limits::{MAX_FRAME_BYTES, MAX_GRAPH_RESULT_ELEMENTS};
759 let source = SourceRef::Message {
760 generation: None,
761 stream: u32::MAX,
762 topic: u32::MAX,
763 partition: u32::MAX,
764 offset: u64::MAX,
765 conversation: Some("7ZZZZZZZZZZZZZZZZZZZZZZZZZ".to_owned()),
766 };
767 let half = (MAX_GRAPH_RESULT_ELEMENTS / 2) as u128;
768 let nodes = (0..half)
769 .map(|i| {
770 let mut node = GraphNode::entity("Component", format!("entity-{i}"));
771 node.source = Some(source.clone());
772 node
773 })
774 .collect();
775 let edges = (0..half)
776 .map(|i| GraphEdge {
777 producer: None,
778 id: EdgeId::from_u128(i),
779 from: NodeId::from_u128(i),
780 to: NodeId::from_u128(i + 1),
781 edge_type: "relates_to".to_owned(),
782 weight: 1.0,
783 attrs: Vec::new(),
784 valid_from: None,
785 valid_to: None,
786 source: Some(source.clone()),
787 })
788 .collect();
789 let reply = GraphReply::Ok(GraphResult {
790 nodes,
791 edges,
792 paths: Vec::new(),
793 });
794 let encoded = encode_named(&reply).expect("serializes");
795 assert!(
796 encoded.len() < MAX_FRAME_BYTES,
797 "a full {MAX_GRAPH_RESULT_ELEMENTS}-element reply with source is {} bytes, over the frame cap {MAX_FRAME_BYTES}",
798 encoded.len()
799 );
800 }
801}