1use std::collections::BTreeMap;
2
3use rmpv::Value;
4use unicode_normalization::UnicodeNormalization;
5
6use crate::error::{DejaDbError, Hash, Result};
7use crate::format::field_map::{
8 compact_content_ref_field, compact_embedding_ref_field, compact_field,
9 compact_related_to_field, compact_workflow_edge_field,
10};
11use crate::format::header::{content_address, MgHeader};
12#[allow(clippy::wildcard_imports)]
13use crate::types::*;
14
15fn encode_value_to_vec(value: &Value) -> Result<Vec<u8>> {
17 let mut buf = Vec::new();
18 rmpv::encode::write_value(&mut buf, value)
19 .map_err(|e| DejaDbError::Serialization(format!("msgpack encode error: {}", e)))?;
20 Ok(buf)
21}
22
23pub fn serialize_grain<G: Grain + 'static>(grain: &G) -> Result<(Vec<u8>, Hash)> {
26 let common = grain.common();
27 let created_at = common
28 .created_at
29 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
30
31 let mut map = BTreeMap::<String, Value>::new();
32
33 map.insert(
35 compact_field("type").to_string(),
36 Value::String(grain.grain_type().as_str().into()),
37 );
38
39 add_type_specific_fields(grain, &mut map);
41
42 add_common_fields(common, created_at, &mut map);
44
45 let msgpack_value = btree_to_msgpack_map(map);
47 let payload = encode_value_to_vec(&msgpack_value)?;
48
49 let mut header = MgHeader::new(grain.grain_type(), common.namespace.as_deref(), created_at);
51 if !common.content_refs.is_empty() {
52 header.set_has_content_refs(true);
53 }
54 if !common.embedding_refs.is_empty() {
55 header.set_has_embedding_refs(true);
56 }
57 if let Some(ref st) = common.source_type {
58 match st.as_str() {
59 "llm_generated" | "consolidated" | "inferred" | "a2a_recalled" => {
60 header.set_ai_generated(true);
61 }
62 _ => {}
63 }
64 }
65 header.set_sensitivity(detect_sensitivity(&common.tags));
66
67 let header_bytes = header.to_bytes();
69 let mut blob = Vec::with_capacity(9 + payload.len());
70 blob.extend_from_slice(&header_bytes);
71 blob.extend_from_slice(&payload);
72
73 if blob.len() > crate::format::deserialize::MAX_GRAIN_BYTES {
77 return Err(crate::error::DejaDbError::Format(format!(
78 "grain too large ({} bytes, maximum {})",
79 blob.len(),
80 crate::format::deserialize::MAX_GRAIN_BYTES
81 )));
82 }
83 crate::format::deserialize::guard_msgpack_shape(&payload)?;
84
85 let hash = content_address(&blob);
86 Ok((blob, hash))
87}
88
89#[cfg(feature = "signing")]
100pub fn serialize_grain_signed<G: crate::Grain + 'static>(
101 grain: &G,
102 signing_key: &crate::crypto::signing::GrainSigningKey,
103 org_context: &[u8],
104) -> crate::error::Result<(Vec<u8>, crate::error::Hash, Vec<u8>)> {
105 let (mut blob, _) = serialize_grain(grain)?;
107 blob[1] |= 0x01;
109 let hash = crate::format::header::content_address(&blob);
111 let signed = crate::crypto::signing::sign_grain(&blob, signing_key, org_context)?;
113 Ok((signed.cose_bytes, hash, blob))
114}
115
116fn detect_sensitivity(tags: &[String]) -> u8 {
118 for tag in tags {
119 if tag.starts_with("phi:") {
120 return 0x03; }
122 }
123 for tag in tags {
124 if tag.starts_with("pii:") || tag.starts_with("sec:") || tag.starts_with("legal:") {
125 return 0x02; }
127 }
128 for tag in tags {
129 if tag.starts_with("reg:") {
130 return 0x01; }
132 }
133 0x00 }
135
136fn nfc_string(s: &str) -> Value {
138 Value::String(s.nfc().collect::<String>().into())
139}
140
141fn btree_to_msgpack_map(map: BTreeMap<String, Value>) -> Value {
143 let pairs: Vec<(Value, Value)> = map
144 .into_iter()
145 .map(|(k, v)| (Value::String(k.into()), v))
146 .collect();
147 Value::Map(pairs)
148}
149
150fn json_to_msgpack(json: &serde_json::Value) -> Value {
152 match json {
153 serde_json::Value::Null => Value::Nil,
154 serde_json::Value::Bool(b) => Value::Boolean(*b),
155 serde_json::Value::Number(n) => {
156 if let Some(i) = n.as_i64() {
157 Value::Integer(i.into())
158 } else if let Some(f) = n.as_f64() {
159 Value::F64(f)
160 } else {
161 Value::Nil
162 }
163 }
164 serde_json::Value::String(s) => nfc_string(s),
165 serde_json::Value::Array(arr) => Value::Array(arr.iter().map(json_to_msgpack).collect()),
166 serde_json::Value::Object(obj) => {
167 let mut sorted: BTreeMap<String, Value> = BTreeMap::new();
168 for (k, v) in obj {
169 sorted.insert(k.clone(), json_to_msgpack(v));
170 }
171 btree_to_msgpack_map(sorted)
172 }
173 }
174}
175
176fn json_to_msgpack_with_key_compaction(json: &serde_json::Value) -> Value {
181 match json {
182 serde_json::Value::Object(obj) => {
183 let mut sorted: BTreeMap<String, Value> = BTreeMap::new();
184 for (k, v) in obj {
185 let compacted_key = compact_field(k).to_string();
186 sorted.insert(compacted_key, json_to_msgpack(v));
187 }
188 btree_to_msgpack_map(sorted)
189 }
190 other => json_to_msgpack(other),
192 }
193}
194
195fn add_type_specific_fields<G: Grain + 'static>(grain: &G, map: &mut BTreeMap<String, Value>) {
197 use std::any::Any;
198 let any = grain as &dyn Any;
199
200 if let Some(fact) = any.downcast_ref::<Fact>() {
201 map.insert(
202 compact_field("subject").to_string(),
203 nfc_string(&fact.subject),
204 );
205 map.insert(
206 compact_field("relation").to_string(),
207 nfc_string(&fact.relation),
208 );
209 map.insert(
211 compact_field("object").to_string(),
212 nfc_string(&fact.object),
213 );
214 } else if let Some(ev) = any.downcast_ref::<Event>() {
215 map.insert("content".to_string(), nfc_string(&ev.content));
216 if let Some(ref s) = ev.subject {
217 map.insert(compact_field("subject").to_string(), nfc_string(s));
218 }
219 if let Some(ref o) = ev.object {
220 map.insert(compact_field("object").to_string(), nfc_string(o));
221 }
222 if let Some(r) = ev.role {
223 map.insert(compact_field("role").to_string(), nfc_string(r.as_str()));
224 }
225 if let Some(ref sid) = ev.session_id {
226 map.insert(compact_field("session_id").to_string(), nfc_string(sid));
227 }
228 if let Some(ref pmid) = ev.parent_message_id {
229 map.insert(
230 compact_field("parent_message_id").to_string(),
231 nfc_string(pmid),
232 );
233 }
234 if let Some(ref blocks) = ev.content_blocks {
235 if let Ok(json) = serde_json::to_value(blocks) {
239 map.insert(
240 compact_field("content_blocks").to_string(),
241 json_to_msgpack(&json),
242 );
243 }
244 }
245 if let Some(ref m) = ev.model_id {
246 map.insert(compact_field("model_id").to_string(), nfc_string(m));
247 }
248 if let Some(ref sr) = ev.stop_reason {
249 map.insert(compact_field("stop_reason").to_string(), nfc_string(sr));
250 }
251 if let Some(ref tu) = ev.token_usage {
252 if let Ok(json) = serde_json::to_value(tu) {
253 map.insert(
254 compact_field("token_usage").to_string(),
255 json_to_msgpack(&json),
256 );
257 }
258 }
259 if let Some(ref rid) = ev.run_id {
260 map.insert(compact_field("run_id").to_string(), nfc_string(rid));
261 }
262 } else if let Some(st) = any.downcast_ref::<State>() {
263 map.insert(
264 compact_field("context").to_string(),
265 json_to_msgpack(&st.context_data),
266 );
267 } else if let Some(wf) = any.downcast_ref::<Workflow>() {
268 let nodes: Vec<Value> = wf.nodes.iter().map(|s| nfc_string(s)).collect();
270 map.insert("nodes".to_string(), Value::Array(nodes));
271 if !wf.edges.is_empty() {
273 let edges: Vec<Value> = wf
274 .edges
275 .iter()
276 .map(|e| {
277 let mut m = BTreeMap::new();
278 m.insert("src".to_string(), nfc_string(&e.src));
279 m.insert("dst".to_string(), nfc_string(&e.dst));
280 if let Some(ref c) = e.cond {
281 m.insert("cond".to_string(), nfc_string(c));
282 }
283 if let Some(mc) = e.max_cycles {
284 m.insert(
285 compact_workflow_edge_field("max_cycles").to_string(),
286 Value::Integer(mc.into()),
287 );
288 }
289 btree_to_msgpack_map(m)
290 })
291 .collect();
292 map.insert("edges".to_string(), Value::Array(edges));
293 }
294 if !wf.bindings.is_empty() {
296 let mut bind_map = BTreeMap::new();
297 for (k, v) in &wf.bindings {
298 bind_map.insert(k.clone(), nfc_string(v));
299 }
300 map.insert(
301 compact_field("bindings").to_string(),
302 btree_to_msgpack_map(bind_map),
303 );
304 }
305 if !wf.retries.is_empty() {
307 let mut retry_map = BTreeMap::new();
308 for (k, v) in &wf.retries {
309 retry_map.insert(k.clone(), Value::Integer((*v).into()));
310 }
311 map.insert("retries".to_string(), btree_to_msgpack_map(retry_map));
312 }
313 if let Some(ref trigger) = wf.trigger {
314 map.insert("trigger".to_string(), nfc_string(trigger));
315 }
316 } else if let Some(action) = any.downcast_ref::<Tool>() {
317 map.insert(
318 compact_field("tool_name").to_string(),
319 nfc_string(&action.tool_name),
320 );
321 if action.kind != crate::types::ToolKind::Execution {
324 map.insert(
325 compact_field("kind").to_string(),
326 nfc_string(action.kind.as_str()),
327 );
328 }
329 if let Some(ref inp) = action.input {
330 map.insert(compact_field("input").to_string(), json_to_msgpack(inp));
331 }
332 if let Some(ref cnt) = action.content {
333 map.insert(compact_field("tool_content").to_string(), nfc_string(cnt));
335 }
336 if let Some(is_err) = action.is_error {
337 map.insert(
338 compact_field("is_error").to_string(),
339 Value::Boolean(is_err),
340 );
341 }
342 if let Some(ref err) = action.error {
343 map.insert(compact_field("error").to_string(), nfc_string(err));
344 }
345 if let Some(dur) = action.duration_ms {
346 map.insert(
347 compact_field("duration_ms").to_string(),
348 Value::Integer(dur.into()),
349 );
350 }
351 if let Some(ref ptid) = action.parent_task_id {
352 map.insert(
353 compact_field("parent_task_id").to_string(),
354 nfc_string(ptid),
355 );
356 }
357 if let Some(ref tcid) = action.tool_call_id {
358 map.insert(compact_field("tool_call_id").to_string(), nfc_string(tcid));
359 }
360 if let Some(ref cbid) = action.call_batch_id {
361 map.insert(compact_field("call_batch_id").to_string(), nfc_string(cbid));
362 }
363 if let Some(ref tdesc) = action.tool_description {
365 map.insert(
366 compact_field("tool_description").to_string(),
367 nfc_string(tdesc),
368 );
369 }
370 if let Some(ref ischema) = action.input_schema {
371 map.insert(
372 compact_field("input_schema").to_string(),
373 json_to_msgpack(ischema),
374 );
375 }
376 if let Some(ref osch) = action.output_schema {
378 map.insert(
379 compact_field("output_schema").to_string(),
380 json_to_msgpack(osch),
381 );
382 }
383 if let Some(strict) = action.strict {
384 map.insert(compact_field("strict").to_string(), Value::Boolean(strict));
385 }
386 if let Some(am) = action.async_mode {
387 map.insert(compact_field("async_mode").to_string(), Value::Boolean(am));
388 }
389 if let Some(ref axu) = action.executor_uri {
390 map.insert(compact_field("executor_uri").to_string(), nfc_string(axu));
391 }
392 if let Some(ref lprm) = action.locked_params {
393 map.insert(
394 compact_field("locked_params").to_string(),
395 json_to_msgpack(lprm),
396 );
397 }
398 if let Some(ref exmp) = action.examples {
399 let arr: Vec<Value> = exmp.iter().map(json_to_msgpack).collect();
400 map.insert(compact_field("examples").to_string(), Value::Array(arr));
401 }
402 if let Some(ref anno) = action.annotations {
403 if let Ok(json) = serde_json::to_value(anno) {
404 map.insert(
405 compact_field("annotations").to_string(),
406 json_to_msgpack(&json),
407 );
408 }
409 }
410 if let Some(ref shsh) = action.spec_hash {
411 map.insert(compact_field("spec_hash").to_string(), nfc_string(shsh));
412 }
413 if let Some(ek) = action.executor_kind {
417 if ek != crate::types::executor_kind::ExecutorKind::Host {
418 map.insert(
419 compact_field("executor_kind").to_string(),
420 nfc_string(ek.as_str()),
421 );
422 }
423 }
424 if let Some(st) = action.status {
428 map.insert(compact_field("status").to_string(), nfc_string(st.as_str()));
429 }
430 if let Some(ref cid) = action.correlation_id {
431 map.insert(compact_field("correlation_id").to_string(), nfc_string(cid));
432 }
433 if let Some(exp) = action.expires_at_sec {
434 map.insert(
435 compact_field("expires_at_sec").to_string(),
436 Value::Integer(exp.into()),
437 );
438 }
439 if let Some(ref tdh) = action.transient_definition_hash {
440 map.insert(
441 compact_field("transient_definition_hash").to_string(),
442 Value::Binary(tdh.to_vec()),
443 );
444 }
445 if let Some(fc) = action.failure_cause {
446 map.insert(
447 compact_field("failure_cause").to_string(),
448 nfc_string(fc.as_str()),
449 );
450 }
451 if let Some(ref fd) = action.failure_detail {
452 map.insert(compact_field("failure_detail").to_string(), nfc_string(fd));
453 }
454 if let Some(aex) = action.actor_execution_environment {
455 map.insert(
456 compact_field("actor_execution_environment").to_string(),
457 nfc_string(aex.as_str()),
458 );
459 }
460 } else if let Some(obs) = any.downcast_ref::<Observation>() {
461 map.insert(
462 compact_field("observer_id").to_string(),
463 nfc_string(&obs.observer_id),
464 );
465 map.insert(
466 compact_field("observer_type").to_string(),
467 nfc_string(&obs.observer_type),
468 );
469 if let Some(ref s) = obs.subject {
470 map.insert(compact_field("subject").to_string(), nfc_string(s));
471 }
472 if let Some(ref o) = obs.object {
473 map.insert(compact_field("object").to_string(), nfc_string(o));
474 }
475 if let Some(ref m) = obs.observer_model {
476 map.insert(compact_field("observer_model").to_string(), nfc_string(m));
477 }
478 if let Some(ref mode) = obs.observation_mode {
479 map.insert(
480 compact_field("observation_mode").to_string(),
481 nfc_string(mode.as_str()),
482 );
483 }
484 if let Some(ref scope) = obs.observation_scope {
485 map.insert(
486 compact_field("observation_scope").to_string(),
487 nfc_string(scope.as_str()),
488 );
489 }
490 } else if let Some(goal) = any.downcast_ref::<Goal>() {
491 map.insert(
492 compact_field("description").to_string(),
493 nfc_string(&goal.description),
494 );
495 map.insert(
496 compact_field("goal_state").to_string(),
497 nfc_string(goal.goal_state.as_str()),
498 );
499 if let Some(ref s) = goal.subject {
500 map.insert(compact_field("subject").to_string(), nfc_string(s));
501 }
502 if let Some(ref o) = goal.object {
503 map.insert(compact_field("object").to_string(), nfc_string(o));
504 }
505 if let Some(ref p) = goal.priority {
506 map.insert(
507 compact_field("priority").to_string(),
508 nfc_string(p.as_str()),
509 );
510 }
511 if let Some(ref c) = goal.criteria {
512 map.insert(compact_field("criteria").to_string(), nfc_string(c));
513 }
514 if let Some(ref pgs) = goal.parent_goals {
515 let arr: Vec<Value> = pgs.iter().map(|s| nfc_string(s)).collect();
516 map.insert(compact_field("parent_goals").to_string(), Value::Array(arr));
517 }
518 if let Some(ref sr) = goal.state_reason {
519 map.insert(compact_field("state_reason").to_string(), nfc_string(sr));
520 }
521 if let Some(ref se) = goal.satisfaction_evidence {
522 map.insert(
523 compact_field("satisfaction_evidence").to_string(),
524 json_to_msgpack(se),
525 );
526 }
527 if let Some(p) = goal.progress {
528 map.insert(compact_field("progress").to_string(), Value::F64(p));
529 }
530 if let Some(ref dto) = goal.delegate_to {
531 map.insert(compact_field("delegate_to").to_string(), nfc_string(dto));
532 }
533 if let Some(ref dfo) = goal.delegate_from {
534 map.insert(compact_field("delegate_from").to_string(), nfc_string(dfo));
535 }
536 } else if let Some(reasoning) = any.downcast_ref::<Reasoning>() {
537 if !reasoning.premises.is_empty() {
538 let arr: Vec<Value> = reasoning.premises.iter().map(|s| nfc_string(s)).collect();
539 map.insert(compact_field("premises").to_string(), Value::Array(arr));
540 }
541 if let Some(ref c) = reasoning.conclusion {
542 map.insert(compact_field("conclusion").to_string(), nfc_string(c));
543 }
544 if let Some(ref m) = reasoning.inference_method {
545 map.insert(compact_field("inference_method").to_string(), nfc_string(m));
546 }
547 if !reasoning.alternatives_considered.is_empty() {
548 let arr: Vec<Value> = reasoning
549 .alternatives_considered
550 .iter()
551 .map(|s| nfc_string(s))
552 .collect();
553 map.insert(
554 compact_field("alternatives_considered").to_string(),
555 Value::Array(arr),
556 );
557 }
558 if let Some(ref t) = reasoning.thinking_content {
559 map.insert(compact_field("thinking_content").to_string(), nfc_string(t));
560 }
561 if let Some(tr) = reasoning.thinking_redacted {
562 map.insert(
563 compact_field("thinking_redacted").to_string(),
564 Value::Boolean(tr),
565 );
566 }
567 } else if let Some(consensus) = any.downcast_ref::<Consensus>() {
568 if !consensus.participating_observers.is_empty() {
569 let arr: Vec<Value> = consensus
570 .participating_observers
571 .iter()
572 .map(|s| nfc_string(s))
573 .collect();
574 map.insert(
575 compact_field("participating_observers").to_string(),
576 Value::Array(arr),
577 );
578 }
579 if let Some(t) = consensus.threshold {
580 map.insert(compact_field("threshold").to_string(), Value::F64(t));
581 }
582 if let Some(ac) = consensus.agreement_count {
583 map.insert(
584 compact_field("agreement_count").to_string(),
585 Value::Integer(ac.into()),
586 );
587 }
588 if let Some(dc) = consensus.dissent_count {
589 map.insert(
590 compact_field("dissent_count").to_string(),
591 Value::Integer(dc.into()),
592 );
593 }
594 if !consensus.dissent_grains.is_empty() {
595 let arr: Vec<Value> = consensus
596 .dissent_grains
597 .iter()
598 .map(|s| nfc_string(s))
599 .collect();
600 map.insert(
601 compact_field("dissent_grains").to_string(),
602 Value::Array(arr),
603 );
604 }
605 if let Some(ref ac) = consensus.agreed_content {
606 map.insert(compact_field("agreed_content").to_string(), nfc_string(ac));
607 }
608 } else if let Some(consent) = any.downcast_ref::<Consent>() {
609 map.insert(
610 compact_field("subject_did").to_string(),
611 nfc_string(&consent.subject_did),
612 );
613 if let Some(ref g) = consent.grantee_did {
614 map.insert(compact_field("grantee_did").to_string(), nfc_string(g));
615 }
616 if let Some(ref s) = consent.scope {
617 map.insert(compact_field("scope").to_string(), nfc_string(s));
618 }
619 if let Some(iw) = consent.is_withdrawal {
620 map.insert(
621 compact_field("is_withdrawal").to_string(),
622 Value::Boolean(iw),
623 );
624 }
625 if let Some(ref b) = consent.basis {
626 map.insert(compact_field("basis").to_string(), nfc_string(b));
627 }
628 if let Some(ref j) = consent.jurisdiction {
629 map.insert(compact_field("jurisdiction").to_string(), nfc_string(j));
630 }
631 if let Some(ref pc) = consent.prior_consent {
632 map.insert(compact_field("prior_consent").to_string(), nfc_string(pc));
633 }
634 if !consent.witness_dids.is_empty() {
635 let arr: Vec<Value> = consent.witness_dids.iter().map(|s| nfc_string(s)).collect();
636 map.insert(compact_field("witness_dids").to_string(), Value::Array(arr));
637 }
638 } else if let Some(skill) = any.downcast_ref::<Skill>() {
639 map.insert(compact_field("name").to_string(), nfc_string(&skill.name));
646 map.insert(
648 compact_field("description").to_string(),
649 nfc_string(&skill.description),
650 );
651 if let Some(ref instr) = skill.instructions {
652 map.insert(compact_field("instructions").to_string(), nfc_string(instr));
653 }
654 if let Some(ref wtu) = skill.when_to_use {
655 map.insert(compact_field("when_to_use").to_string(), nfc_string(wtu));
656 }
657 if let Some(ref sver) = skill.version {
658 map.insert(compact_field("version").to_string(), nfc_string(sver));
659 }
660 if !skill.allowed_tools.is_empty() {
661 let arr: Vec<Value> = skill.allowed_tools.iter().map(|s| nfc_string(s)).collect();
662 map.insert(
663 compact_field("allowed_tools").to_string(),
664 Value::Array(arr),
665 );
666 }
667 if !skill.resources.is_empty() {
668 let arr: Vec<Value> = skill.resources.iter().map(|s| nfc_string(s)).collect();
669 map.insert(compact_field("resources").to_string(), Value::Array(arr));
670 }
671 if !skill.dependencies.is_empty() {
672 let arr: Vec<Value> = skill.dependencies.iter().map(|s| nfc_string(s)).collect();
673 map.insert(compact_field("dependencies").to_string(), Value::Array(arr));
674 }
675 if !skill.input_modalities.is_empty() {
676 let arr: Vec<Value> = skill
677 .input_modalities
678 .iter()
679 .map(|s| nfc_string(s))
680 .collect();
681 map.insert(
682 compact_field("input_modalities").to_string(),
683 Value::Array(arr),
684 );
685 }
686 if !skill.output_modalities.is_empty() {
687 let arr: Vec<Value> = skill
688 .output_modalities
689 .iter()
690 .map(|s| nfc_string(s))
691 .collect();
692 map.insert(
693 compact_field("output_modalities").to_string(),
694 Value::Array(arr),
695 );
696 }
697 if let Some(ref dom) = skill.domain {
698 map.insert(compact_field("domain").to_string(), nfc_string(dom));
699 }
700 if let Some(ref hdid) = skill.holder_did {
701 map.insert(compact_field("holder_did").to_string(), nfc_string(hdid));
702 }
703 if skill.is_held() {
705 map.insert(
706 compact_field("proficiency").to_string(),
707 Value::F64(skill.common.confidence),
708 );
709 }
710 if let Some(prcnt) = skill.practice_count {
711 map.insert(
712 compact_field("practice_count").to_string(),
713 Value::Integer(prcnt.into()),
714 );
715 }
716 if let Some(lpa) = skill.last_practiced_at {
717 map.insert(
718 compact_field("last_practiced_at").to_string(),
719 Value::Integer(lpa.into()),
720 );
721 }
722 if !skill.strategies.is_empty() {
723 if let Ok(json) = serde_json::to_value(&skill.strategies) {
727 map.insert(
728 compact_field("strategies").to_string(),
729 json_to_msgpack(&json),
730 );
731 }
732 }
733 if let Some(xfer) = skill.transferable {
734 map.insert(
735 compact_field("transferable").to_string(),
736 Value::Boolean(xfer),
737 );
738 }
739 }
740}
741
742fn add_common_fields(common: &GrainCommon, created_at: i64, map: &mut BTreeMap<String, Value>) {
744 if let Some(ref adid) = common.author_did {
745 map.insert(compact_field("author_did").to_string(), nfc_string(adid));
746 }
747 map.insert(
748 compact_field("confidence").to_string(),
749 Value::F64(common.confidence),
750 );
751 map.insert(
752 compact_field("created_at").to_string(),
753 Value::Integer(created_at.into()),
754 );
755
756 if let Some(ref ns) = common.namespace {
757 map.insert(compact_field("namespace").to_string(), nfc_string(ns));
758 }
759 if let Some(ref uid) = common.user_id {
760 map.insert(compact_field("user_id").to_string(), nfc_string(uid));
761 }
762 if !common.tags.is_empty() {
763 let arr: Vec<Value> = common.tags.iter().map(|s| nfc_string(s)).collect();
764 map.insert(
765 compact_field("structural_tags").to_string(),
766 Value::Array(arr),
767 );
768 }
769 if let Some(ref st) = common.source_type {
770 map.insert(compact_field("source_type").to_string(), nfc_string(st));
771 }
772 if let Some(im) = common.importance {
773 map.insert(compact_field("importance").to_string(), Value::F64(im));
774 }
775 if let Some(ref tt) = common.temporal_type {
776 let s = match tt {
777 TemporalType::State => "state",
778 TemporalType::Event => "event",
779 TemporalType::Interval => "interval",
780 };
781 map.insert(compact_field("temporal_type").to_string(), nfc_string(s));
782 }
783 if let Some(vf) = common.valid_from {
784 map.insert(
785 compact_field("valid_from").to_string(),
786 Value::Integer(vf.into()),
787 );
788 }
789 if let Some(vt) = common.valid_to {
790 map.insert(
791 compact_field("valid_to").to_string(),
792 Value::Integer(vt.into()),
793 );
794 }
795 if let Some(svf) = common.system_valid_from {
796 map.insert(
797 compact_field("system_valid_from").to_string(),
798 Value::Integer(svf.into()),
799 );
800 }
801 if let Some(svt) = common.system_valid_to {
802 map.insert(
803 compact_field("system_valid_to").to_string(),
804 Value::Integer(svt.into()),
805 );
806 }
807 if let Some(ref df) = common.derived_from {
808 map.insert(compact_field("derived_from").to_string(), nfc_string(df));
809 }
810 if let Some(cl) = common.consolidation_level {
811 map.insert(
812 compact_field("consolidation_level").to_string(),
813 Value::Integer(cl.into()),
814 );
815 }
816 if let Some(ref odid) = common.origin_did {
817 map.insert(compact_field("origin_did").to_string(), nfc_string(odid));
818 }
819 if let Some(ref ons) = common.origin_namespace {
820 map.insert(
821 compact_field("origin_namespace").to_string(),
822 nfc_string(ons),
823 );
824 }
825 if let Some(ref sb) = common.superseded_by {
826 map.insert(compact_field("superseded_by").to_string(), nfc_string(sb));
827 }
828 if let Some(ref vs) = common.verification_status {
829 map.insert(
830 compact_field("verification_status").to_string(),
831 nfc_string(vs),
832 );
833 }
834
835 if let Some(ref et) = common.embedding_text {
837 map.insert(compact_field("embedding_text").to_string(), nfc_string(et));
838 }
839
840 if let Some(ref ctx) = common.context {
842 map.insert(
843 compact_field("context").to_string(),
844 json_to_msgpack_with_key_compaction(ctx),
845 );
846 }
847
848 if let Some(ref ip) = common.invalidation_policy {
850 let mut ip_map = BTreeMap::new();
851 if let Some(ref auth) = ip.authorized {
852 let arr: Vec<Value> = auth.iter().map(|s| nfc_string(s)).collect();
853 ip_map.insert("authorized".to_string(), Value::Array(arr));
854 }
855 if let Some(ref fb) = ip.fallback_mode {
856 ip_map.insert("fallback_mode".to_string(), nfc_string(fb));
857 }
858 if let Some(locked_until) = ip.locked_until {
859 ip_map.insert(
860 "locked_until".to_string(),
861 Value::Integer(locked_until.into()),
862 );
863 }
864 ip_map.insert("mode".to_string(), nfc_string(&ip.mode));
865 if let Some(ref reason) = ip.protection_reason {
866 ip_map.insert("protection_reason".to_string(), nfc_string(reason));
867 }
868 if let Some(ref scope) = ip.scope {
869 ip_map.insert("scope".to_string(), nfc_string(scope));
870 }
871 if let Some(threshold) = ip.threshold {
872 ip_map.insert("threshold".to_string(), Value::Integer(threshold.into()));
873 }
874 map.insert(
875 compact_field("invalidation_policy").to_string(),
876 btree_to_msgpack_map(ip_map),
877 );
878 }
879
880 if !common.content_refs.is_empty() {
882 let arr: Vec<Value> = common
883 .content_refs
884 .iter()
885 .map(|cr| {
886 let mut m = BTreeMap::new();
887 if let Some(ref ck) = cr.checksum {
888 m.insert(
889 compact_content_ref_field("checksum").to_string(),
890 nfc_string(ck),
891 );
892 }
893 if let Some(ref md) = cr.metadata {
894 m.insert(
895 compact_content_ref_field("metadata").to_string(),
896 json_to_msgpack(md),
897 );
898 }
899 if let Some(ref mt) = cr.mime_type {
900 m.insert(
901 compact_content_ref_field("mime_type").to_string(),
902 nfc_string(mt),
903 );
904 }
905 if let Some(ref mod_) = cr.modality {
906 m.insert(
907 compact_content_ref_field("modality").to_string(),
908 nfc_string(mod_),
909 );
910 }
911 if let Some(sz) = cr.size_bytes {
912 m.insert(
913 compact_content_ref_field("size_bytes").to_string(),
914 Value::Integer(sz.into()),
915 );
916 }
917 m.insert(
918 compact_content_ref_field("uri").to_string(),
919 nfc_string(&cr.uri),
920 );
921 btree_to_msgpack_map(m)
922 })
923 .collect();
924 map.insert(compact_field("content_refs").to_string(), Value::Array(arr));
925 }
926
927 if !common.embedding_refs.is_empty() {
929 let arr: Vec<Value> = common
930 .embedding_refs
931 .iter()
932 .map(|er| {
933 let mut m = BTreeMap::new();
934 if let Some(ref di) = er.distance_metric {
935 m.insert(
936 compact_embedding_ref_field("distance_metric").to_string(),
937 nfc_string(di),
938 );
939 }
940 if let Some(dm) = er.dimensions {
941 m.insert(
942 compact_embedding_ref_field("dimensions").to_string(),
943 Value::Integer(dm.into()),
944 );
945 }
946 if let Some(ref mo) = er.model {
947 m.insert(
948 compact_embedding_ref_field("model").to_string(),
949 nfc_string(mo),
950 );
951 }
952 if let Some(ref ms) = er.modality_source {
953 m.insert(
954 compact_embedding_ref_field("modality_source").to_string(),
955 nfc_string(ms),
956 );
957 }
958 m.insert(
959 compact_embedding_ref_field("vector_id").to_string(),
960 nfc_string(&er.vector_id),
961 );
962 btree_to_msgpack_map(m)
963 })
964 .collect();
965 map.insert(
966 compact_field("embedding_refs").to_string(),
967 Value::Array(arr),
968 );
969 }
970
971 if !common.related_to.is_empty() {
973 let arr: Vec<Value> = common
974 .related_to
975 .iter()
976 .map(|rt| {
977 let mut m = BTreeMap::new();
978 m.insert(
979 compact_related_to_field("hash").to_string(),
980 nfc_string(&rt.hash),
981 );
982 m.insert(
983 compact_related_to_field("relation_type").to_string(),
984 nfc_string(&rt.relation_type),
985 );
986 if let Some(w) = rt.weight {
987 m.insert(
988 compact_related_to_field("weight").to_string(),
989 Value::F64(w),
990 );
991 }
992 btree_to_msgpack_map(m)
993 })
994 .collect();
995 map.insert(compact_field("related_to").to_string(), Value::Array(arr));
996 }
997
998 for (key, value) in &common.extra_fields {
1001 if map.contains_key(key) || map.contains_key(&compact_field(key).to_string()) {
1003 continue;
1004 }
1005 map.insert(key.clone(), json_to_msgpack(value));
1006 }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012
1013 #[test]
1014 fn test_serialize_minimal_fact() {
1015 let fact = Fact::new("user", "likes", "coffee")
1017 .confidence(0.9)
1018 .source_type("user_explicit")
1019 .created_at(1768471200000_i64)
1020 .namespace("shared")
1021 .author_did("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
1022
1023 let (blob, hash) = serialize_grain(&fact).unwrap();
1024
1025 assert!(!hash.to_hex().is_empty());
1027 assert_eq!(blob[0], 0x01); assert_eq!(blob[2], 0x01); }
1030
1031 #[test]
1032 fn test_serialize_protected_fact() {
1033 let mut fact = Fact::new(
1035 "agent-007",
1036 "constraint",
1037 "never delete user files without confirmation",
1038 )
1039 .confidence(1.0)
1040 .source_type("user_explicit")
1041 .created_at(1768471200000_i64)
1042 .namespace("safety");
1043
1044 fact.common.invalidation_policy = Some(InvalidationPolicy {
1045 mode: "locked".to_string(),
1046 authorized: Some(vec![
1047 "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(),
1048 ]),
1049 threshold: None,
1050 locked_until: None,
1051 fallback_mode: None,
1052 scope: None,
1053 protection_reason: None,
1054 });
1055
1056 let (blob, _hash) = serialize_grain(&fact).unwrap();
1057 assert!(!blob.is_empty());
1058 assert_eq!(blob[2], 0x01); }
1060
1061 #[test]
1062 fn test_serialize_tool_with_output_schema() {
1063 use crate::format::deserialize::deserialize_blob;
1064
1065 let schema = serde_json::json!({
1066 "type": "object",
1067 "properties": {
1068 "id": { "type": "integer" },
1069 "number": { "type": "integer" },
1070 "html_url": { "type": "string" }
1071 }
1072 });
1073
1074 let tool = Tool::new("github:create-issue")
1075 .output_schema(schema)
1076 .created_at(1768471200000_i64)
1077 .namespace("example:connectors:github");
1078
1079 let (blob, _hash) = serialize_grain(&tool).unwrap();
1080 assert_eq!(blob[2], 0x05); let deserialized = deserialize_blob(&blob).unwrap();
1084 assert_eq!(deserialized.grain_type, GrainType::Tool);
1085 assert_eq!(
1086 deserialized.get_str("tool_name"),
1087 Some("github:create-issue")
1088 );
1089 let osch = deserialized.fields.get("output_schema");
1090 assert!(
1091 osch.is_some(),
1092 "output_schema should be present after deserialization"
1093 );
1094 let osch_obj = osch.unwrap().as_object().unwrap();
1095 assert_eq!(osch_obj.get("type").unwrap(), "object");
1096 }
1097
1098 #[test]
1099 fn test_serialize_tool_without_output_schema() {
1100 let tool = Tool::new("calculator")
1102 .content("42")
1103 .created_at(1768471200000_i64);
1104
1105 let (blob, _hash) = serialize_grain(&tool).unwrap();
1106 let blob_str = String::from_utf8_lossy(&blob);
1108 assert!(
1109 !blob_str.contains("osch"),
1110 "osch should be omitted when output_schema is None"
1111 );
1112 }
1113
1114 #[test]
1115 fn test_serialize_context_with_integration_keys() {
1116 use crate::format::deserialize::deserialize_blob;
1117
1118 let mut tool = Tool::new("github:create-issue")
1119 .created_at(1768471200000_i64)
1120 .namespace("example:connectors:github");
1121
1122 tool.common.context = Some(serde_json::json!({
1124 "int:base_url": "https://api.github.com",
1125 "int:http_method": "POST",
1126 "int:http_path": "/repos/{owner}/{repo}/issues",
1127 "int:connector": "github",
1128 "int:auth_type": "api_key:bearer"
1129 }));
1130
1131 let (blob, _hash) = serialize_grain(&tool).unwrap();
1132 let deserialized = deserialize_blob(&blob).unwrap();
1133
1134 let ctx = deserialized
1136 .fields
1137 .get("context")
1138 .unwrap()
1139 .as_object()
1140 .unwrap();
1141 assert_eq!(ctx.get("int:base_url").unwrap(), "https://api.github.com");
1142 assert_eq!(ctx.get("int:http_method").unwrap(), "POST");
1143 assert_eq!(
1144 ctx.get("int:http_path").unwrap(),
1145 "/repos/{owner}/{repo}/issues"
1146 );
1147 assert_eq!(ctx.get("int:connector").unwrap(), "github");
1148 assert_eq!(ctx.get("int:auth_type").unwrap(), "api_key:bearer");
1149 }
1150
1151 #[test]
1152 fn test_serialize_event() {
1153 let ev = Event::new("User discussed vacation plans for March")
1155 .namespace("conversations")
1156 .created_at(1768471200000_i64);
1157
1158 let (blob, _hash) = serialize_grain(&ev).unwrap();
1159 assert!(!blob.is_empty());
1160 assert_eq!(blob[0], 0x01); assert_eq!(blob[2], 0x02); }
1163
1164 #[test]
1165 fn test_extra_fields_round_trip() {
1166 use crate::format::deserialize::deserialize_blob;
1167
1168 let mut goal = Goal::new("complete benchmark run")
1170 .confidence(0.95)
1171 .namespace("meta-agent")
1172 .created_at(1768471200000_i64);
1173
1174 goal.common_mut().extra_fields.insert(
1175 "output".to_string(),
1176 serde_json::json!("benchmark passed with 97% recall"),
1177 );
1178 goal.common_mut()
1179 .extra_fields
1180 .insert("total_tokens".to_string(), serde_json::json!(42500));
1181 goal.common_mut()
1182 .extra_fields
1183 .insert("steps_executed".to_string(), serde_json::json!(12));
1184 goal.common_mut()
1185 .extra_fields
1186 .insert("cost_micro_credits".to_string(), serde_json::json!(1500));
1187 goal.common_mut().extra_fields.insert(
1189 "metrics".to_string(),
1190 serde_json::json!({"recall": 0.97, "precision": 0.92, "f1": 0.945}),
1191 );
1192
1193 let (blob, _hash) = serialize_grain(&goal).unwrap();
1194
1195 let deserialized = deserialize_blob(&blob).unwrap();
1197 assert_eq!(deserialized.grain_type, GrainType::Goal);
1198
1199 assert_eq!(
1201 deserialized.get_str("description"),
1202 Some("complete benchmark run")
1203 );
1204 assert_eq!(
1205 deserialized
1206 .fields
1207 .get("confidence")
1208 .and_then(|v| v.as_f64()),
1209 Some(0.95)
1210 );
1211
1212 assert_eq!(
1214 deserialized.fields.get("output").and_then(|v| v.as_str()),
1215 Some("benchmark passed with 97% recall"),
1216 );
1217 assert_eq!(
1218 deserialized
1219 .fields
1220 .get("total_tokens")
1221 .and_then(|v| v.as_i64()),
1222 Some(42500),
1223 );
1224 assert_eq!(
1225 deserialized
1226 .fields
1227 .get("steps_executed")
1228 .and_then(|v| v.as_i64()),
1229 Some(12),
1230 );
1231 assert_eq!(
1232 deserialized
1233 .fields
1234 .get("cost_micro_credits")
1235 .and_then(|v| v.as_i64()),
1236 Some(1500),
1237 );
1238
1239 let metrics = deserialized
1241 .fields
1242 .get("metrics")
1243 .unwrap()
1244 .as_object()
1245 .unwrap();
1246 assert_eq!(metrics.get("recall").and_then(|v| v.as_f64()), Some(0.97));
1247 assert_eq!(
1248 metrics.get("precision").and_then(|v| v.as_f64()),
1249 Some(0.92)
1250 );
1251 }
1252
1253 #[test]
1254 fn test_extra_fields_empty_no_overhead() {
1255 use crate::format::deserialize::deserialize_blob;
1256
1257 let fact = Fact::new("user", "likes", "rust")
1259 .confidence(0.9)
1260 .created_at(1768471200000_i64);
1261
1262 assert!(fact.common().extra_fields.is_empty());
1263
1264 let (blob, _) = serialize_grain(&fact).unwrap();
1265 let deserialized = deserialize_blob(&blob).unwrap();
1266
1267 let known_keys: std::collections::HashSet<&str> = [
1269 "type",
1270 "subject",
1271 "relation",
1272 "object",
1273 "confidence",
1274 "created_at",
1275 ]
1276 .into_iter()
1277 .collect();
1278 for key in deserialized.fields.keys() {
1279 assert!(
1280 known_keys.contains(key.as_str()),
1281 "unexpected field in deserialized output: {key}"
1282 );
1283 }
1284 }
1285
1286 #[test]
1287 fn test_extra_field_builder() {
1288 use crate::format::deserialize::deserialize_blob;
1289
1290 let event = Event::new("agent completed task")
1292 .namespace("tasks")
1293 .created_at(1768471200000_i64)
1294 .extra_field("task_id", serde_json::json!("task-42"))
1295 .extra_field("duration_sec", serde_json::json!(3.15));
1296
1297 let (blob, _) = serialize_grain(&event).unwrap();
1298 let deserialized = deserialize_blob(&blob).unwrap();
1299
1300 assert_eq!(
1301 deserialized.fields.get("task_id").and_then(|v| v.as_str()),
1302 Some("task-42"),
1303 );
1304 assert_eq!(
1305 deserialized
1306 .fields
1307 .get("duration_sec")
1308 .and_then(|v| v.as_f64()),
1309 Some(3.15),
1310 );
1311 }
1312
1313 #[test]
1314 fn test_event_chat_fields_roundtrip() {
1315 use crate::format::deserialize::deserialize_blob;
1316
1317 let blocks = vec![
1318 ContentBlock::Text {
1319 text: "let me check".into(),
1320 },
1321 ContentBlock::ToolUse {
1322 id: "toolu_01".into(),
1323 name: "calculator".into(),
1324 input: serde_json::json!({"x": 2, "y": 3}),
1325 },
1326 ];
1327 let tu = TokenUsage {
1328 input_tokens: 42,
1329 output_tokens: 17,
1330 cache_read_tokens: None,
1331 cache_creation_tokens: None,
1332 };
1333 let ev = Event::new("hello from assistant")
1334 .role(Role::Assistant)
1335 .session("conv-abc".into())
1336 .parent_message("deadbeef".into())
1337 .content_blocks(blocks.clone())
1338 .model("claude-opus-4.7".into())
1339 .stop_reason("end_turn".into())
1340 .token_usage(tu)
1341 .run_id("run-99".into());
1342 let mut ev = ev;
1343 ev.common.namespace = Some("harness:slug:conv-abc".into());
1344 ev.common.user_id = Some("alice".into());
1345 ev.common.created_at = Some(1_768_471_200_000);
1346
1347 let (blob, _) = serialize_grain(&ev).unwrap();
1348 let des = deserialize_blob(&blob).unwrap();
1349 let back = des.to_event().unwrap();
1350
1351 assert_eq!(back.content, "hello from assistant");
1352 assert_eq!(back.role, Some(Role::Assistant));
1353 assert_eq!(back.session_id.as_deref(), Some("conv-abc"));
1354 assert_eq!(back.parent_message_id.as_deref(), Some("deadbeef"));
1355 assert_eq!(back.model_id.as_deref(), Some("claude-opus-4.7"));
1356 assert_eq!(back.stop_reason.as_deref(), Some("end_turn"));
1357 assert_eq!(back.run_id.as_deref(), Some("run-99"));
1358 assert_eq!(back.token_usage, Some(tu));
1359 assert_eq!(back.content_blocks.as_ref().unwrap(), &blocks);
1360 }
1361
1362 #[test]
1363 fn test_event_compact_keys_present_in_blob() {
1364 let ev = Event::new("hi")
1365 .role(Role::User)
1366 .session("conv-x".into())
1367 .parent_message("abc".into())
1368 .model("gpt-5".into())
1369 .stop_reason("end_turn".into())
1370 .token_usage(TokenUsage::default());
1371
1372 let (blob, _) = serialize_grain(&ev).unwrap();
1373 let payload = &blob[9..];
1375 assert!(has_str(payload, "role"), "role key missing");
1377 assert!(has_str(payload, "sid2"), "sid2 key missing");
1378 assert!(has_str(payload, "pmid"), "pmid key missing");
1379 assert!(has_str(payload, "mdl"), "mdl key missing");
1380 assert!(has_str(payload, "stopr"), "stopr key missing");
1381 assert!(has_str(payload, "toku"), "toku key missing");
1382 }
1383
1384 #[test]
1385 fn test_event_no_new_fields_legacy_shape_deserializes() {
1386 use crate::format::deserialize::deserialize_blob;
1387
1388 let ev = Event::new("legacy").subject("alice");
1389 let mut ev = ev;
1390 ev.common.namespace = Some("ns".into());
1391 ev.common.user_id = Some("u1".into());
1392 ev.common.created_at = Some(1_768_471_200_000);
1393
1394 let (blob, _) = serialize_grain(&ev).unwrap();
1395 let des = deserialize_blob(&blob).unwrap();
1396 let back = des.to_event().unwrap();
1397
1398 assert_eq!(back.content, "legacy");
1399 assert_eq!(back.subject.as_deref(), Some("alice"));
1400 assert!(back.role.is_none());
1401 assert!(back.session_id.is_none());
1402 assert!(back.content_blocks.is_none());
1403 assert!(back.token_usage.is_none());
1404 }
1405
1406 fn has_str(payload: &[u8], needle: &str) -> bool {
1407 payload
1408 .windows(needle.len())
1409 .any(|w| w == needle.as_bytes())
1410 }
1411}