blob: d872366ce8602db674847c01491b75afa6aba352 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::LogScope;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::model::ModelEvent;
use maka_agent_facts::model::ModelFinishReason;
use maka_agent_facts::model::ModelPart;
use maka_agent_facts::model::ModelStep;
use maka_agent_facts::model::ModelToolCall;
use maka_agent_facts::tool_call::ToolCallIdentity;
use maka_agent_facts::tool_call::ToolOrigin;
use maka_agent_facts::tool_call::ToolRejection;
use maka_event_log::EventLog;
use maka_presentation::tool_message_id;
use rusqlite::Connection;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
#[path = "payloads.rs"]
mod payloads;
fn event(fact: Fact) -> EventWrite {
EventWrite::plain(RuntimeEvent::new(
Invocation {
session_id: "session".into(),
turn_id: "turn".into(),
run_id: "run".into(),
invocation_id: "invocation".into(),
},
fact,
))
.unwrap()
}
fn success(operation: &str, output: Value) -> EventWrite {
let template = event(Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
});
let template = template.event();
EventWrite::tool_success(
template.id.clone(),
template.recorded_at,
template.invocation.clone(),
operation.into(),
output.into(),
)
.unwrap()
.0
}
async fn accepted(log: &EventLog, step: &str, name: &str) -> u64 {
let call = ModelToolCall {
id: "reused-provider-id".into(),
name: name.into(),
input: json!({}).into(),
provider_options: None,
provider_executed: false,
};
let facts = [
Fact::ModelRequested {
context_breakdown: None,
item_acceptance: false,
purpose: maka_agent_facts::context::ModelPurpose::Main,
context: None,
checkpoint_event_id: None,
step_id: step.into(),
model_id: "fixture".into(),
source_scope: LogScope::Root,
source_high_water: 0,
source_digest: "".into(),
effective_source_digest: None,
input_digest: "".into(),
route_identity: "".into(),
},
Fact::ModelObserved {
step_id: step.into(),
event: ModelEvent::ToolCall(call.clone()),
},
Fact::ModelCompleted {
step_id: step.into(),
output: ModelStep {
parts: vec![ModelPart::ToolCall { call }],
finish_reason: ModelFinishReason::ToolCalls,
usage: Default::default(),
provider_options: None,
response_id: None,
model: None,
timestamp: None,
},
},
];
*log.append_batch(&facts.map(event))
.await
.unwrap()
.last()
.unwrap()
}
fn rows(db: &Connection) -> Vec<(maka_agent_facts::history::RowPosition, Vec<u8>, String)> {
db.prepare("SELECT source_sequence, item_index, payload, digest FROM transcript_cache.rows ORDER BY source_sequence, item_index")
.unwrap()
.query_map([], |row| Ok((maka_agent_facts::history::RowPosition { source_sequence: row.get::<_, i64>(0)? as u64, item_index: row.get::<_, i64>(1)? as u64 }, row.get(2)?, row.get(3)?)))
.unwrap()
.collect::<Result<_, _>>()
.unwrap()
}
async fn materialize_all(log: &EventLog, fence: u64) {
crate::transcript_cache::materialize_all_rows(log, "session", fence)
.await
.unwrap();
}
#[tokio::test]
async fn tool_boundaries_page_independently_and_rebuild_exact_ids_without_reading_sibling_results()
{
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.append(&event(Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
source_messages: Vec::new(),
content: "read".into(),
request_fingerprint: None,
},
}))
.await
.unwrap();
let accepted_fence = accepted(&log, "first", "exec").await;
let parent = "first:reused-provider-id";
log.append(&event(Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: parent.into(),
call: ToolCallIdentity::provider("first".into(), "reused-provider-id".into()),
name: "exec".into(),
input: json!({}),
}))
.await
.unwrap();
for index in 0..3 {
let operation = format!("child-{index}");
log.append(&event(Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: operation.clone(),
call: ToolCallIdentity {
tool_call_id: format!("nested-{index}"),
origin: ToolOrigin::CodeMode {
parent_operation_id: parent.into(),
parent_tool_call_id: "reused-provider-id".into(),
},
},
name: "Read".into(),
input: json!({"path":format!("file-{index}")}),
}))
.await
.unwrap();
// Three siblings exceed the 16 MiB evidence budget together. Each tool
// boundary needs only its own result, parent T1 and accepted model step.
log.append(&success(
&operation,
json!({"content":"x".repeat(6 * 1024 * 1024)}),
))
.await
.unwrap();
}
log.append(&success(
parent,
json!({"ok":true,"value":null,"toolCalls":[]}),
))
.await
.unwrap();
accepted(&log, "second", "Read").await;
let refusal = event(Fact::ToolRejected {
operation_id: "second:reused-provider-id".into(),
call: ToolCallIdentity::provider("second".into(), "reused-provider-id".into()),
name: "Read".into(),
input: json!({}).into(),
reason: ToolRejection::InvalidInput {
message: "path required".into(),
},
});
log.append(&refusal).await.unwrap();
let through = log
.append(&event(Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
}))
.await
.unwrap();
let notices = log.subscribe_commits();
// A historical fence sees the planned provider call, but no later dispatch,
// nested call, result or rejection from the already-existing future suffix.
materialize_all(&log, accepted_fence).await;
let first = crate::test_store::inspect(&log, rows).await;
assert_eq!(first.len(), 3);
let parent_id = tool_message_id("invocation", parent);
let planned: Value = serde_json::from_slice(&first[2].1).unwrap();
assert_eq!(planned["type"], "tool_call");
assert_eq!(planned["id"], parent_id);
assert!(
planned.get("stepId").is_none(),
"pure-tool steps need no fake assistant anchor"
);
materialize_all(&log, through).await;
assert!(
!notices.has_changed().unwrap(),
"derived cache must not publish execution commits"
);
let original = crate::test_store::inspect(&log, rows).await;
assert_eq!(&original[..3], first.as_slice());
assert_eq!(original.len(), 17);
let values: Vec<Value> = original
.iter()
.map(|(_, bytes, digest)| {
assert_eq!(
digest,
&format!("sha256:{}", hex::encode(Sha256::digest(bytes)))
);
serde_json::from_slice(bytes).unwrap()
})
.filter(|row: &Value| row["status"] != "running" && row["type"] != "tool_activity")
.collect();
for index in 0..3 {
let call = &values[2 + index * 2];
let result = &values[3 + index * 2];
let id = tool_message_id("invocation", &format!("child-{index}"));
assert_eq!(call["id"], id);
assert_eq!(result["toolUseId"], id);
for value in [call, result] {
assert_eq!(value["origin"], "code_mode");
assert_eq!(value["modelVisibility"], "hidden");
assert_eq!(value["parentToolCallId"], parent_id);
assert_eq!(value["parentOperationId"], parent);
}
assert_eq!(result["isError"], false);
assert_eq!(result["content"]["kind"], "json");
assert_eq!(
result["content"]["value"]["content"]
.as_str()
.unwrap()
.len(),
6 * 1024 * 1024
);
}
let second_id = tool_message_id("invocation", "second:reused-provider-id");
assert_ne!(
second_id, parent_id,
"raw provider ID reuse must not alias UI rows"
);
assert_eq!(values[8]["toolUseId"], parent_id);
assert_eq!(values[9]["id"], second_id);
assert_eq!(values[10]["toolUseId"], second_id);
assert_eq!(values[10]["id"], refusal.event().id);
assert_eq!(values[10]["isError"], true);
assert_eq!(values[10]["content"]["kind"], "text");
assert!(
values[10]["content"]["text"]
.as_str()
.unwrap()
.contains("path required")
);
assert_eq!(values[11]["status"], "completed");
// Search scans only bounded disposable-index batches, including large
// nested results; visible trace parents and errors remain searchable.
for (query, include_internal, expected) in [
("xxxx", false, 3),
("toolCalls", false, 1),
("toolCalls", true, 1),
("path required", false, 1),
] {
let mut after = None;
let mut sequences = Vec::new();
let mut batches = 0;
loop {
let result = log
.search_transcript(
"session",
maka_event_log::transcript::search::TranscriptSearch {
through,
after,
query: query.into(),
case_sensitive: true,
whole_word: false,
include_internal,
max_matches: 64,
},
)
.await
.unwrap();
batches += 1;
for found in result.matches {
let (sequence, preview) = (found.position, found.preview);
assert!(preview.len() <= 384 && preview.contains(query));
assert!(sequences.last().is_none_or(|last| sequence > *last));
sequences.push(sequence);
}
let Some(next) = result.next_after else {
break;
};
assert!(after.is_none_or(|after| next > after) && batches < 8);
after = Some(next);
}
assert_eq!(sequences.len(), expected, "{query}");
assert!(batches >= 2, "18 MiB of siblings must not fit in one scan");
}
assert!(
!notices.has_changed().unwrap(),
"search emits no execution commits"
);
let canonical = log.prefix(128, 32 * 1024 * 1024).await.unwrap().digest;
crate::test_store::inspect(&log, move |db| {
db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows")
.unwrap()
})
.await;
log.close().await.unwrap();
let reopened = EventLog::open(&path).await.unwrap();
materialize_all(&reopened, through).await;
assert_eq!(
crate::test_store::inspect(&reopened, rows).await,
original,
"replaying earlier call evidence must be idempotent"
);
assert_eq!(
reopened.prefix(128, 32 * 1024 * 1024).await.unwrap().digest,
canonical
);
crate::test_store::inspect(&reopened, move |db| {
db.execute("DELETE FROM transcript_cache.rows", []).unwrap()
})
.await;
crate::test_store::inspect(&reopened, move |db| {
db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows")
.unwrap()
})
.await;
materialize_all(&reopened, through).await;
assert_eq!(
crate::test_store::inspect(&reopened, rows).await,
original,
"the complete index is disposable"
);
}