rightkit_agent_engine_protocol/references.rs
1use serde::{Deserialize, Serialize};
2
3/// Persisted (metadata-only) reference attached to a user turn. Mirrors the
4/// TypeScript `ReferenceMeta` shape used by the desktop composer. **No image
5/// bytes live here** — the desktop keeps the bytes in-memory only; this
6/// record is the transcript-visible card (filename, size, kind, trust) that
7/// the UI persists under `cr.references.v1` and re-attaches on restart.
8///
9/// Wire compatibility: all fields use `#[serde(default)]` and
10/// `skip_serializing_if` so legacy rows (pre-references) deserialize cleanly.
11/// ReferenceMeta is exposed as a standalone type — the desktop owns the
12/// transcript mutation surface in the user-message variant, and Agent 4's
13/// integration pass binds this type into the `PostMessage` body when the
14/// composer forwards references to the engine.
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "camelCase")]
17pub struct ReferenceMeta {
18 /// Stable client-generated id (uuid v4 on the desktop). Reused by the
19 /// transcript's chip strip and the inline resolver; never recast on
20 /// restart so the chip identity survives a page reload.
21 pub id: String,
22 /// `"external_link" | "workspace_file" | "generated_document" | "image"
23 /// | "artifact" | "change_set"`. Free-form on the wire so a future
24 /// kind can be introduced without breaking older clients; the desktop
25 /// renders unknown kinds as a generic chip.
26 pub kind: String,
27 /// Display target: URL, workspace-relative path, doc id, artifact id, or
28 /// change-set id. Used by `openExternal` / `revealInFolder` / file
29 /// resolver.
30 pub destination: String,
31 /// Optional MIME hint — `"image/png"`, `"text/markdown"`,
32 /// `"application/octet-stream"`, etc.
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub mime: Option<String>,
35 /// Display filename shown on the chip when `kind` is `image`,
36 /// `workspace_file`, or `generated_document`. Optional for
37 /// `external_link` and `change_set` where the destination is the label.
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub filename: Option<String>,
40 /// Best-effort byte count for the original artifact. `None` for
41 /// `external_link` and `change_set` where the size is not knowable up
42 /// front.
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub size_bytes: Option<i64>,
45 /// Provenance + trust. `source` is the producer string
46 /// (`"composer.paste"`, `"workspace_resolver"`, etc.), `createdAt` is the
47 /// ISO timestamp the user added the reference, `trust` is the
48 /// `ContentTrust` label, `contentHash` (when known) is the SHA-256 of the
49 /// original bytes — present for `image` / `workspace_file` /
50 /// `generated_document` once the desktop has read the file.
51 pub provenance: ReferenceProvenance,
52 /// Action affordances the chip exposes. Free-form on the wire; the
53 /// desktop renders the intersection with its known action set so a
54 /// future `download` action sent from a newer sender doesn't break an
55 /// older renderer.
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub actions: Vec<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61#[serde(rename_all = "camelCase")]
62pub struct ReferenceProvenance {
63 pub source: String,
64 pub created_at: String,
65 pub trust: String,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub content_hash: Option<String>,
68}
69
70impl ReferenceMeta {
71 /// Wire-friendly default: an empty record with no actions / no MIME / no
72 /// size. Useful for tests that need a placeholder.
73 pub fn empty(id: &str, kind: &str, destination: &str) -> Self {
74 Self {
75 id: id.to_string(),
76 kind: kind.to_string(),
77 destination: destination.to_string(),
78 mime: None,
79 filename: None,
80 size_bytes: None,
81 provenance: ReferenceProvenance {
82 source: "composer".to_string(),
83 created_at: "1970-01-01T00:00:00Z".to_string(),
84 trust: "user".to_string(),
85 content_hash: None,
86 },
87 actions: Vec::new(),
88 }
89 }
90}