blob: ee4bc16d69fbac1cf1d4b3ff7687c463df34e69e [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 crate::message::message;
use crate::tool_message_id;
use crate::{Content, Message, ProjectionError, ToolContent};
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::model::ModelPart;
use maka_agent_facts::model::ModelStep;
use maka_agent_facts::model::ToolInput;
use maka_agent_facts::tool_call::ToolCallIdentity;
use maka_agent_facts::tool_call::ToolOrigin;
use maka_agent_facts::tool_output::ToolOutput;
mod state;
use state::CallState;
pub(super) use state::Tools;
fn display_input(input: &ToolInput) -> serde_json::Value {
match input {
ToolInput::Json(value) => value.clone(),
ToolInput::InvalidJson { raw } => serde_json::Value::String(raw.clone()),
}
}
impl Tools {
pub fn accept(
&mut self,
event: &RuntimeEvent,
ts: u64,
step: &str,
output: &ModelStep,
text: Vec<Message>,
) -> Result<Vec<Message>, ProjectionError> {
let anchor = text.last().map(|message| message.id.clone());
let mut text = text.into_iter();
let mut rows = Vec::new();
for (index, part) in output.parts.iter().enumerate() {
if matches!(part, ModelPart::Text { .. }) {
rows.push(
text.next()
.ok_or(ProjectionError::Invalid("missing accepted text"))?,
);
} else {
rows.extend(self.accept_item(
event,
ts,
step,
index,
part,
anchor.as_deref(),
output.model.as_deref().unwrap_or_default(),
)?);
}
}
Ok(rows)
}
#[allow(clippy::too_many_arguments)]
pub fn accept_item(
&mut self,
event: &RuntimeEvent,
ts: u64,
step: &str,
index: usize,
part: &ModelPart,
anchor: Option<&str>,
model: &str,
) -> Result<Vec<Message>, ProjectionError> {
let mut rows = Vec::new();
match part {
ModelPart::Source { source } => {
let text = match source {
maka_agent_facts::model::ModelSource::Url { url, title, .. } => {
format!("Source: {}\n{}", title.as_deref().unwrap_or(""), url)
}
maka_agent_facts::model::ModelSource::Document {
title, filename, ..
} => format!("Source: {}\n{}", title, filename.as_deref().unwrap_or("")),
};
rows.push(message(
event,
ts,
format!("{}:source:{index}", event.id),
Content::Assistant {
imported: false,
text,
model_id: model.to_owned(),
interrupted: false,
thinking: None,
provider_options: None,
},
));
}
ModelPart::Text { .. } => {}
ModelPart::ToolCall { call } => {
let operation = format!("{step}:{}", call.id);
let identity = ToolCallIdentity::provider(step.into(), call.id.clone());
let metadata = self.insert(
&operation,
identity,
&call.name,
call.input.clone(),
if call.provider_executed {
CallState::ProviderExecuted
} else {
CallState::AwaitingDispatch
},
event,
)?;
rows.push(message(
event,
ts,
tool_message_id(&event.invocation.invocation_id, &operation),
Content::ToolCall {
tool_name: call.name.clone(),
args: display_input(&call.input),
step_id: anchor.map(str::to_owned),
provider_options: call.provider_options.clone(),
provider_executed: Some(call.provider_executed),
metadata,
},
));
}
ModelPart::ToolResult {
id,
name,
output,
is_error,
..
} => {
let operation = format!("{step}:{id}");
let pending = self
.pending
.get(&operation)
.ok_or(ProjectionError::Invalid("provider result without call"))?;
if pending.name != *name || pending.state != CallState::ProviderExecuted {
return Err(ProjectionError::Invalid("provider result name mismatch"));
}
rows.push(self.result(
event,
ts,
&operation,
maka_agent_facts::tool_call::provider_result_id(&event.id, index),
*is_error,
ToolContent::Json {
value: output.clone(),
},
)?);
}
}
Ok(rows)
}
pub fn boundary(
&mut self,
event: &RuntimeEvent,
ts: u64,
resolved: Option<&ToolOutput>,
) -> Result<Vec<Message>, ProjectionError> {
match &event.fact {
Fact::ToolDispatched {
operation_id,
call,
name,
..
}
| Fact::ToolRejected {
operation_id,
call,
name,
..
} => {
let rejected = matches!(event.fact, Fact::ToolRejected { .. });
let mut rows = Vec::new();
if matches!(call.origin, ToolOrigin::Provider { .. }) {
let pending =
self.pending
.get_mut(operation_id)
.ok_or(ProjectionError::Invalid(
"provider dispatch without accepted call",
))?;
let same_input = match &event.fact {
Fact::ToolDispatched { input, .. } => {
pending.input.as_json() == Some(input)
}
Fact::ToolRejected { input, .. } => pending.input == *input,
_ => unreachable!("tool admission fact"),
};
if pending.identity != *call
|| pending.name != *name
|| !same_input
|| pending.state != CallState::AwaitingDispatch
{
return Err(ProjectionError::Invalid(
"dispatch differs from accepted call",
));
}
pending.state = if rejected {
CallState::AwaitingDispatch
} else {
CallState::Dispatched
};
} else {
let input = match &event.fact {
Fact::ToolDispatched { input, .. } => ToolInput::Json(input.clone()),
Fact::ToolRejected { input, .. } => input.clone(),
_ => unreachable!("tool admission fact"),
};
let args = display_input(&input);
let metadata = self.insert(
operation_id,
call.clone(),
name,
input,
if rejected {
CallState::AwaitingDispatch
} else {
CallState::Dispatched
},
event,
)?;
rows.push(message(
event,
ts,
tool_message_id(&event.invocation.invocation_id, operation_id),
Content::ToolCall {
tool_name: name.clone(),
args,
step_id: None,
provider_options: None,
provider_executed: None,
metadata,
},
));
}
if let Fact::ToolDispatched {
title,
presentation,
..
} = &event.fact
{
if let Some(title) = title {
title.validate().map_err(ProjectionError::Invalid)?;
}
if let Some(key) = presentation {
maka_agent_facts::display::validate_presentation(key)
.map_err(ProjectionError::Invalid)?;
}
rows.push(message(
event,
ts,
format!("{}:activity", event.id),
Content::ToolActivity {
tool_use_id: tool_message_id(
&event.invocation.invocation_id,
operation_id,
),
tool_name: name.clone(),
title: title.clone(),
presentation: presentation.clone(),
},
));
}
if let Fact::ToolRejected { reason, .. } = &event.fact {
rows.push(self.result(
event,
ts,
operation_id,
event.id.clone(),
true,
ToolContent::Text {
text: reason.to_string(),
},
)?);
}
Ok(rows)
}
Fact::ToolSettled {
operation_id,
outcome,
} => {
if !self
.pending
.get(operation_id)
.is_some_and(|pending| pending.state == CallState::Dispatched)
{
return Err(ProjectionError::Invalid("result without dispatch"));
}
let (is_error, content) = ToolContent::from_outcome(outcome, resolved)?;
let mut rows = vec![self.result(
event,
ts,
operation_id,
event.id.clone(),
is_error,
content,
)?];
if let Some(ToolOutput::Model(result)) = resolved
&& let (Some(input), Some(output)) =
(result.usage.input_tokens, result.usage.output_tokens)
{
rows.push(message(
event,
ts,
maka_agent_facts::tool_call::metered_usage_id(&event.id),
Content::TokenUsage {
input,
output,
cache_read: result.usage.cache_read_tokens,
cache_creation: result.usage.cache_write_tokens,
reasoning: result.usage.reasoning_tokens,
},
));
}
Ok(rows)
}
_ => unreachable!(),
}
}
fn result(
&mut self,
event: &RuntimeEvent,
ts: u64,
operation: &str,
id: String,
is_error: bool,
content: ToolContent,
) -> Result<Message, ProjectionError> {
let pending = self
.pending
.remove(operation)
.ok_or(ProjectionError::Invalid("result without call"))?;
let row = message(
event,
ts,
id,
Content::ToolResult {
tool_use_id: tool_message_id(&event.invocation.invocation_id, operation),
settlement: match &event.fact {
Fact::ToolRejected { .. } => {
Some(maka_agent_facts::tool_call::ToolSettlement::Rejected)
}
Fact::ToolSettled { outcome, .. } => Some(match outcome {
maka_agent_facts::event::ToolOutcome::Succeeded { .. } => {
maka_agent_facts::tool_call::ToolSettlement::Returned
}
maka_agent_facts::event::ToolOutcome::Failed { .. } => {
maka_agent_facts::tool_call::ToolSettlement::Failed
}
maka_agent_facts::event::ToolOutcome::Unknown { .. } => {
maka_agent_facts::tool_call::ToolSettlement::Unknown
}
}),
_ => None,
},
is_error,
content,
metadata: pending.metadata,
},
);
Ok(row)
}
}