blob: 65d06a233444e3e636fd77fdcd34d72cbe008ad4 [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::*;
use maka_agent_facts::language::Language;
use maka_agent_facts::{
artifact::{ArtifactKind, ArtifactSource},
event::Invocation,
tool_output::ToolContent,
};
use maka_code_runtime::CellOutput;
use maka_tools::request::{BindingRequest, BoundTool, RequestBinding};
use maka_workspace_api::computer::{ReplState, ResultValue};
use std::collections::BTreeMap;
pub(super) fn title(method: &str, input: &Value) -> maka_agent_facts::display::Text {
use maka_agent_facts::display::Text;
if method == "reset" {
return Text::localized(
"Reset computer session",
"重置电脑操作环境",
"重設電腦操作環境",
);
}
if let Some(title) = input["title"].as_str().and_then(Text::single_line) {
return title;
}
Text::localized("Use computer", "操作电脑", "操作電腦")
}
pub(super) struct Binding {
pub service: Service,
pub workers: tokio_util::task::TaskTracker,
}
impl RequestBinding for Binding {
fn bind(
&self,
input: BindingRequest,
) -> futures_util::future::BoxFuture<'static, Result<BTreeMap<String, BoundTool>, ToolError>>
{
let service = self.service.clone();
let workers = self.workers.clone();
Box::pin(async move {
let request = service
.services
.request(
Identity::Agent {
invocation: input.invocation,
operation_id: None,
},
input.cancellation.clone(),
)
.map_err(ToolError::Failed)?;
let import = service.selection.import.clone();
let state = workers
.spawn(async move {
let result = AssertUnwindSafe(async {
request
.call(
&import,
0,
"state".into(),
serde_json::json!({}),
Attachments::default(),
)?
.await
})
.catch_unwind()
.await;
request
.finish()
.await
.map_err(ToolError::CleanupUnconfirmed)?;
result
.map_err(|_| ToolError::Failed("Computer state capture panicked".into()))?
.map_err(ToolError::Failed)
})
.await
.map_err(|_| {
ToolError::CleanupUnconfirmed("Computer state worker disappeared".into())
})??;
let state: ReplState = serde_json::from_value(state)
.map_err(|_| ToolError::Failed("Computer state invalid".into()))?;
let hint = match state {
ReplState::Fresh => {
"Current REPL state: fresh. Earlier transcript variables and target bindings are unavailable."
}
ReplState::Ready => {
"Current REPL state: ready. Existing variables and target bindings are retained in this Host process."
}
ReplState::ResetRequired => {
"Current REPL state: reset_required. Call cua_reset and rebind targets. Dispatched input may already have taken effect."
}
};
let name = service.selection.name.clone();
let description = format!("{}\n{}", service.selection.description, hint);
Ok(BTreeMap::from([(
name,
BoundTool::Described {
handler: Arc::new(service),
description,
},
)]))
})
}
}
pub(super) async fn project(
store: &EventLog,
invocation: &Invocation,
reply: OperationReply,
language: Language,
) -> Result<ToolSuccess, ToolError> {
// Validate original physical completion before interpreting business output.
mutation_result(reply.clone(), language)?;
let Receipt::Recorded {
outcome: OperationOutcome::Returned { value },
..
} = &reply.receipt
else {
unreachable!("validated receipt")
};
let result = serde_json::from_value(value.clone());
let raw = ToolOutput::Operation(Box::new(reply));
match parts(store, invocation, result, language).await {
Ok(parts) => Ok(ToolSuccess::content(raw, parts)),
// A display failure cannot erase a completed native effect or its original receipt.
Err(_) => Ok(ToolSuccess::projected(
raw,
maka_agent_facts::tool_output::DurableToolProjection::Failure,
)),
}
}
async fn parts(
store: &EventLog,
invocation: &Invocation,
result: Result<ResultValue, serde_json::Error>,
language: Language,
) -> Result<Vec<ToolContent>, ToolError> {
let result = result.map_err(|_| ToolError::Failed("Computer result invalid".into()))?;
let state_label = match language {
Language::En => "REPL state",
Language::ZhCn => "REPL 状态",
Language::ZhTw => "REPL 狀態",
};
let parts = match result {
ResultValue::Failed { message, state } => vec![ToolContent::Text(format!(
"{message}\n{state_label}: {}",
serde_json::to_string(&state).expect("state")
))],
ResultValue::Completed { artifact, state } => {
let bytes = store
.with_artifact(
maka_event_log::artifacts::ArtifactReadScope::Execution(invocation),
&artifact,
|record, reader| {
if record.source != ArtifactSource::SessionEffect
|| record.kind != ArtifactKind::File
|| record.mime_type.as_deref() != Some("application/json")
|| record.size_bytes > 50 * 1024 * 1024
{
return Err(ToolError::Failed(
"Computer output does not belong to the original invocation".into(),
));
}
let mut bytes = Vec::with_capacity(record.size_bytes as usize);
reader
.read_to_end(&mut bytes)
.map_err(|_| ToolError::Failed("Computer output unavailable".into()))?;
if bytes.len() as u64 != record.size_bytes {
return Err(ToolError::Failed("Computer output unavailable".into()));
}
Ok(bytes)
},
)
.await
.map_err(|error| ToolError::Persistence(error.to_string()))?
.ok_or_else(|| {
ToolError::Failed(
"Computer output does not belong to the original invocation".into(),
)
})??;
let outputs: Vec<CellOutput> = serde_json::from_slice(&bytes)
.map_err(|_| ToolError::Failed("Computer output invalid".into()))?;
if outputs
.iter()
.any(|output| matches!(output, CellOutput::Image { .. }))
{
return Err(ToolError::Failed(
"Computer output contains an unowned image reference".into(),
));
}
let mut parts: Vec<_> = outputs
.into_iter()
.map(|output| match output {
CellOutput::Text { text } => ToolContent::Text(text),
CellOutput::Media { content, detail } => ToolContent::Media { content, detail },
CellOutput::Image { .. } => unreachable!("stored images rejected below"),
})
.collect();
parts.push(ToolContent::Text(format!(
"{state_label}: {}",
serde_json::to_string(&state).expect("state")
)));
parts
}
};
Ok(parts)
}