blob: 5f15c6d8989b961888a0ffb8dcb9124b7d51492b [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 futures_util::future::BoxFuture;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use crate::CodeExecutor;
use maka_agent_facts::event::EventSink;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::language::Language;
use maka_agent_facts::model::ModelToolCall;
use maka_agent_facts::tool_call::ToolCallIdentity;
use maka_agent_facts::tool_call::ToolRejection;
use maka_agent_facts::tools::ToolError;
use maka_agent_facts::tools::ToolJournal;
use serde_json::Value;
use tokio_util::sync::CancellationToken;
use crate::availability::{Availability, LoadedTools, SEARCH};
use crate::{
PreparedEffect, ToolCallContext, ToolCatalog, ToolDefinition, ToolMode, ToolSemantics, cell,
};
/// Run-scoped authority. Cell capacity is shared across runs, not recreated here.
pub struct RunTools {
language: Language,
model: Option<maka_agent_facts::tools::ModelToolContext>,
journal: ToolJournal,
availability: Availability,
mode: ToolMode,
cells: CodeExecutor,
code: cell::Cells,
}
impl Drop for RunTools {
fn drop(&mut self) {
self.code.cancel();
}
}
impl RunTools {
pub fn new(
language: Language,
sink: Arc<dyn EventSink>,
invocation: Invocation,
catalog: ToolCatalog,
mode: ToolMode,
cells: CodeExecutor,
loaded: LoadedTools,
) -> Self {
Self {
language,
model: None,
journal: ToolJournal::new(sink, invocation),
availability: Availability::new(catalog, loaded),
mode,
cells,
code: cell::Cells::default(),
}
}
pub fn with_store(mut self, store: maka_code_runtime::CellStore) -> Self {
self.code = cell::Cells::new(store);
self
}
pub async fn shutdown(&self) -> Result<(), ToolError> {
self.code.shutdown().await
}
pub fn code_idle(&self) -> bool {
self.code.is_idle()
}
pub async fn finish_output(&self, observed: u64) -> Result<bool, ToolError> {
self.code.finish_output(observed).await
}
pub fn with_model(mut self, model: maka_agent_facts::tools::ModelToolContext) -> Self {
self.set_model(model);
self
}
pub fn with_behavior(mut self, behavior: maka_agent_facts::execution::BehaviorId) -> Self {
self.availability.set_behavior(behavior);
self
}
pub fn set_model(&mut self, model: maka_agent_facts::tools::ModelToolContext) {
self.model = Some(model);
}
pub fn checkpoint(&self) -> maka_agent_facts::handoff::HandoffTools {
self.availability.checkpoint()
}
pub fn restore(
&self,
checkpoint: &maka_agent_facts::handoff::HandoffTools,
) -> Result<(), ToolError> {
self.availability.restore(checkpoint)
}
/// Capture once per logical model step, before sending any physical attempt.
pub async fn capture(
&self,
cwd: &str,
cancellation: CancellationToken,
) -> Result<RequestTools<'_>, ToolError> {
self.code.check()?;
let current = self
.availability
.capture(
self.model.clone(),
self.journal.invocation().clone(),
cwd.into(),
cancellation,
)
.await?;
Ok(self.request(current))
}
/// Replay seals settled history and registered capabilities, not live state.
/// Each model step captures live bindings after invocation admission.
pub fn replay_definitions(&self) -> Vec<ToolDefinition> {
self.request(self.availability.clone()).definitions()
}
fn request(&self, current: Availability) -> RequestTools<'_> {
let digest = current.digest();
let direct = if self.mode == ToolMode::CodeMode {
current.direct_only()
} else {
ToolCatalog::default()
};
let availability = if self.mode == ToolMode::CodeMode {
current.nested()
} else {
current
};
let described = availability.snapshot();
RequestTools {
finished: Arc::new(AtomicBool::new(false)),
digest,
direct,
run: self,
catalog: if self.mode == ToolMode::CodeMode {
availability.all()
} else {
described.clone()
},
described,
availability,
}
}
}
/// The advertised schemas and their handlers share one captured capability view.
/// Search settlement affects future captures, never this request or its retries.
#[derive(Clone)]
pub struct RequestTools<'a> {
finished: Arc<AtomicBool>,
digest: String,
direct: ToolCatalog,
run: &'a RunTools,
catalog: ToolCatalog,
/// Frozen description selection, separate from Code Mode callability.
described: ToolCatalog,
availability: Availability,
}
impl<'a> RequestTools<'a> {
pub fn finished(&self) -> bool {
self.finished.load(Ordering::Acquire)
}
pub fn catalog_digest(&self) -> &str {
&self.digest
}
pub fn definitions(&self) -> Vec<ToolDefinition> {
if self.run.mode == ToolMode::CodeMode {
let mut exec = cell::definition(self.run.cells.language());
let definitions: Vec<_> = self.described.definitions().cloned().collect();
exec.description.push('\n');
exec.description
.push_str(&cell::declarations(&definitions, self.run.cells.language()));
std::iter::once(exec)
.chain(std::iter::once(cell::wait_definition()))
.chain(self.direct.definitions().cloned())
.collect()
} else {
let mut definitions: Vec<_> = self.catalog.definitions().cloned().collect();
definitions.extend(self.availability.definition());
definitions
}
}
pub fn into_step(self, step_id: &str) -> StepTools<'a> {
StepTools {
finished: self.finished.clone(),
request: self,
step_id: step_id.to_owned(),
admission: Admission::Fresh,
}
}
}
/// Call-order admission is separate from execution scheduling. Serial execution
/// alone does not make exclusive-step siblings legal.
pub struct StepTools<'a> {
finished: Arc<AtomicBool>,
request: RequestTools<'a>,
step_id: String,
admission: Admission,
}
#[derive(Default)]
enum Admission {
#[default]
Fresh,
Parallel,
Exclusive,
}
impl Admission {
fn admit(&mut self, semantics: ToolSemantics) -> Result<(), ToolRejection> {
*self = match (&self, semantics) {
(Self::Fresh, ToolSemantics::ExclusiveStep | ToolSemantics::FinishTurn) => {
Self::Exclusive
}
(Self::Fresh | Self::Parallel, ToolSemantics::Parallel) => Self::Parallel,
_ => return Err(ToolRejection::ExclusiveConflict),
};
Ok(())
}
}
impl<'a> StepTools<'a> {
pub fn finished(&self) -> bool {
self.finished.load(Ordering::Acquire)
}
pub async fn invoke(
&mut self,
call: &ModelToolCall,
cancellation: CancellationToken,
) -> Result<Value, ToolError> {
self.dispatch(call.clone(), cancellation).await
}
/// Admission happens in arrival order before any concurrent future runs.
pub fn dispatch(
&mut self,
call: ModelToolCall,
cancellation: CancellationToken,
) -> BoxFuture<'a, Result<Value, ToolError>> {
let request = self.request.clone();
let run = request.run;
let operation_id = format!("{}:{}", self.step_id, call.id);
let identity = ToolCallIdentity::provider(self.step_id.clone(), call.id.clone());
let finished = self.finished.clone();
let catalog = if run.mode == ToolMode::CodeMode {
&request.direct
} else {
&request.catalog
};
let semantics =
if run.mode == ToolMode::CodeMode && matches!(call.name.as_str(), "exec" | "wait") {
Ok(ToolSemantics::ExclusiveStep)
} else if run.mode == ToolMode::Direct
&& call.name == SEARCH
&& request.availability.enabled()
{
Ok(ToolSemantics::Parallel)
} else {
catalog.semantics(&call.name)
};
let admission = if cancellation.is_cancelled() {
Err(ToolRejection::Cancelled)
} else {
semantics.and_then(|semantics| self.admission.admit(semantics))
};
Box::pin(async move {
run.code.check()?;
let catalog = if run.mode == ToolMode::CodeMode {
&request.direct
} else {
&request.catalog
};
let preparation: Result<(PreparedEffect, Value), ToolRejection> = async {
if cancellation.is_cancelled() {
return Err(ToolRejection::Cancelled);
}
admission?;
let input = call
.input
.as_json()
.ok_or_else(|| ToolRejection::InvalidInput {
message: "arguments are not valid JSON".into(),
})?;
let effect = if call.name == "exec" && run.mode == ToolMode::CodeMode {
let executor = cell::CellTool::new(
run.cells.clone(),
cell::source(input, run.cells.language())?,
request.catalog.clone(),
run.journal.clone(),
operation_id.clone(),
call.id.clone(),
);
let code = run.code.clone();
let language = run.language;
let effect: PreparedEffect = PreparedEffect::new(move |cancellation| {
Box::pin(async move { code.start(executor, language, cancellation).await })
});
Ok(effect.titled(maka_agent_facts::display::Text::localized(
"Run tools",
"执行工具",
"執行工具",
)))
} else if call.name == "wait" && run.mode == ToolMode::CodeMode {
let input = cell::wait_input(input)?;
let code = run.code.clone();
let language = run.language;
Ok(PreparedEffect::new(move |cancellation| {
Box::pin(async move { code.observe(input, language, cancellation).await })
}))
} else if run.mode == ToolMode::Direct
&& call.name == SEARCH
&& request.availability.enabled()
{
request.availability.prepare_search(input)
} else {
catalog
.prepare(
call.name.clone(),
input.clone(),
ToolCallContext {
invocation: run.journal.invocation().clone(),
operation_id: operation_id.clone(),
},
cancellation.clone(),
)
.await
}?;
Ok((effect, input.clone()))
}
.await;
let (effect, input) = match preparation {
Ok(effect) => effect,
Err(reason) => {
return run
.journal
.reject(
operation_id,
identity,
call.name.clone(),
call.input.clone(),
reason,
)
.await;
}
};
let result = run
.journal
.invoke_prepared_call(
operation_id,
identity,
call.name.clone(),
input,
cancellation,
effect,
)
.await?;
if catalog.semantics(&call.name) == Ok(ToolSemantics::FinishTurn) {
finished.store(true, Ordering::Release);
}
request.availability.settled(&call.name, &result)?;
Ok(result)
})
}
}