blob: dae2ccf935a895a0e68318558811077f25c0d543 [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::InvocationOutcome;
use maka_agent_facts::event::StoredEvent;
use maka_agent_facts::event::ToolOutcome;
use maka_agent_facts::tool_call::ToolCallIdentity;
use maka_agent_facts::tool_call::ToolOrigin;
use maka_agent_facts::tool_call::ToolRejection;
use maka_presentation::{InvocationView, tool_message_id};
use serde_json::{Value, json};
#[path = "tools/fixtures.rs"]
mod fixtures;
use fixtures::{opening, step, stored};
#[path = "tools/payloads.rs"]
mod payloads;
#[test]
fn malformed_arguments_display_exact_raw_and_cannot_alias_a_parsed_dispatch() {
use maka_agent_facts::model::{ModelEvent, ModelPart, ToolInput};
let raw = " {\"code\":\n";
let input = ToolInput::from_json(raw.into());
let mut facts = vec![opening()];
step(&mut facts, "step", false, false);
for fact in &mut facts {
match fact {
Fact::ModelObserved {
event: ModelEvent::ToolCall(call),
..
} => call.input = input.clone(),
Fact::ModelCompleted { output, .. } => {
for part in &mut output.parts {
if let ModelPart::ToolCall { call } = part {
call.input = input.clone();
}
}
}
_ => {}
}
}
let mut forged = facts.clone();
forged.push(Fact::ToolDispatched {
operation_id: "step:raw".into(),
call: ToolCallIdentity::provider("step".into(), "raw".into()),
name: "exec".into(),
input: json!({}),
title: None,
presentation: None,
});
let mut view = InvocationView::new();
let events = stored(forged);
for event in &events[..events.len() - 1] {
view.push(event).unwrap();
}
assert!(view.push(events.last().unwrap()).is_err());
facts.push(Fact::ToolRejected {
operation_id: "step:raw".into(),
call: ToolCallIdentity::provider("step".into(), "raw".into()),
name: "exec".into(),
input,
reason: ToolRejection::InvalidInput {
message: "arguments are not valid JSON".into(),
},
});
let mut view = InvocationView::new();
let rows: Vec<_> = stored(facts)
.iter()
.flat_map(|event| view.push(event).unwrap())
.collect();
let calls: Vec<_> = rows
.iter()
.filter_map(|row| match &row.message.content {
maka_presentation::Content::ToolCall { args, .. } => Some(args),
_ => None,
})
.collect();
assert_eq!(calls, [&json!(raw)]);
assert!(rows.iter().any(|row| matches!(&row.message.content,
maka_presentation::Content::ToolResult { is_error: true, content: maka_presentation::ToolContent::Text { text }, .. }
if text == "invalid tool input: arguments are not valid JSON")));
}
#[test]
fn accepted_tools_replay_with_distinct_ids_parentage_rejections() {
let mut facts = vec![opening()];
step(&mut facts, "first", true, false);
facts.push(Fact::ToolDispatched {
presentation: None,
title: Some(maka_agent_facts::display::Text::plain(
"Inspect application",
)),
operation_id: "first:raw".into(),
call: ToolCallIdentity::provider("first".into(), "raw".into()),
name: "exec".into(),
input: json!({"code":"Read()"}),
});
facts.push(Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "child".into(),
call: ToolCallIdentity {
tool_call_id: "nested".into(),
origin: ToolOrigin::CodeMode {
parent_operation_id: "first:raw".into(),
parent_tool_call_id: "raw".into(),
},
},
name: "Read".into(),
input: json!({"path":"file"}),
});
facts.push(Fact::ToolSettled {
operation_id: "child".into(),
outcome: success(&json!({"content":"file text"}).into()),
});
facts.push(Fact::ToolSettled {
operation_id: "first:raw".into(),
outcome: success(&json!({"status":"completed"}).into()),
});
step(&mut facts, "second", false, false);
facts.push(Fact::ToolRejected {
operation_id: "second:raw".into(),
call: ToolCallIdentity::provider("second".into(), "raw".into()),
name: "exec".into(),
input: json!({"code":"Read()"}).into(),
reason: ToolRejection::Unavailable,
});
step(&mut facts, "native", false, true);
step(&mut facts, "unknown", false, false);
facts.push(Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "unknown:raw".into(),
call: ToolCallIdentity::provider("unknown".into(), "raw".into()),
name: "exec".into(),
input: json!({"code":"Read()"}),
});
facts.push(Fact::InvocationEnded {
outcome: InvocationOutcome::Failed {
model_failure: None,
class: "unknown_outcome".into(),
message: None,
},
});
let events = stored(facts);
let mut view = InvocationView::new();
let mut replay = InvocationView::new();
let mut rows = Vec::new();
for stored in &events {
let raw = match &stored.event.fact {
Fact::ToolSettled {
operation_id,
outcome: ToolOutcome::Succeeded { .. },
} => Some(
if operation_id == "child" {
json!({"content":"file text"})
} else {
json!({"status":"completed"})
}
.into(),
),
_ => None,
};
let next = view.push_with_tool_output(stored, raw.as_ref()).unwrap();
let roundtrip = StoredEvent {
sequence: stored.sequence,
event: serde_json::from_value(serde_json::to_value(&stored.event).unwrap()).unwrap(),
};
assert_eq!(
next,
replay
.push_with_tool_output(&roundtrip, raw.as_ref())
.unwrap()
);
if matches!(stored.event.fact, Fact::ModelObserved { .. }) {
assert!(next.is_empty());
assert!(
view.overlay()
.iter()
.all(|message| serde_json::to_value(message).unwrap()["type"] == "assistant")
);
}
rows.extend(next);
}
assert!(
rows.windows(2)
.all(|pair| pair[0].position < pair[1].position)
);
let messages: Vec<Value> = rows
.iter()
.map(|row| serde_json::to_value(&row.message).unwrap())
.collect();
let activity = messages
.iter()
.find(|row| row["type"] == "tool_activity")
.unwrap();
assert_eq!(activity["title"]["fallback"], "Inspect application");
assert_eq!(
activity["toolUseId"],
tool_message_id("invocation", "first:raw")
);
let calls: Vec<_> = messages
.iter()
.filter(|message| message["type"] == "tool_call")
.collect();
assert_eq!(
calls.len(),
5,
"dispatch never duplicates the accepted call"
);
assert_eq!(calls[0]["id"], tool_message_id("invocation", "first:raw"));
assert_eq!(
calls[0]["stepId"],
messages
.iter()
.find(|row| row["type"] == "assistant")
.unwrap()["id"]
);
assert_eq!(calls[0]["modelVisibility"], "visible");
assert_eq!(calls[1]["parentToolCallId"], calls[0]["id"]);
assert_eq!(calls[1]["parentOperationId"], "first:raw");
assert_eq!(calls[1]["modelVisibility"], "hidden");
assert_ne!(calls[0]["id"], calls[2]["id"]);
assert!(
calls[2].get("stepId").is_none(),
"pure tool step has no empty assistant anchor"
);
let results: Vec<_> = messages
.iter()
.filter(|message| message["type"] == "tool_result")
.collect();
assert_eq!(results.len(), 4, "unknown outcome produces no result");
assert_eq!(
messages
.iter()
.filter(|row| row["type"] == "tool_activity")
.count(),
3,
"each original dispatch is visible even without display metadata; rejection is not dispatch"
);
assert_eq!(results[0]["settlement"], "returned");
assert_eq!(results[1]["settlement"], "returned");
assert_eq!(results[2]["settlement"], "rejected");
assert!(
results[3].get("settlement").is_none(),
"provider result is not a Host settlement"
);
assert_eq!(results[0]["toolUseId"], calls[1]["id"]);
assert_eq!(
results[0]["content"],
json!({"kind":"json","value":{"content":"file text"}})
);
assert_eq!(results[2]["toolUseId"], calls[2]["id"]);
assert_eq!(results[2]["isError"], true);
assert_eq!(results[2]["content"]["kind"], "text");
for result in &results[..3] {
let fact = events
.iter()
.find(|stored| stored.event.id == result["id"])
.unwrap();
assert_eq!(result["ts"], 999 + fact.sequence);
}
assert_eq!(results[3]["toolUseId"], calls[3]["id"]);
}
fn success(output: &maka_agent_facts::tool_output::ToolOutput) -> ToolOutcome {
let template = stored(vec![opening()]).remove(0).event;
let write = EventWrite::tool_success(
template.id,
template.recorded_at,
template.invocation,
"operation".into(),
output.clone().into(),
)
.unwrap()
.0;
match &write.event().fact {
Fact::ToolSettled { outcome, .. } => outcome.clone(),
_ => unreachable!(),
}
}