feat(session): share immutable captures and bounded body transport
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
//! Public consumer boundary: real capture adapters, checked DTO codec and pure readers.
|
||||
//! No Worker, persistence, live Session provider, or substituted capture implementation.
|
||||
|
||||
use agen::{HistoryEntry, Item};
|
||||
use protocol::{
|
||||
SessionEntryProvenance, SessionSnapshot, SessionSnapshotEntry, SessionSnapshotEntryData,
|
||||
};
|
||||
use session_store::capture::*;
|
||||
use session_store::{
|
||||
FeatureInputPayload, HistoryInputMetadata, HistoryInputUsePolicy, InputCompactionUse,
|
||||
InputEvidenceUse, InputObservationUse, LoggedHistoryDerivation, LoggedSessionHistoryEntryId,
|
||||
LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin, PublicSystemItem,
|
||||
PublicSystemPresentation,
|
||||
};
|
||||
|
||||
fn history(
|
||||
id: &str,
|
||||
item: Item,
|
||||
input_metadata: Option<HistoryInputMetadata>,
|
||||
) -> HistoryEntry<LoggedSessionHistoryMetadata> {
|
||||
HistoryEntry::new(
|
||||
item,
|
||||
LoggedSessionHistoryMetadata {
|
||||
entry_id: LoggedSessionHistoryEntryId(id.into()),
|
||||
origin: LoggedSessionHistoryOrigin::LocalHumanInput,
|
||||
derivation: Some(LoggedHistoryDerivation {
|
||||
sources: vec![LoggedSessionHistoryEntryId("earlier-entry".into())],
|
||||
}),
|
||||
input_metadata,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(recording: bool, memory: bool, observation: bool) -> HistoryInputUsePolicy {
|
||||
HistoryInputUsePolicy {
|
||||
recording_material: if recording {
|
||||
InputEvidenceUse::Include
|
||||
} else {
|
||||
InputEvidenceUse::Exclude
|
||||
},
|
||||
memory_evidence: if memory {
|
||||
InputEvidenceUse::Include
|
||||
} else {
|
||||
InputEvidenceUse::Exclude
|
||||
},
|
||||
observation: if observation {
|
||||
InputObservationUse::Readable
|
||||
} else {
|
||||
InputObservationUse::Hidden
|
||||
},
|
||||
compaction: InputCompactionUse::Retain,
|
||||
}
|
||||
}
|
||||
|
||||
fn reference(capture: &SessionCapture, id: &str) -> ScopedSessionEntryRef {
|
||||
capture
|
||||
.scoped_ref(&SessionEntryRef::parse(id).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_consumer_roundtrip_retains_body_original_source_order_origin_and_independent_policy() {
|
||||
let entries = vec![
|
||||
history(
|
||||
"recording",
|
||||
Item::user_message("record-only"),
|
||||
Some(HistoryInputMetadata::new(usage(true, false, false), None, None).unwrap()),
|
||||
),
|
||||
history(
|
||||
"memory",
|
||||
Item::user_message("memory-only"),
|
||||
Some(HistoryInputMetadata::new(usage(false, true, false), None, None).unwrap()),
|
||||
),
|
||||
history(
|
||||
"observation",
|
||||
Item::user_message("observation-only"),
|
||||
Some(HistoryInputMetadata::new(usage(false, false, true), None, None).unwrap()),
|
||||
),
|
||||
];
|
||||
let capture = SessionCapture::from_history_entries("synthetic-session", "selected", entries);
|
||||
let mut dto = capture.to_dto();
|
||||
// The checked boundary also accepts entries retained from different original segments.
|
||||
dto.entries[0].source.segment_id = "original-a".into();
|
||||
dto.entries[0].source.entry_index = 80;
|
||||
dto.entries[1].source.segment_id = "original-b".into();
|
||||
dto.entries[1].source.entry_index = 3;
|
||||
let capture = SessionCapture::try_from_dto(dto.clone(), CaptureLimits::default()).unwrap();
|
||||
let encoded = capture.encode(CaptureLimits::default()).unwrap();
|
||||
let restored = SessionCapture::decode(&encoded, CaptureLimits::default()).unwrap();
|
||||
assert_eq!(restored.to_dto(), dto);
|
||||
assert_eq!(restored.capture_id(), capture.capture_id());
|
||||
for (id, allowed) in [
|
||||
("Erecording", CapturePurpose::Recording),
|
||||
("Ememory", CapturePurpose::MemoryEvidence),
|
||||
("Eobservation", CapturePurpose::Observation),
|
||||
] {
|
||||
let reference = reference(&capture, id);
|
||||
assert_eq!(
|
||||
restored.resolve_ref(&reference, allowed).unwrap(),
|
||||
capture.resolve_ref(&reference, allowed).unwrap()
|
||||
);
|
||||
for purpose in [
|
||||
CapturePurpose::Recording,
|
||||
CapturePurpose::MemoryEvidence,
|
||||
CapturePurpose::Observation,
|
||||
] {
|
||||
assert_eq!(
|
||||
restored.resolve_ref(&reference, purpose).is_ok(),
|
||||
purpose == allowed
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(restored.overview().len(), 1);
|
||||
assert_eq!(restored.search(&SearchOptions::default()).len(), 1);
|
||||
assert!(restored.evidence_for("Erecording").is_none());
|
||||
let evidence = restored.evidence_for("Ememory").unwrap();
|
||||
assert_eq!(evidence.segment_id, "original-b");
|
||||
assert!(evidence.excerpt.contains("memory-only"));
|
||||
let recorded = restored
|
||||
.slice(CapturePurpose::Recording, None, 10, 16_000)
|
||||
.unwrap();
|
||||
assert_eq!(recorded.slice().items.len(), 1);
|
||||
assert_eq!(recorded.slice().items[0].text, "record-only");
|
||||
for (purpose, expected) in [
|
||||
(CapturePurpose::MemoryEvidence, "memory-only"),
|
||||
(CapturePurpose::Observation, "observation-only"),
|
||||
] {
|
||||
let slice = restored.slice(purpose, None, 10, 16_000).unwrap();
|
||||
assert_eq!(slice.slice().items.len(), 1);
|
||||
assert_eq!(slice.slice().items[0].text, expected);
|
||||
assert!(
|
||||
restored
|
||||
.read_raw(
|
||||
&reference(&restored, "Erecording"),
|
||||
purpose,
|
||||
RawField::MessagePart(0),
|
||||
0,
|
||||
100
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_data_drops_reasoning_raw_system_and_private_payload_even_when_unindexed() {
|
||||
let payload = FeatureInputPayload::encode(
|
||||
"capture-test",
|
||||
1,
|
||||
&serde_json::json!({"secret": "PRIVATE-PAYLOAD"}),
|
||||
)
|
||||
.unwrap();
|
||||
let public = PublicSystemItem {
|
||||
kind: "notice".into(),
|
||||
body: "public notice".into(),
|
||||
presentation: PublicSystemPresentation::Ignored,
|
||||
};
|
||||
let system =
|
||||
HistoryInputMetadata::new(usage(true, true, true), Some(payload.clone()), Some(public))
|
||||
.unwrap();
|
||||
let private = HistoryInputMetadata::new(usage(true, true, true), Some(payload), None).unwrap();
|
||||
let capture = SessionCapture::from_history_entries(
|
||||
"s",
|
||||
"segment",
|
||||
vec![
|
||||
history("raw", Item::system_message("RAW-SYSTEM-PROMPT"), None),
|
||||
history("reasoning", Item::reasoning("HIDDEN-REASONING"), None),
|
||||
history(
|
||||
"private",
|
||||
Item::system_message("PRIVATE-SYSTEM-BODY"),
|
||||
Some(private),
|
||||
),
|
||||
history(
|
||||
"public",
|
||||
Item::system_message("MODEL-SYSTEM-BODY"),
|
||||
Some(system),
|
||||
),
|
||||
history("ordinary", Item::user_message("ordinary"), None),
|
||||
],
|
||||
);
|
||||
let encoded = String::from_utf8(capture.encode(CaptureLimits::default()).unwrap()).unwrap();
|
||||
let retained_debug = format!("{capture:?}");
|
||||
for secret in [
|
||||
"RAW-SYSTEM-PROMPT",
|
||||
"HIDDEN-REASONING",
|
||||
"PRIVATE-SYSTEM-BODY",
|
||||
"MODEL-SYSTEM-BODY",
|
||||
"PRIVATE-PAYLOAD",
|
||||
] {
|
||||
assert!(!encoded.contains(secret));
|
||||
assert!(!retained_debug.contains(secret));
|
||||
}
|
||||
assert_eq!(capture.to_dto().entries.len(), 2);
|
||||
assert!(encoded.contains("public notice"));
|
||||
// Filtered entries do not renumber the public source positions.
|
||||
assert_eq!(capture.to_dto().entries[0].order, 3);
|
||||
assert_eq!(
|
||||
capture.source_index_for_ref(&SessionEntryRef::parse("Eordinary").unwrap()),
|
||||
Some(4)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_projection_does_not_invent_recording_permission() {
|
||||
let capture = SessionCapture::from_session_snapshot(
|
||||
"s",
|
||||
"seg",
|
||||
SessionSnapshot {
|
||||
pending_submissions: Default::default(),
|
||||
entries: vec![SessionSnapshotEntry {
|
||||
entry_id: "projected".into(),
|
||||
timestamp: 0,
|
||||
provenance: SessionEntryProvenance::HumanInput,
|
||||
derived_from: vec!["source".into()],
|
||||
use_policy: Some(protocol::SessionEntryUsePolicy {
|
||||
observation_readable: true,
|
||||
memory_evidence: false,
|
||||
}),
|
||||
data: SessionSnapshotEntryData::Message {
|
||||
role: protocol::SessionMessageRole::User,
|
||||
content: vec![protocol::SessionContentPart::Text {
|
||||
text: "projected".into(),
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
);
|
||||
let reference = reference(&capture, "Eprojected");
|
||||
assert!(matches!(
|
||||
capture.resolve_ref(&reference, CapturePurpose::Recording),
|
||||
Err(CaptureError::PolicyDenied)
|
||||
));
|
||||
assert!(
|
||||
capture
|
||||
.resolve_ref(&reference, CapturePurpose::Observation)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(capture.evidence_for("Eprojected").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_dtos_and_oversized_representations_are_rejected() {
|
||||
let capture = SessionCapture::new("s", "seg", vec![Item::user_message("body")]);
|
||||
let original = serde_json::to_value(capture.to_dto()).unwrap();
|
||||
let mut invalids = Vec::new();
|
||||
let mut value = original.clone();
|
||||
value["version"] = 999.into();
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["unexpected"] = true.into();
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["policy"]["recording_material"] = "include".into();
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["policy"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("memory_evidence");
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["body"] = serde_json::json!({"kind":"reasoning","text":"secret"});
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["body"]["feature_payload"] = serde_json::json!({"private":true});
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["body"]["role"] = "system".into();
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["source"]["unknown"] = true.into();
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["source"]["history_origin"] =
|
||||
serde_json::json!({"kind":"legacy_unknown","unknown":true});
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"][0]["entry_id"] = "bad/id".into();
|
||||
invalids.push(value);
|
||||
let mut value = original.clone();
|
||||
value["entries"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(original["entries"][0].clone());
|
||||
invalids.push(value);
|
||||
for invalid in invalids {
|
||||
let bytes = serde_json::to_vec(&invalid).unwrap();
|
||||
assert!(
|
||||
SessionCapture::decode(&bytes, CaptureLimits::default()).is_err(),
|
||||
"accepted {invalid}"
|
||||
);
|
||||
}
|
||||
let bytes = capture.encode(CaptureLimits::default()).unwrap();
|
||||
assert!(
|
||||
SessionCapture::decode(
|
||||
&bytes,
|
||||
CaptureLimits {
|
||||
max_bytes: bytes.len() - 1,
|
||||
..CaptureLimits::default()
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
capture
|
||||
.encode(CaptureLimits {
|
||||
max_entries: 0,
|
||||
..CaptureLimits::default()
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
capture
|
||||
.encode(CaptureLimits {
|
||||
max_text_bytes: 3,
|
||||
..CaptureLimits::default()
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let mut reversed = capture.to_dto();
|
||||
let mut second = reversed.entries[0].clone();
|
||||
second.entry_id = "second".into();
|
||||
second.source.entry_index = 4;
|
||||
second.order = 0;
|
||||
reversed.entries[0].order = 1;
|
||||
reversed.entries.push(second);
|
||||
assert!(SessionCapture::try_from_dto(reversed, CaptureLimits::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_utf8_tool_input_and_result_are_lossless_without_preview_markers() {
|
||||
let arguments = "\"quoted\"\\path\n日本語🙂".repeat(300);
|
||||
let result = "result\n\t\\\"λ🙂".repeat(400);
|
||||
let capture = SessionCapture::new(
|
||||
"s",
|
||||
"seg",
|
||||
vec![
|
||||
Item::tool_call("call", "tool", arguments.clone()),
|
||||
Item::tool_result_item("call", "summary", Some(result.clone()), false),
|
||||
],
|
||||
);
|
||||
for (id, field, expected) in [
|
||||
("E00000000", RawField::ToolArguments, arguments),
|
||||
("E00000001", RawField::ToolContent, result),
|
||||
] {
|
||||
let reference = reference(&capture, id);
|
||||
let mut offset = 0;
|
||||
let mut actual = String::new();
|
||||
loop {
|
||||
let part = capture
|
||||
.read_raw(&reference, CapturePurpose::Observation, field, offset, 17)
|
||||
.unwrap();
|
||||
assert!(part.text.len() <= 17);
|
||||
assert_eq!(part.offset, offset);
|
||||
actual.push_str(&part.text);
|
||||
match part.next_offset {
|
||||
Some(next) => {
|
||||
assert!(next > offset);
|
||||
offset = next;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
let unicode = SessionCapture::new("s", "seg", vec![Item::tool_call("call", "tool", "🙂")]);
|
||||
let reference = reference(&unicode, "E00000000");
|
||||
assert!(matches!(
|
||||
unicode.read_raw(
|
||||
&reference,
|
||||
CapturePurpose::Observation,
|
||||
RawField::ToolArguments,
|
||||
1,
|
||||
8
|
||||
),
|
||||
Err(CaptureError::InvalidRange)
|
||||
));
|
||||
assert!(matches!(
|
||||
unicode.read_raw(
|
||||
&reference,
|
||||
CapturePurpose::Observation,
|
||||
RawField::ToolArguments,
|
||||
0,
|
||||
3
|
||||
),
|
||||
Err(CaptureError::BudgetTooSmall)
|
||||
));
|
||||
assert!(matches!(
|
||||
unicode.read_raw(
|
||||
&reference,
|
||||
CapturePurpose::Observation,
|
||||
RawField::ToolArguments,
|
||||
5,
|
||||
8
|
||||
),
|
||||
Err(CaptureError::InvalidRange)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_json_slice_budget_includes_metadata_and_bound_continuation() {
|
||||
let body = "\"\\\n\t🙂日本語".repeat(500);
|
||||
let capture = SessionCapture::new(
|
||||
"synthetic-session",
|
||||
"segment",
|
||||
vec![Item::tool_call("call", "tool", body.clone())],
|
||||
);
|
||||
let other_session = SessionCapture::new(
|
||||
"other-session",
|
||||
"segment",
|
||||
vec![Item::tool_call("call", "tool", body.clone())],
|
||||
);
|
||||
let changed = SessionCapture::new(
|
||||
"synthetic-session",
|
||||
"segment",
|
||||
vec![Item::tool_call("call", "tool", "changed")],
|
||||
);
|
||||
let mut cursor = None;
|
||||
let mut actual = String::new();
|
||||
let budget = 1400;
|
||||
loop {
|
||||
let part = capture
|
||||
.slice(CapturePurpose::Recording, cursor.as_ref(), 1, budget)
|
||||
.unwrap();
|
||||
assert!(part.encoded_len() <= budget);
|
||||
assert_eq!(part.encoded_len(), part.as_json().len());
|
||||
assert_eq!(serde_json::to_vec(part.slice()).unwrap(), part.as_json());
|
||||
assert_eq!(part.slice().items.len(), 1);
|
||||
let item = &part.slice().items[0];
|
||||
assert_eq!(item.offset, actual.len());
|
||||
actual.push_str(&item.text);
|
||||
cursor = part.slice().next_cursor.clone();
|
||||
if let Some(cursor) = &cursor {
|
||||
assert!(matches!(
|
||||
other_session.slice(CapturePurpose::Recording, Some(cursor), 1, budget),
|
||||
Err(CaptureError::ForeignReference)
|
||||
));
|
||||
assert!(matches!(
|
||||
changed.slice(CapturePurpose::Recording, Some(cursor), 1, budget),
|
||||
Err(CaptureError::ForeignReference)
|
||||
));
|
||||
assert!(matches!(
|
||||
capture.slice(CapturePurpose::Observation, Some(cursor), 1, budget),
|
||||
Err(CaptureError::ForeignReference)
|
||||
));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(actual, body);
|
||||
assert!(matches!(
|
||||
capture.slice(CapturePurpose::Recording, None, 1, 8),
|
||||
Err(CaptureError::BudgetTooSmall)
|
||||
));
|
||||
assert!(matches!(
|
||||
capture.slice(CapturePurpose::Recording, None, 0, budget),
|
||||
Err(CaptureError::BudgetTooSmall)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_immutability_and_scoped_operations_reject_foreign_or_unknown_refs_and_bad_ranges() {
|
||||
let mut source = vec![
|
||||
Item::user_message("before"),
|
||||
Item::assistant_message("answer"),
|
||||
];
|
||||
let capture = SessionCapture::new("s", "seg", source.clone());
|
||||
let fixed_bytes = capture.encode(CaptureLimits::default()).unwrap();
|
||||
let first = reference(&capture, "E00000000");
|
||||
let second = reference(&capture, "E00000001");
|
||||
source.push(Item::user_message("appended"));
|
||||
source[0] = Item::user_message("compacted");
|
||||
source.clear();
|
||||
assert_eq!(
|
||||
capture.encode(CaptureLimits::default()).unwrap(),
|
||||
fixed_bytes
|
||||
);
|
||||
assert_eq!(
|
||||
capture.clone().encode(CaptureLimits::default()).unwrap(),
|
||||
fixed_bytes
|
||||
);
|
||||
let foreign = SessionCapture::new(
|
||||
"different",
|
||||
"seg",
|
||||
vec![
|
||||
Item::user_message("before"),
|
||||
Item::assistant_message("answer"),
|
||||
],
|
||||
);
|
||||
assert!(matches!(
|
||||
foreign.resolve_ref(&first, CapturePurpose::Observation),
|
||||
Err(CaptureError::ForeignReference)
|
||||
));
|
||||
assert!(matches!(
|
||||
capture.checked_range(&second, &first, CapturePurpose::Observation),
|
||||
Err(CaptureError::InvalidRange)
|
||||
));
|
||||
assert_eq!(
|
||||
capture
|
||||
.checked_range(&first, &second, CapturePurpose::Observation)
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
let unknown = SessionEntryRef::parse("E99999999").unwrap();
|
||||
assert_eq!(capture.source_index_for_ref(&unknown), None);
|
||||
assert!(matches!(
|
||||
capture.scoped_ref(&unknown),
|
||||
Err(CaptureError::UnknownReference)
|
||||
));
|
||||
assert!(
|
||||
capture
|
||||
.search_checked(&SearchOptions {
|
||||
from: Some(unknown.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
capture
|
||||
.search(&SearchOptions {
|
||||
through: Some(unknown),
|
||||
..Default::default()
|
||||
})
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
capture
|
||||
.read_checked(ReadSelector::EntryRange([1, 0]), ReadOptions::default())
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
capture
|
||||
.read_checked(ReadSelector::EntryRange([0, 99]), ReadOptions::default())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slices_preserve_empty_and_long_result_fields_under_item_and_metadata_budgets() {
|
||||
let summary = "summary\n\"🙂".repeat(100);
|
||||
let content = "content\t\\日本語".repeat(200);
|
||||
let capture = SessionCapture::new(
|
||||
"s",
|
||||
"seg",
|
||||
vec![
|
||||
Item::tool_result_item("first", summary.clone(), Some(content.clone()), true),
|
||||
Item::tool_result_item("empty", "", Some(String::new()), false),
|
||||
],
|
||||
);
|
||||
let mut cursor = None;
|
||||
let mut fields: Vec<(String, RawField, String)> = Vec::new();
|
||||
loop {
|
||||
let part = capture
|
||||
.slice(CapturePurpose::Recording, cursor.as_ref(), 2, 1500)
|
||||
.unwrap();
|
||||
assert!(part.slice().items.len() <= 2);
|
||||
assert!(serde_json::to_vec(part.slice()).unwrap().len() <= 1500);
|
||||
for item in &part.slice().items {
|
||||
let id = item.reference.entry_ref.to_string();
|
||||
if let Some((last_id, last_field, text)) = fields.last_mut() {
|
||||
if *last_id == id && *last_field == item.field {
|
||||
assert_eq!(text.len(), item.offset);
|
||||
text.push_str(&item.text);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
assert_eq!(item.offset, 0);
|
||||
fields.push((id, item.field, item.text.clone()));
|
||||
}
|
||||
cursor = part.slice().next_cursor.clone();
|
||||
if cursor.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
fields,
|
||||
vec![
|
||||
("E00000000".into(), RawField::ToolSummary, summary),
|
||||
("E00000000".into(), RawField::ToolContent, content),
|
||||
("E00000001".into(), RawField::ToolSummary, String::new()),
|
||||
("E00000001".into(), RawField::ToolContent, String::new()),
|
||||
]
|
||||
);
|
||||
let metadata_heavy = SessionCapture::new(
|
||||
"s",
|
||||
"seg",
|
||||
vec![Item::tool_call("call", "name".repeat(200), "x")],
|
||||
);
|
||||
assert!(matches!(
|
||||
metadata_heavy.slice(CapturePurpose::Recording, None, 1, 700),
|
||||
Err(CaptureError::BudgetTooSmall)
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user