mj_transcript/
authorization.rs1use anyhow::{Result, ensure};
3use mj_core::continuation::{ASSISTANT_BYTES, ContinuationEvidence, EvidenceMessage};
4use mj_core::state::MaterializedSession;
5use mj_core::transcript::{TranscriptBody, materialized_chunks_text, materialized_content_text};
6
7pub fn evidence(session: &MaterializedSession) -> Result<ContinuationEvidence> {
8 evidence_from_items(session.transcript.iter().rev().cloned().map(Ok))
9}
10
11pub fn evidence_from_items(
12 items: impl IntoIterator<Item = Result<std::sync::Arc<mj_core::state::TranscriptItem>>>,
13) -> Result<ContinuationEvidence> {
14 let mut messages = Vec::new();
15 let mut user_bytes = 0;
16 let mut assistant_bytes = 0;
17 let mut assistant_history_omitted = false;
18 for item in items {
21 let item = item?;
22 if mj_core::archive::is_context_boundary(&item.stable_id) {
23 break;
24 }
25 let (role, text) = match &item.body {
26 TranscriptBody::User { content } => {
27 let id = item
28 .stable_id
29 .strip_prefix("user:")
30 .unwrap_or(&item.stable_id);
31 if mj_core::continuation::is_generated_prompt(id) {
32 continue;
33 }
34 ensure!(
35 content.iter().all(|v| v["type"] == "text"),
36 "authorization depends on non-text context"
37 );
38 let text = materialized_content_text(content);
39 if mj_core::continuation::is_generated_prompt_text(&text) {
40 continue;
41 }
42 if matches!(
43 mj_core::acp::context_command_text(&text),
44 Some((mj_core::acp::ContextCommand::Clear, _))
45 ) {
46 break;
47 }
48 user_bytes += text.len();
49 ensure!(
50 user_bytes <= mj_core::continuation::USER_BYTES,
51 "user history exceeds evidence budget"
52 );
53 ("user", text)
54 }
55 TranscriptBody::Agent { chunks, streaming } => {
56 ensure!(!streaming, "assistant reply is still streaming");
57 let text = materialized_chunks_text(chunks);
58 if text.trim().is_empty() {
59 continue;
60 }
61 if assistant_history_omitted || assistant_bytes + text.len() > ASSISTANT_BYTES {
62 ensure!(
63 assistant_bytes > 0,
64 "final assistant reply exceeds evidence budget"
65 );
66 assistant_history_omitted = true;
67 continue;
68 }
69 assistant_bytes += text.len();
70 ("assistant", text)
71 }
72 _ => continue,
73 };
74 if !text.trim().is_empty() {
75 ensure!(messages.len() < 256, "too many continuation messages");
76 messages.push(EvidenceMessage {
77 id: item.stable_id.clone(),
78 role: role.into(),
79 text,
80 });
81 }
82 }
83 messages.reverse();
84 let evidence = ContinuationEvidence {
85 quota_message: None,
86 messages,
87 assistant_history_omitted,
88 };
89 evidence.validate()?;
90 Ok(evidence)
91}