use serde::Serialize;
use serde_json::value::RawValue;
use super::files::TranscriptFile;
pub const INGEST_PATH: &str = "/v1/ingest/transcript";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TranscriptSession {
pub harness_id: String,
pub harness_session_id: String,
pub harness_version: Option<String>,
pub cwd: Option<String>,
pub org_id: String,
pub auth_subject: String,
}
impl TranscriptSession {
pub fn new(harness_id: impl Into<String>, harness_session_id: impl Into<String>) -> Self {
Self {
harness_id: harness_id.into(),
harness_session_id: harness_session_id.into(),
harness_version: None,
cwd: None,
org_id: String::new(),
auth_subject: String::new(),
}
}
#[must_use]
pub fn with_harness_version(mut self, version: Option<String>) -> Self {
self.harness_version = version;
self
}
#[must_use]
pub fn with_cwd(mut self, cwd: Option<String>) -> Self {
self.cwd = cwd;
self
}
#[must_use]
pub fn with_auth_subject(mut self, auth_subject: impl Into<String>) -> Self {
self.auth_subject = auth_subject.into();
self
}
}
#[derive(Debug, Serialize)]
pub struct IngestEnvelope<'a> {
pub org_id: &'a str,
pub auth_subject: &'a str,
pub harness_id: &'a str,
pub harness_session_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness_version: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<&'a str>,
}
#[derive(Debug, Serialize)]
pub struct TranscriptPayload<'a> {
pub session: IngestEnvelope<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_type: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<&'a str>,
pub records: &'a RawValue,
}
pub const KIND_INTERACTED: &str = "interacted";
#[must_use]
pub fn build_payload<'a>(
session: &'a TranscriptSession,
file: &'a TranscriptFile,
records: &'a RawValue,
) -> TranscriptPayload<'a> {
let some_nonempty = |s: &'a str| (!s.is_empty()).then_some(s);
TranscriptPayload {
session: IngestEnvelope {
org_id: &session.org_id,
auth_subject: &session.auth_subject,
harness_id: &session.harness_id,
harness_session_id: &session.harness_session_id,
harness_version: session.harness_version.as_deref(),
cwd: session.cwd.as_deref(),
},
agent_id: file.agent_id.as_deref(),
agent_type: some_nonempty(&file.meta.agent_type),
description: some_nonempty(&file.meta.description),
tool_use_id: some_nonempty(&file.meta.tool_use_id),
kind: None,
records,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::transcript::files::SubagentMeta;
fn session() -> TranscriptSession {
TranscriptSession::new(
tapes_capture::envelope::HARNESS_ID_CLAUDE,
"0ea3c2cc-fe9d-41ff-aab1-4134ad00c350",
)
.with_harness_version(Some("2.1.161".to_owned()))
.with_cwd(Some("/Users/me/src/repo".to_owned()))
}
fn main_file() -> TranscriptFile {
TranscriptFile {
path: "/tmp/x.jsonl".into(),
agent_id: None,
meta: SubagentMeta::default(),
}
}
fn subagent_file() -> TranscriptFile {
TranscriptFile {
path: "/tmp/agent-abc.jsonl".into(),
agent_id: Some("abc".to_owned()),
meta: SubagentMeta {
tool_use_id: "toolu_01".to_owned(),
agent_type: "general-purpose".to_owned(),
description: "dig".to_owned(),
},
}
}
#[test]
fn main_payload_matches_go_client_shape() {
let session = session();
let file = main_file();
let records = RawValue::from_string(r#"[{"b":1,"a":2}]"#.to_owned()).unwrap();
let payload = build_payload(&session, &file, &records);
let got = serde_json::to_string(&payload).unwrap();
assert_eq!(
got,
r#"{"session":{"org_id":"","auth_subject":"","harness_id":"claude","harness_session_id":"0ea3c2cc-fe9d-41ff-aab1-4134ad00c350","harness_version":"2.1.161","cwd":"/Users/me/src/repo"},"records":[{"b":1,"a":2}]}"#,
);
}
#[test]
fn subagent_payload_carries_fork_metadata() {
let session = session();
let file = subagent_file();
let records = RawValue::from_string("[]".to_owned()).unwrap();
let payload = build_payload(&session, &file, &records);
let got: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&payload).unwrap()).unwrap();
assert_eq!(got["agent_id"], "abc");
assert_eq!(got["agent_type"], "general-purpose");
assert_eq!(got["description"], "dig");
assert_eq!(got["tool_use_id"], "toolu_01");
}
#[test]
fn subagent_payload_omits_empty_meta_fields() {
let session = session();
let mut file = subagent_file();
file.meta = SubagentMeta::default();
let records = RawValue::from_string("[]".to_owned()).unwrap();
let payload = build_payload(&session, &file, &records);
let got = serde_json::to_string(&payload).unwrap();
assert!(!got.contains("agent_type"), "got: {got}");
assert!(!got.contains("tool_use_id"), "got: {got}");
assert!(!got.contains("description"), "got: {got}");
assert!(got.contains(r#""agent_id":"abc""#), "got: {got}");
}
#[test]
fn identity_fields_are_always_present_and_carry_a_subject_when_set() {
let session = TranscriptSession::new("claude", "sid").with_auth_subject("local:alice");
let file = main_file();
let records = RawValue::from_string("[]".to_owned()).unwrap();
let got = serde_json::to_string(&build_payload(&session, &file, &records)).unwrap();
assert!(
got.contains(r#""auth_subject":"local:alice""#),
"got: {got}"
);
assert!(got.contains(r#""org_id":"""#), "got: {got}");
assert!(!got.contains("harness_version"), "got: {got}");
assert!(!got.contains("cwd"), "got: {got}");
}
#[test]
fn an_unset_kind_leaves_the_payload_bytes_unchanged() {
let session = session();
let file = subagent_file();
let records = RawValue::from_string("[]".to_owned()).unwrap();
let payload = build_payload(&session, &file, &records);
assert!(payload.kind.is_none(), "build_payload never sets kind");
let got = serde_json::to_string(&payload).unwrap();
assert!(!got.contains("kind"), "got: {got}");
assert_eq!(
got,
r#"{"session":{"org_id":"","auth_subject":"","harness_id":"claude","harness_session_id":"0ea3c2cc-fe9d-41ff-aab1-4134ad00c350","harness_version":"2.1.161","cwd":"/Users/me/src/repo"},"agent_id":"abc","agent_type":"general-purpose","description":"dig","tool_use_id":"toolu_01","records":[]}"#,
);
}
#[test]
fn an_anchor_kind_serializes_after_tool_use_id() {
let session = session();
let file = subagent_file();
let records = RawValue::from_string("[]".to_owned()).unwrap();
let payload = TranscriptPayload {
kind: Some(KIND_INTERACTED),
..build_payload(&session, &file, &records)
};
let got = serde_json::to_string(&payload).unwrap();
assert!(
got.contains(r#""tool_use_id":"toolu_01","kind":"interacted","records":[]"#),
"got: {got}",
);
}
#[test]
fn records_embed_verbatim() {
let session = session();
let file = main_file();
let raw = r#"[{"z":1,"a": 2},{"b":[3, 4]}]"#;
let records = RawValue::from_string(raw.to_owned()).unwrap();
let got = serde_json::to_string(&build_payload(&session, &file, &records)).unwrap();
assert!(
got.ends_with(&format!(r#","records":{raw}}}"#)),
"got: {got}",
);
}
}