blob: e5c1dc4393bc19d5a49508b874ad8b546feb830c [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::{
Configuration, IMPLEMENTATION, NativeContext, configuration_schema, contract::*, workspace,
};
use futures_util::future::BoxFuture;
use maka_agent_engine::{Engine, RunError, RunInput, RunWork};
mod assistant;
mod attachments;
mod auxiliary;
mod compaction;
mod context;
mod execution;
mod files;
mod fork;
mod goal;
mod graph;
mod imports;
mod interactions;
mod lifecycle;
mod notifications;
mod plan;
mod preferences;
mod projects;
pub(crate) use notifications::ReturnRoute;
mod git;
mod global_instructions;
mod patches;
mod queue;
mod recall;
mod recap;
mod runs;
mod scheduler;
mod skills;
mod title;
mod todo;
mod work;
mod worktrees;
use maka_agent_facts::composition::SourceKind;
use maka_agent_facts::composition::SourceRevision;
use maka_agent_facts::configuration::policy::RuntimePolicySnapshot;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::execution::*;
use maka_agent_facts::input::DeliveredMessage;
use maka_agent_facts::input::MessageInput;
use maka_agent_facts::language::{Language, LanguagePreference};
use maka_agent_facts::message::*;
use maka_config::ConfigurationStore;
use maka_event_log::{EventLog, sessions::SessionRecord};
use maka_model::ModelExecutor;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
time::Duration,
};
use tokio::sync::Mutex;
use tokio_util::{sync::CancellationToken, task::TaskTracker};
use uuid::Uuid;
use maka_agent_api::session::SessionConfiguration;
/// Frozen accepted message prerequisites, interpreted only by this Agent.
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct MessageBasis {
#[serde(default, skip_serializing_if = "Option::is_none")]
compaction_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
graph: Option<maka_graph::Epoch>,
#[serde(default, skip_serializing_if = "is_agent_collaboration")]
collaboration_mode: CollaborationMode,
#[serde(default, skip_serializing_if = "BackgroundKind::is_schedule")]
background_kind: BackgroundKind,
#[serde(default)]
tool_discovery: bool,
#[serde(default)]
allowed_tools: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
background: Option<ScheduledTarget>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
internal_tools: Vec<maka_agent_facts::tools::ToolDefinition>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
internal_bindings: BTreeMap<String, Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
behavior: Option<SystemPrompt>,
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<crate::model::AcceptedModel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
executor: Option<crate::executor::Accepted>,
workspace: Workspace,
fingerprint: String,
tools: Vec<crate::tools::SelectedTool>,
#[serde(default, skip_serializing_if = "Language::is_english")]
language: Language,
}
fn is_agent_collaboration(mode: &CollaborationMode) -> bool {
*mode == CollaborationMode::Agent
}
#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum BackgroundKind {
#[default]
Schedule,
Goal(Uuid),
Plan(String),
}
impl BackgroundKind {
fn is_schedule(&self) -> bool {
*self == Self::Schedule
}
}
enum Selection {
Model(ModelSelection, Option<ThinkingLevel>),
Executor(maka_agent_api::executor::Selection),
}
struct Submission {
id: String,
text: String,
attachments: Vec<String>,
selection: Selection,
}
enum PreparedBackend {
Model(Box<(crate::model::Source, maka_agent_engine::PreparedModel)>),
Executor(Box<crate::executor::Backend>),
}
struct Control {
compacting: bool,
invocation: Invocation,
basis: MessageBasis,
cancellation: CancellationToken,
failure: Option<&'static str>,
}
#[derive(Default)]
struct Runs {
closing: bool,
controls: BTreeMap<String, Control>,
/// Sessions whose original execution has not confirmed physical cleanup.
quarantined: BTreeSet<String>,
}
pub(crate) struct Owner {
graphs: Option<Arc<crate::workhub::tasks::graph::Graphs>>,
pub store: Arc<EventLog>,
pub recall: Arc<recall::Recall>,
scheduler: Arc<scheduler::Scheduler>,
todo: Arc<todo::Todo>,
goals: Arc<goal::Goals>,
plans: Arc<plan::Plans>,
recap_storage: Arc<maka_host_runtime::storage::Storage>,
auxiliary: auxiliary::Auxiliary,
pub settings: Configuration,
pub prefix: String,
pub data_source: DataSource,
pub(crate) configuration: Arc<ConfigurationStore>,
pub(crate) catalog: Arc<maka_providers::catalog::Catalog>,
credentials: crate::model::Credentials,
revision: SourceRevision,
behavior: Option<BuiltinBehavior>,
configured_locale: LanguagePreference,
native_language: Language,
pub workspace_roots: Vec<workspace::Root>,
instruction_home: Option<Arc<cap_std::fs::Dir>>,
pub import_roots: Vec<workspace::Root>,
internal_tools: Vec<maka_agent_facts::tools::ToolDefinition>,
internal_bindings: BTreeMap<String, Option<String>>,
managed_worktrees: std::path::PathBuf,
engine: Engine,
tools: maka_tools::ToolCatalog,
tool_basis: Vec<crate::tools::SelectedTool>,
editing_tools: Vec<(String, maka_agent_facts::execution::EditingTools)>,
planning_tools: Vec<String>,
skills: Option<skills::Library>,
executors: crate::executor::Executors,
runs: Arc<runs::Registry>,
workers: TaskTracker,
producers: TaskTracker,
producer_gate: Arc<Mutex<()>>,
#[cfg(test)]
title_write_gate: std::sync::Mutex<Option<Arc<title::WriteGate>>>,
#[cfg(all(test, target_os = "macos"))]
file_native_hints: std::sync::atomic::AtomicBool,
stop: CancellationToken,
}
#[derive(Clone, Copy)]
pub(crate) enum BuiltinBehavior {
WorkHub,
}
impl Owner {
pub(crate) fn resolve_language(&self, snapshot: &RuntimePolicySnapshot) -> Language {
snapshot
.policy
.language
.unwrap_or(self.configured_locale)
.resolve(self.native_language)
}
pub(crate) async fn executor_setup(
&self,
input: maka_agent_api::executor::setup::Input,
context: maka_host_runtime::application::Context,
) -> Result<Value, String> {
self.executors.setup(input, context).await
}
pub async fn open(
context: NativeContext,
input: Value,
catalog: Arc<maka_providers::catalog::Catalog>,
) -> Result<Arc<Self>, String> {
Self::open_behavior(context, input, None, vec![], BTreeMap::new(), None, catalog).await
}
pub(crate) async fn open_behavior(
context: NativeContext,
input: Value,
behavior: Option<BuiltinBehavior>,
mut additional: Vec<maka_agent_facts::tools::ToolRegistration>,
internal_bindings: BTreeMap<String, Option<String>>,
graphs: Option<Arc<crate::workhub::tasks::graph::Graphs>>,
catalog: Arc<maka_providers::catalog::Catalog>,
) -> Result<Arc<Self>, String> {
let implementation = match behavior {
Some(BuiltinBehavior::WorkHub) => maka_agent_api::workhub::IMPLEMENTATION,
None => IMPLEMENTATION,
};
if !jsonschema_valid(&input) {
return Err("agent_configuration_invalid".into());
}
let settings: Configuration =
serde_json::from_value(input).map_err(|_| "agent_configuration_invalid")?;
let persistence = context.persistence().ok_or("agent_requires_persistence")?;
let root = persistence
.store()
.owned_root()
.map_err(|_| "agent_root_unavailable")?;
let data_source = DataSource {
state_domain: persistence.source().state_domain.clone(),
deployment: persistence.source().deployment,
namespace: context.activation().instance().clone(),
};
let prefix = maka_agent_authority::session_namespace(
&persistence.source().state_domain,
persistence.source().deployment,
context.activation().instance(),
implementation != IMPLEMENTATION,
);
let managed_worktrees = root.canonical_path().join("subagent-worktrees");
let workspace_roots =
workspace::open(settings.workspace_roots.clone(), &managed_worktrees)?;
let import_roots = if settings.import_roots.is_empty() {
Vec::new()
} else {
workspace::open(settings.import_roots.clone(), root.canonical_path())?
};
let cells = maka_tools::CodeExecutor::new(maka_code_runtime::CellLimits::default())
.map_err(|_| "agent_executor_unavailable")?;
let workers = TaskTracker::new();
let stop = CancellationToken::new();
let revision = SourceRevision {
kind: SourceKind::ModelProvider,
name: implementation.into(),
package_id: context.package().id.clone(),
entry_id: implementation.into(),
activation: context.activation().epoch().to_string(),
revision: context.package().revision.clone(),
};
// Match Codex's five-minute stream idle allowance for slow model steps.
let models = ModelExecutor::for_source(Duration::from_secs(300), revision.clone())
.map_err(|_| "agent_executor_unavailable")?;
let auxiliary = auxiliary::Auxiliary::new(revision.clone())?;
let configured_locale = LanguagePreference::from_locale(&settings.locale);
let native_language = maka_config::language::native_language();
let skills = skills::Library::select(&context, &settings, revision.clone())?;
let instruction_home = assistant::open_home(settings.assistant.as_ref())?;
let executors = crate::executor::Executors::capture(&context, &settings.executors)?;
let configuration = Arc::new(
ConfigurationStore::for_root(root)
.await
.map_err(|error| {
tracing::error!(stage = "configuration_store_open", cause = crate::diagnostics::config_cause(&error),
activation = %context.activation().epoch(), instance = ?context.activation().instance(),
"Agent configuration store failed to open");
"agent_configuration_unavailable"
})?,
);
let recall = Arc::new(recall::Recall {
store: persistence.store().clone(),
configuration: configuration.clone(),
prefix: prefix.clone(),
data_source: data_source.clone(),
enabled: settings.recall,
stop: stop.clone(),
workers: workers.clone(),
services: context.services(),
});
if settings.recall {
additional.extend(recall.tools());
}
let scheduler = Arc::new(scheduler::Scheduler::new(
persistence.store().clone(),
prefix.clone(),
));
if settings.scheduled_tasks {
additional.extend(scheduler.tools());
}
let plans = Arc::new(plan::Plans::new(
persistence.store().clone(),
prefix.clone(),
));
if settings.plans {
additional.extend(plans.tools());
}
let goals = Arc::new(goal::Goals::new(
persistence.store().clone(),
prefix.clone(),
));
if settings.goals {
additional.extend(goals.tools());
}
let todo = Arc::new(todo::Todo::new(persistence.store().clone(), prefix.clone()));
let recap_storage = Arc::new(maka_host_runtime::storage::Storage::new(
persistence.store().clone(),
prefix.clone(),
maka_capabilities::storage::Namespace::new(
"maka.session-recap",
maka_plugin_core::scope::Scope::Profile,
)
.expect("recap namespace"),
));
if settings.todo {
additional.extend(todo.tools());
}
let internal_tools = additional
.iter()
.map(|tool| tool.definition.clone())
.collect();
let prepared_tools = crate::tools::catalog(
context.services(),
persistence.store().clone(),
configuration.clone(),
&settings.tools,
additional,
workers.clone(),
)
.and_then(|mut catalog| {
if settings.plans {
catalog.planning.push("SubmitPlan".into());
catalog.tools = catalog
.tools
.with_always_visible(&["SubmitPlan", "update_plan", "cancel_plan"])
.and_then(|tools| {
tools.with_request_binding(
["SubmitPlan", "update_plan", "cancel_plan"]
.into_iter()
.map(str::to_owned)
.collect(),
plans.clone(),
)
})
.map_err(|error| error.to_string())?;
}
if graphs.as_ref().is_some_and(|graphs| graphs.enabled()) {
catalog.tools = catalog
.tools
.with_always_visible(crate::workhub::tasks::graph::NAMES)
.map_err(|error| error.to_string())?;
}
if settings.goals {
catalog.tools = catalog
.tools
.with_always_visible(&["GoalStatus"])
.map_err(|error| error.to_string())?;
}
if settings.recall {
catalog
.planning
.extend(["Recall", "RecallMore", "RecallMaterial"].map(str::to_owned));
catalog.tools = catalog
.tools
.with_request_binding(
maka_agent_facts::tools::ToolPreparer::names(recall.as_ref())
.into_iter()
.collect(),
recall.clone(),
)
.map_err(|error| error.to_string())?;
}
Ok(catalog)
});
let crate::tools::Catalog {
tools,
basis: tool_basis,
editing: editing_tools,
planning: planning_tools,
} = match prepared_tools {
Ok(tools) => tools,
Err(error) => {
configuration
.shutdown()
.await
.map_err(|error| {
tracing::error!(stage = "configuration_store_shutdown", cause = crate::diagnostics::config_cause(&error),
activation = %context.activation().epoch(), instance = ?context.activation().instance(),
"Agent configuration store cleanup failed");
"agent_configuration_cleanup_unconfirmed"
})?;
return Err(error);
}
};
let credentials = crate::model::Credentials {
executor: models.clone(),
refresh: Arc::new(crate::model::oauth::Authority::new(
workers.clone(),
stop.clone(),
)),
};
let engine = Engine::with_pricing(
persistence.store().clone(),
models,
cells,
Arc::new(crate::model::Prices(configuration.clone())),
);
Ok(Arc::new(Self {
graphs,
plans,
goals,
auxiliary,
todo,
recap_storage,
scheduler,
recall,
store: persistence.store().clone(),
prefix,
data_source,
workspace_roots,
import_roots,
managed_worktrees,
revision,
behavior,
configured_locale,
native_language,
instruction_home,
internal_tools,
internal_bindings,
settings,
configuration,
catalog,
credentials,
engine,
tools,
tool_basis,
editing_tools,
planning_tools,
skills,
executors,
runs: Arc::default(),
workers,
producers: TaskTracker::new(),
producer_gate: Arc::default(),
#[cfg(test)]
title_write_gate: std::sync::Mutex::default(),
#[cfg(all(test, target_os = "macos"))]
file_native_hints: std::sync::atomic::AtomicBool::new(true),
stop,
}))
}
pub fn operation_id(&self, operation: &maka_plugin_core::operation::RecoveryRef) -> String {
// Use the actual complete original reference. UUIDs alone do not name
// operations across provider namespaces or deployments. The Host journal
// still rejects conflicting reuse inside its original operation namespace.
let bytes = serde_json::to_vec(operation).expect("admitted operation reference");
let digest = hex::encode(Sha256::digest(bytes));
format!("{}{}", self.prefix, &digest[..32])
}
pub fn owns(&self, id: &str) -> bool {
owns_session(&self.prefix, id)
}
pub async fn require_session(&self, id: &str) -> Result<(), String> {
if !self.owns(id) {
return Err("agent_session_unavailable".into());
}
if self
.store
.session_is_available(id)
.await
.map_err(|_| "agent_store_unavailable".to_owned())?
{
Ok(())
} else {
Err("agent_session_unavailable".into())
}
}
pub async fn session(&self, id: &str) -> Result<SessionRecord<SessionConfiguration>, String> {
if !self.owns(id) {
return Err("agent_session_unavailable".into());
}
self.store
.get_session(id)
.await
.map_err(|_| "agent_store_unavailable".to_owned())?
.ok_or("agent_session_unavailable".into())
}
pub async fn create(
self: &Arc<Self>,
input: Create,
operation: &maka_plugin_core::operation::RecoveryRef,
scope: &maka_capabilities::call::Scope,
) -> Result<Created, String> {
let operation = operation.clone();
self.resource_work(scope, move |owner, scope| {
Box::pin(async move { owner.create_inner(input, &operation, &scope).await })
})
.await
}
async fn create_inner(
&self,
input: Create,
operation: &maka_plugin_core::operation::RecoveryRef,
scope: &maka_capabilities::call::Scope,
) -> Result<Created, String> {
if let Some(reason) = metadata_rejection(&input.title, &input.labels) {
return Ok(Created::Rejected { reason });
}
let runs = self.runs.lock().await;
if runs.closing {
return Ok(Created::Rejected {
reason: Rejection::Closed,
});
}
let mut tools = input.tools;
if let Some(names) = &mut tools {
names.sort();
if names.windows(2).any(|pair| pair[0] == pair[1])
|| names.iter().any(|name| !self.tools.contains(name))
{
return Ok(Created::Rejected {
reason: Rejection::ToolsUnavailable,
});
}
}
let selected = match input.workspace {
WorkspaceSelection::IsolatedGit { selection } => {
let Some(workspace) = self.isolated_workspace(selection, operation, scope).await?
else {
return Ok(Created::Rejected {
reason: Rejection::WorkspaceUnavailable,
});
};
workspace
}
WorkspaceSelection::Project { project, selection } => {
let Some(workspace) = self.select_project(project, selection).await? else {
return Ok(Created::Rejected {
reason: Rejection::WorkspaceUnavailable,
});
};
workspace
}
WorkspaceSelection::Directory { selection } => {
let Some(workspace) = self.create_project_workspace(selection).await? else {
return Ok(Created::Rejected {
reason: Rejection::WorkspaceUnavailable,
});
};
workspace
}
WorkspaceSelection::Session { session_id } => {
if !self.owns(&session_id) {
return Ok(Created::Rejected {
reason: Rejection::SessionUnavailable,
});
}
let Some(session) = self
.store
.get_session::<SessionConfiguration>(&session_id)
.await
.map_err(|_| "agent_store_unavailable")?
else {
return Ok(Created::Rejected {
reason: Rejection::SessionUnavailable,
});
};
if session.archived
|| !self
.project_accepts_session(&session.configuration.workspace)
.await?
{
return Ok(Created::Rejected {
reason: Rejection::SessionUnavailable,
});
}
if workspace::validate(
&self.workspace_roots,
&session.configuration.workspace,
&self.managed_worktrees,
)
.await
.is_err()
{
return Ok(Created::Rejected {
reason: Rejection::WorkspaceUnavailable,
});
}
if let Some(parent) = session.configuration.tools {
tools = Some(match tools {
None => parent,
Some(names) => names
.into_iter()
.filter(|name| parent.contains(name))
.collect(),
});
}
session.configuration.workspace
}
};
let id = self.operation_id(operation);
let record = self
.store
.create_session(
&id,
&operation.fingerprint,
&SessionConfiguration {
backend: None,
collaboration_mode: CollaborationMode::Agent,
tools,
title: input.title,
title_is_manual: !input.automatic_title,
labels: input.labels,
workspace: selected,
permissions: self.settings.permissions.clone(),
boundary_revision: 0,
},
now(),
)
.await
.map_err(|_| "agent_creation_failed")?;
Ok(Created::Created {
session: Box::new(crate::service::summary(self, record)),
})
}
pub async fn mark_read(&self, input: MarkRead) -> Result<MarkReadResult, String> {
let runs = self.runs.lock().await;
if runs.closing {
return Ok(MarkReadResult::Rejected {
reason: Rejection::Closed,
});
}
if !self.owns(&input.session_id) {
return Ok(MarkReadResult::Rejected {
reason: Rejection::SessionUnavailable,
});
}
let session = self
.store
.get_session::<SessionConfiguration>(&input.session_id)
.await
.map_err(|_| "agent_store_unavailable")?;
if session.is_none_or(|session| session.archived) {
return Ok(MarkReadResult::Rejected {
reason: Rejection::SessionUnavailable,
});
}
// The original store compares the exposed message with the durable tail
// inside its transaction. A stale reader cannot clear a newer message.
let record = self
.store
.set_session_read_marker::<SessionConfiguration>(&input.session_id, &input.message_id)
.await
.map_err(|_| "agent_read_marker_failed")?;
Ok(MarkReadResult::Observed {
session: Box::new(crate::service::summary(self, record)),
})
}
pub async fn metadata(&self, input: Metadata) -> Result<MetadataResult, String> {
if let Some(reason) = metadata_rejection(&input.title, &input.labels) {
return Ok(MetadataResult::Rejected { reason });
}
let runs = self.runs.lock().await;
if runs.closing {
return Ok(MetadataResult::Rejected {
reason: Rejection::Closed,
});
}
if !self.owns(&input.session_id) {
return Ok(MetadataResult::Rejected {
reason: Rejection::SessionUnavailable,
});
}
let Some(session) = self
.store
.get_session::<SessionConfiguration>(&input.session_id)
.await
.map_err(|_| "agent_store_unavailable")?
else {
return Ok(MetadataResult::Rejected {
reason: Rejection::SessionUnavailable,
});
};
if session.archived {
return Ok(MetadataResult::Rejected {
reason: Rejection::SessionUnavailable,
});
}
use maka_event_log::sessions::SessionMutation;
let mode_changed = input
.collaboration_mode
.is_some_and(|mode| mode != session.configuration.collaboration_mode);
if mode_changed {
if self.graph_selected(&input.session_id).await? {
return Ok(MetadataResult::Rejected {
reason: Rejection::Busy,
});
}
if input.collaboration_mode == Some(CollaborationMode::Plan) && !self.settings.plans {
return Ok(MetadataResult::Rejected {
reason: Rejection::ToolsUnavailable,
});
}
if runs.controls.contains_key(&input.session_id)
|| !self.store.message_queue(&input.session_id).await.map_err(|_| "agent_queue_unavailable")?.entries.is_empty()
|| self.store.latest_turn_boundary(&input.session_id).await.map_err(|_| "agent_execution_unavailable")?
.is_some_and(|boundary| matches!(boundary.state, maka_event_log::turns::InvocationState::Ended { outcome: maka_agent_facts::event::InvocationOutcome::HandoffPaused { .. }, .. })) {
return Ok(MetadataResult::Rejected { reason: Rejection::Busy });
}
}
let permissions = input.permissions.is_some() || mode_changed;
let update = move |configuration: &mut SessionConfiguration| {
// Policy changes do not turn an automatic title into a manual title.
if (input.collaboration_mode.is_none() && input.permissions.is_none())
|| configuration.title != input.title
{
configuration.title_is_manual = true;
}
configuration.title = input.title;
configuration.labels = input.labels;
if mode_changed
|| input.permissions.as_ref().is_some_and(|change| {
change.settings != configuration.permissions || change.revoke_grants
})
{
configuration.boundary_revision = configuration
.boundary_revision
.checked_add(1)
.filter(|revision| *revision <= maka_agent_facts::interaction::MAX_SAFE_INTEGER)
.ok_or_else(|| {
maka_event_log::StoreError::InvalidTransition(
"permission boundary revision exhausted".into(),
)
})?;
}
if let Some(change) = input.permissions {
configuration.permissions = change.settings;
}
if let Some(mode) = input.collaboration_mode {
configuration.collaboration_mode = mode;
}
Ok(())
};
let changed = if permissions {
self.store
.update_session_permissions(
&input.session_id,
input.expected_revision,
now(),
update,
)
.await
} else {
self.store
.update_session_metadata(&input.session_id, input.expected_revision, update)
.await
}
.map_err(|_| "agent_metadata_failed")?;
match changed {
SessionMutation::Committed(record) => Ok(MetadataResult::Committed {
session: Box::new(crate::service::summary(self, record)),
}),
SessionMutation::RevisionConflict { expected, actual } => {
Ok(MetadataResult::RevisionConflict { expected, actual })
}
}
}
pub(super) fn tool_names(&self) -> crate::contract::ToolNames {
crate::contract::ToolNames {
names: self.tools.names(),
}
}
fn selected_tools(
&self,
basis: &MessageBasis,
discovery: bool,
editing: Option<maka_agent_facts::execution::EditingTools>,
) -> maka_tools::ToolCatalog {
let selected = plan::select(&self.tools, basis, &self.planning_tools);
let selected = if basis.graph.is_some() {
selected
} else {
selected.excluding(crate::workhub::tasks::graph::NAMES)
};
let selected = match basis.allowed_tools.as_deref() {
None => selected.clone(),
Some(allowed) => {
let names = selected.names();
let excluded: Vec<_> = names
.iter()
.filter(|name| !allowed.contains(name))
.map(String::as_str)
.collect();
selected.excluding(&excluded)
}
};
let selected = match editing {
None => selected,
Some(editing) => {
let excluded: Vec<_> = self
.editing_tools
.iter()
.filter(|(_, style)| *style != editing)
.map(|(name, _)| name.as_str())
.collect();
selected.excluding(&excluded)
}
};
if discovery {
selected.with_discovery()
} else {
selected
}
}
pub async fn send(
self: &Arc<Self>,
input: Send,
operation: &maka_plugin_core::operation::RecoveryRef,
parent: &maka_capabilities::call::Scope,
) -> Result<crate::contract::SendResult, String> {
let Send {
session_id: id,
message: text,
model: selection,
thinking_level,
attachments,
} = input;
self.submit(
Submission {
id,
text,
attachments,
selection: Selection::Model(selection, thinking_level),
},
operation,
parent,
)
.await
}
pub async fn execute(
self: &Arc<Self>,
input: Execute,
operation: &maka_plugin_core::operation::RecoveryRef,
parent: &maka_capabilities::call::Scope,
) -> Result<SendResult, String> {
self.submit(
Submission {
id: input.session_id,
text: input.message,
attachments: input.attachments,
selection: Selection::Executor(input.executor),
},
operation,
parent,
)
.await
}
async fn submit(
self: &Arc<Self>,
input: Submission,
operation: &maka_plugin_core::operation::RecoveryRef,
parent: &maka_capabilities::call::Scope,
) -> Result<SendResult, String> {
let Submission {
id,
text,
attachments,
selection,
} = input;
if text.trim().is_empty() && attachments.is_empty() {
return Err("agent_message_invalid".into());
}
let admission = self.runs.session(&id).await;
let queued = {
let runs = admission.state().await;
if runs.closing {
return Err("agent_closed".into());
}
if runs.quarantined.contains(&id) {
return Err("agent_cleanup_unconfirmed".into());
}
let current = runs.controls.get(&id);
if let Some(reason) = current.and_then(|control| control.failure) {
return Err(reason.into());
}
current.map(|control| control.invocation.clone())
};
let session = self.session(&id).await?;
if session.archived {
return Err("agent_session_configuration_changed".into());
}
let backend = match selection {
Selection::Model(selection, thinking_level) => PreparedBackend::Model(Box::new(
crate::model::Source::select(
self.configuration.clone(),
self.catalog.clone(),
self.credentials.clone(),
selection,
thinking_level,
id.clone(),
self.revision.clone(),
)
.await
.map_err(public_error)?,
)),
Selection::Executor(selection) => {
if session.configuration.tools.is_some() {
return Err("agent_executor_cannot_enforce_tool_selection".into());
}
PreparedBackend::Executor(Box::new(self.executors.select(selection)?))
}
};
let (model, executor) = match &backend {
PreparedBackend::Model(value) => (Some(value.0.accepted.clone()), None),
PreparedBackend::Executor(value) => (None, Some(value.accepted.clone())),
};
workspace::validate(
&self.workspace_roots,
&session.configuration.workspace,
&self.managed_worktrees,
)
.await?;
let external = executor.is_some();
if session.configuration.collaboration_mode == CollaborationMode::Plan
&& (!self.settings.plans || external)
{
return Err("agent_planning_unavailable".into());
}
let snapshot = self
.configuration
.runtime_policy()
.await
.map_err(|_| "agent_preferences_unavailable")?;
let language = self.resolve_language(&snapshot);
let mut basis = MessageBasis {
language,
compaction_prompt: Some(self.effective_compaction_prompt(&snapshot, language)),
graph: None,
collaboration_mode: session.configuration.collaboration_mode,
background_kind: BackgroundKind::Schedule,
tool_discovery: !external && self.settings.tool_discovery,
allowed_tools: session.configuration.tools.clone(),
background: None,
internal_tools: if external {
vec![]
} else {
self.internal_tools.clone()
},
internal_bindings: if external {
BTreeMap::new()
} else {
self.internal_bindings.clone()
},
behavior: self
.capture_behavior(&session.configuration.workspace, &snapshot, language)
.await?,
model,
executor,
workspace: session.configuration.workspace.clone(),
fingerprint: operation.fingerprint.clone(),
tools: if external {
vec![]
} else {
self.tool_basis.clone()
},
};
self.prepare_plan_behavior(&id, &mut basis).await?;
self.prepare_graph_behavior(&id, &mut basis, queued.is_none())
.await?;
let identity = self.operation_id(operation);
let invocation = queued.clone().unwrap_or_else(|| Invocation {
session_id: id.clone(),
turn_id: format!("t_{identity}"),
run_id: format!("r_{identity}"),
invocation_id: format!("i_{identity}"),
});
let mut message: MessageInput = text.into();
message.attachments = self.attachment_refs(&id, attachments).await?;
let unprepared = message;
let (message, required_tools) = match self
.prepare_skills(
unprepared.clone(),
language,
Some(session.configuration.workspace.clone()),
parent,
if basis.executor.is_some() {
vec![]
} else {
self.selected_tools(
&basis,
false,
basis.model.as_ref().and_then(|model| model.editing_tools),
)
.names()
},
)
.await?
{
skills::Input::Ready {
message,
required_tools,
} => (message, required_tools),
skills::Input::Rejected { reason, skills } => {
return Ok(SendResult::Rejected {
session_id: id,
reason,
skills,
});
}
};
let delivered = RootSourceMessage {
message: DeliveredMessage {
origin: None,
message_id: format!("m_{identity}"),
submitted_content_digest: unprepared
.content_digest()
.map_err(|_| "agent_message_invalid")?,
content: message.clone(),
},
unprepared_content: unprepared,
submitted_placement: if queued.is_some() {
Placement::NextTurn
} else {
Placement::CurrentTurn
},
disposition: if queued.is_some() {
MessageDisposition::Followup
} else {
MessageDisposition::TurnStarted
},
submitted_intent: None,
};
let pending = maka_event_log::message_admissions::PendingMessageAdmission {
execution: maka_event_log::message_admissions::PendingExecution {
owner: self.prefix.clone(),
basis: serde_json::to_value(&basis).expect("accepted message basis"),
},
invocation: invocation.clone(),
steering_invocation: None,
source: delivered.clone(),
required_tools,
admitted_at: now(),
};
if queued.is_some() {
loop {
let revision = self
.store
.message_queue_revision(&id)
.await
.map_err(|_| "agent_queue_unavailable")?;
match self
.store
.admit_queued_message(&self.revision.activation, revision, pending.clone())
.await
{
Ok(receipt) => {
return Ok(SendResult::Queued {
session_id: id,
message_id: delivered.message.message_id,
revision: receipt.queue_revision,
});
}
Err(maka_event_log::StoreError::RevisionConflict { .. }) => continue,
Err(_) => return Err("agent_admission_failed".into()),
}
}
}
let input = self
.input(invocation.clone(), backend, basis.clone(), delivered)
.await?;
self.store
.admit_message(pending)
.await
.map_err(|_| "agent_admission_failed")?;
let cancellation = self.stop.child_token();
let running = match self.start(input, cancellation.clone()).await {
Ok(running) => running,
Err(error) => {
if error.is_cleanup_unconfirmed() {
admission.state().await.quarantined.insert(id.clone());
}
return Err(public_error(error));
}
};
self.publish(&mut *admission.state().await, &running, basis);
let owner = self.clone();
self.workers
.spawn(async move { owner.drive(running).await });
Ok(crate::contract::SendResult::Started {
session_id: invocation.session_id.clone(),
invocation_id: invocation.invocation_id,
message_id: format!("m_{identity}"),
})
}
}
fn metadata_rejection(title: &str, labels: &[String]) -> Option<Rejection> {
if title.trim().is_empty() || title.chars().any(char::is_control) {
Some(Rejection::TitleInvalid)
} else if labels
.iter()
.any(|label| label.trim().is_empty() || label.chars().any(char::is_control))
{
Some(Rejection::LabelsInvalid)
} else {
None
}
}
impl Owner {
pub(crate) fn prepare_retirement(
&self,
) -> Option<Box<dyn maka_host_runtime::application::NativeRetirement>> {
let producer = self.producer_gate.clone().try_lock_owned().ok()?;
let runs = self.runs.clone().try_lock_owned().ok()?;
if !runs.controls.is_empty()
|| !runs.quarantined.is_empty()
|| !self.workers.is_empty()
|| !self.auxiliary.resources.is_idle()
|| self.goals.keeps_host_awake()
|| self.plans.keeps_host_awake()
|| self
.graphs
.as_ref()
.is_some_and(|graphs| graphs.keeps_host_awake())
{
return None;
}
Some(Box::new(Idle {
runs,
_producer: producer,
stop: self.stop.clone(),
}))
}
pub fn close(self: &Arc<Self>) -> BoxFuture<'static, Result<(), String>> {
let owner = self.clone();
Box::pin(async move {
{
let mut runs = owner.runs.lock().await;
runs.closing = true;
owner.stop.cancel();
}
owner.producers.close();
owner.producers.wait().await;
owner.workers.close();
owner.workers.wait().await;
owner.engine.drain().await;
let scheduler = owner.scheduler.finish().await;
let todo = owner.todo.finish().await;
let goals = owner.goals.finish().await;
let plans = owner.plans.finish().await;
let graph = match &owner.graphs {
Some(graphs) => graphs.finish().await,
None => Ok(()),
};
let recap = owner.recap_storage.finish().await;
let auxiliary = owner.auxiliary.resources.finish().await;
let configuration = owner.configuration.shutdown().await;
if let Err(error) = &configuration {
tracing::error!(stage = "configuration_store_shutdown", cause = crate::diagnostics::config_cause(error),
activation = %owner.revision.activation, instance = ?owner.data_source.namespace,
"Agent configuration store cleanup failed");
}
let runs = owner.runs.lock().await;
if !runs.quarantined.is_empty() {
return Err("agent_cleanup_unconfirmed".into());
}
scheduler?;
todo?;
goals?;
plans?;
graph?;
recap?;
auxiliary.map_err(|error| error.to_string())?;
configuration.map_err(|_| "agent_configuration_cleanup_unconfirmed".into())
})
}
}
struct Idle {
runs: runs::Exclusive,
_producer: tokio::sync::OwnedMutexGuard<()>,
stop: CancellationToken,
}
impl maka_host_runtime::application::NativeRetirement for Idle {
fn commit(mut self: Box<Self>) {
self.runs.closing = true;
self.stop.cancel();
}
}
use maka_agent_authority::now;
fn jsonschema_valid(input: &Value) -> bool {
jsonschema::validator_for(&configuration_schema())
.expect("configuration schema")
.is_valid(input)
}
fn public_error(error: RunError) -> String {
match error {
RunError::Busy => "agent_session_busy",
RunError::Cancelled => "agent_cancelled",
RunError::CleanupUnconfirmed => "agent_cleanup_unconfirmed",
_ => "agent_execution_unavailable",
}
.into()
}
#[cfg(test)]
#[path = "owner/tests.rs"]
mod tests;
pub(crate) fn owns_session(prefix: &str, id: &str) -> bool {
id.strip_prefix(prefix).is_some_and(|tail| {
tail.len() == 32
&& tail
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
})
}