blob: 1cc8668026f21dd2f5ceeda8199015e9f92ece1a [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 super::support::preflight as fixture;
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::model::ModelFinishReason;
use maka_agent_facts::model::ModelPart;
use maka_agent_facts::model::ModelStep;
use maka_agent_facts::tool_call::ToolOrigin;
use maka_agent_facts::tools::ToolExecutor;
use maka_agent_facts::tools::ToolFuture;
use maka_code_runtime::CellLimits;
use maka_event_log::EventLog;
use maka_tools::CodeExecutor;
use maka_tools::{
RunTools, ToolCatalog, ToolDefinition, ToolHandler, ToolMode, ToolNesting, ToolRegistration,
ToolSemantics,
};
use serde_json::{Value, json};
use std::sync::Arc;
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
struct Block(Arc<Notify>);
impl ToolExecutor for Block {
fn names(&self) -> Vec<String> {
vec!["block".into()]
}
fn invoke(&self, _: String, _: Value, cancellation: CancellationToken) -> ToolFuture {
let release = self.0.clone();
Box::pin(async move {
tokio::select! { _ = release.notified() => {}, _ = cancellation.cancelled() => {} }
Ok(json!({"settled":true}))
})
}
}
async fn invoke(
log: &EventLog,
run: &RunTools,
invocation: &Invocation,
step: &str,
name: &str,
input: Value,
) -> Value {
invoke_result(log, run, invocation, step, name, input)
.await
.unwrap()
}
async fn invoke_result(
log: &EventLog,
run: &RunTools,
invocation: &Invocation,
step: &str,
name: &str,
input: Value,
) -> Result<Value, maka_agent_facts::tools::ToolError> {
let call = fixture::call(step, name, input);
log.append_batch(&[
EventWrite::plain(RuntimeEvent::new(
invocation.clone(),
Fact::ModelRequested {
context_breakdown: None,
item_acceptance: false,
step_id: step.into(),
model_id: "fixture".into(),
source_scope: maka_agent_facts::event::LogScope::Root,
source_high_water: 0,
source_digest: String::new(),
effective_source_digest: None,
input_digest: String::new(),
route_identity: String::new(),
checkpoint_event_id: None,
purpose: maka_agent_facts::context::ModelPurpose::Main,
context: None,
},
))
.unwrap(),
EventWrite::plain(RuntimeEvent::new(
invocation.clone(),
Fact::ModelObserved {
step_id: step.into(),
event: maka_agent_facts::model::ModelEvent::ToolCall(call.clone()),
},
))
.unwrap(),
EventWrite::plain(RuntimeEvent::new(
invocation.clone(),
Fact::ModelCompleted {
step_id: step.into(),
output: ModelStep {
parts: vec![ModelPart::ToolCall { call: call.clone() }],
finish_reason: ModelFinishReason::ToolCalls,
usage: Default::default(),
provider_options: None,
response_id: None,
model: None,
timestamp: None,
},
},
))
.unwrap(),
])
.await
.unwrap();
run.capture(".", CancellationToken::new())
.await
.unwrap()
.into_step(step)
.invoke(&call, CancellationToken::new())
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn yielding_cells_keep_ancestry_deliver_deltas_share_json_and_drain_on_run_end() {
tokio::time::timeout(std::time::Duration::from_secs(15), async {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = Arc::new(EventLog::open(&path).await.unwrap());
let invocation = fixture::invocation("cells");
log.create_session(&invocation.session_id, "create", &json!({}), 1).await.unwrap();
fixture::accepted(&log, &invocation, &[]).await;
let release = Arc::new(Notify::new());
let catalog = ToolCatalog::new([ToolRegistration {
definition:ToolDefinition { freeform: None, output_schema: None, provider:None,name:"block".into(),description:"wait".into(),input_schema:json!({"type":"object"}) },
handler:ToolHandler::Immediate(Arc::new(Block(release.clone()))),nesting:ToolNesting::Nestable,semantics:ToolSemantics::Parallel,
}]).unwrap();
let loaded = maka_tools::availability::LoadedTools::default();
let run = RunTools::new(maka_agent_facts::language::Language::En, log.clone(), invocation.clone(), catalog, ToolMode::CodeMode, CodeExecutor::new(CellLimits::default()).unwrap(), loaded.clone());
let first = invoke(&log, &run, &invocation, "first", "exec", json!("// @exec: {\"yield_time_ms\":1000,\"max_output_tokens\":1000}\ntext('early'); await yield_control(); await tools.block({}); text('late'); store('n',42);")).await;
assert_eq!(first["state"], "running");
assert_eq!(first["content"], json!([{"kind":"text","text":"early"}]));
let cell_id = first["cell_id"].as_str().unwrap();
assert!(log.append(&maka_agent_facts::event::EventWrite::plain(maka_agent_facts::event::RuntimeEvent::new(invocation.clone(), Fact::ToolNotified {
operation_id: "first:first".into(), text: "forged".into(), model_text: "forged".into(),
})).unwrap()).await.is_err(), "only the cell, not its exec control, owns notifications");
let prefix = log.prefix(100, 1024*1024).await.unwrap();
assert!(prefix.events.iter().any(|e| matches!(&e.event.fact, Fact::ToolDispatched {operation_id,call,..}
if operation_id==cell_id && matches!(&call.origin,ToolOrigin::CodeCell{parent_operation_id,..} if parent_operation_id=="first:first"))));
assert!(log.invocation_recovery(&invocation,100,1024*1024).await.unwrap().uncertain_operations.contains(&cell_id.to_string()));
release.notify_one();
let last = invoke(&log,&run,&invocation,"observe","wait",json!({"cell_id":cell_id,"max_tokens":1000})).await;
assert_eq!(last["state"],"completed");
assert!(log.append(&maka_agent_facts::event::EventWrite::plain(maka_agent_facts::event::RuntimeEvent::new(invocation.clone(), Fact::ToolNotified {
operation_id: cell_id.into(), text: "late".into(), model_text: "late".into(),
})).unwrap()).await.is_err(), "settled cells cannot inject notifications");
assert_eq!(last["result"]["value"],Value::Null);
assert_eq!(last["content"],json!([{"kind":"text","text":"late"}]));
loaded.clear();
let next = invoke(&log,&run,&invocation,"next","exec",json!({"code":format!("const complete_source = {}; text(load('n'));", serde_json::to_string(&"甲".repeat(24_000)).unwrap())})).await;
assert_eq!(next["content"][0]["text"],"42");
let stopping = invoke(&log,&run,&invocation,"stopping","exec",json!({"code":"notify('stoppable'); await yield_control(); await tools.block({});"})).await;
assert_eq!(stopping["state"],"running");
let stopped = invoke(&log,&run,&invocation,"terminate","wait",json!({"cell_id":stopping["cell_id"],"terminate":true})).await;
assert_eq!(stopped["state"],"terminated");
assert_eq!(stopped["content"],json!([]), "termination must not repeat previously observed output");
assert!(log.invocation_recovery(&invocation,100,1024*1024).await.unwrap().uncertain_operations.is_empty());
let unfinished = invoke(&log,&run,&invocation,"unfinished","exec",json!({"code":"notify('pending'); await yield_control(); await tools.block({});"})).await;
assert_eq!(unfinished["state"],"running");
run.shutdown().await.unwrap();
assert!(log.invocation_recovery(&invocation,100,1024*1024).await.unwrap().uncertain_operations.is_empty());
let fence = log.prefix(100,1024*1024).await.unwrap().high_water;
let headers = log.transcript_headers(&invocation.session_id, &maka_event_log::transcript::TranscriptRead {
through: fence, position: None, direction: maka_event_log::transcript::TranscriptDirection::Older, limit: 8,
}).await.unwrap();
let mut result = false;
for header in headers {
let bytes = log.transcript_fragment(&invocation.session_id, header.position, 0, header.total_bytes).await.unwrap();
let row: Value = serde_json::from_slice(&bytes).unwrap();
result |= row["type"] == "tool_result";
}
assert!(result, "nested cells remain renderable after their control call settles");
drop(run);
fixture::close(log).await;
let reopened = EventLog::open(&path).await.unwrap();
assert!(reopened.invocation_recovery(&invocation,100,1024*1024).await.unwrap().uncertain_operations.is_empty());
reopened.close().await.unwrap();
}).await.expect("Code Mode observation and shutdown must not hang");
}
#[tokio::test]
async fn collected_cell_recovery_uses_the_original_run_language_and_keeps_user_output() {
use maka_agent_facts::{language::Language, tools::ToolError};
for (language, unavailable, caution) in [
(
Language::En,
"cell is unavailable",
"do not repeat effects blindly",
),
(Language::ZhCn, "代码单元不可用", "请勿盲目重复执行"),
(Language::ZhTw, "程式碼單元無法使用", "請勿盲目重複執行"),
] {
let directory = tempfile::tempdir().unwrap();
let log = Arc::new(
EventLog::open(&directory.path().join("events.sqlite"))
.await
.unwrap(),
);
let invocation = fixture::invocation("collected-cell-language");
log.create_session(&invocation.session_id, "create", &json!({}), 1)
.await
.unwrap();
fixture::accepted(&log, &invocation, &[]).await;
let run = RunTools::new(
language,
log.clone(),
invocation.clone(),
ToolCatalog::default(),
ToolMode::CodeMode,
CodeExecutor::new(CellLimits::default()).unwrap(),
Default::default(),
);
let finished = invoke(
&log,
&run,
&invocation,
"execute",
"exec",
json!({"code":"text('original 用户資料');"}),
)
.await;
let missing = invoke_result(
&log,
&run,
&invocation,
"observe-collected",
"wait",
json!({"cell_id":finished["cell_id"]}),
)
.await;
run.shutdown().await.unwrap();
drop(run);
fixture::close(log).await;
assert_eq!(finished["state"], "completed");
assert_eq!(
finished["content"],
json!([{"kind":"text","text":"original 用户資料"}])
);
let Err(ToolError::Failed(message)) = missing else {
panic!("waiting on the actually collected cell must fail")
};
assert!(
message.starts_with(unavailable),
"missing-cell recovery language differs: {message}"
);
assert!(
message.contains(caution),
"missing-cell recovery must warn against duplicate effects: {message}"
);
}
}