blob: 6446984f9c2449b9bdee407d9e73e3feb806b44d [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.
*/
//! Explicitly bound question producer. Canonical records belong to the Run;
//! the original admitted Host call owns waiting and durable withdrawal.
use futures_util::future::BoxFuture;
use maka_agent_facts::interaction::InteractionOutcome;
use maka_agent_facts::interaction::InteractionRecord;
use maka_event_log::EventLog;
use maka_host_runtime::application::{
Context, Handler, NativeActivation, NativeOwner, NativeRegistry,
};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, sync::Arc};
mod projection;
use maka_interactions_api::questions::*;
pub fn provide(registry: &mut NativeRegistry) -> Result<(), String> {
registry.insert(
maka_interactions_api::questions::IMPLEMENTATION.into(),
Arc::new(|context, _| {
Box::pin(async move {
let Some(persistence) = context.persistence() else {
return NativeActivation {
exports: Err("questions_require_persistence".into()),
owner: Arc::new(()),
};
};
let owner = Arc::new(Questions(persistence.store().clone()));
NativeActivation {
exports: Ok(BTreeMap::from([(
EXPORT.into(),
maka_plugin_core::runtime::Published {
contract: contract(),
value: owner.clone() as Arc<dyn Handler>,
},
)])),
owner,
}
})
}),
)
}
struct Questions(Arc<EventLog>);
impl Handler for Questions {
fn call(
&self,
method: String,
input: Value,
context: Context,
) -> BoxFuture<'static, Result<Value, String>> {
let store = self.0.clone();
Box::pin(async move {
if method != "ask" {
return Err("question_method_unavailable".into());
}
let invocation = context
.scope()
.identity
.agent()
.ok_or("question_requires_agent")?
.clone();
let operation_id = context
.scope()
.identity
.operation_id()
.ok_or("question_requires_tool")?;
let operation = context.operation().ok_or("question_requires_mutation")?;
let tool = maka_agent_facts::tools::ToolCallContext {
invocation: invocation.clone(),
operation_id: operation_id.into(),
};
let request = match projection::project_question_request(
&tool.tool_use_id(),
&input["questions"],
) {
Ok(request) => request,
Err(_) => {
return encode(Answer::Rejected {
reason: Rejection::InvalidInput,
});
}
};
let input: Ask = serde_json::from_value(input).map_err(|_| "question_input_invalid")?;
if context.scope().cancellation.is_cancelled() {
return encode(Answer::Rejected {
reason: Rejection::Unavailable,
});
}
let record = InteractionRecord {
session_id: invocation.session_id.clone(),
turn_id: invocation.turn_id.clone(),
run_id: invocation.run_id.clone(),
request_id: format!(
"question_{}",
hex::encode(Sha256::digest(
serde_json::to_vec(operation).expect("operation identity")
))
),
created_at: maka_agent_authority::now(),
request,
outcome: None,
};
let Some(result) = maka_agent_authority::interaction_wait::publish(
&store,
&context,
&invocation,
&record,
)
.await?
else {
return encode(Answer::Rejected {
reason: Rejection::Unavailable,
});
};
let answer = match result {
InteractionOutcome::QuestionAnswer { answers, .. } => Answer::Answered {
answers: input
.questions
.into_iter()
.zip(answers)
.map(|(question, answer)| Response {
question: question.question,
answer,
})
.collect(),
},
InteractionOutcome::Closure { reason, .. } => Answer::Closed { reason },
_ => return Err("question_outcome_invalid".into()),
};
encode(answer)
})
}
}
fn encode(answer: Answer) -> Result<Value, String> {
serde_json::to_value(answer).map_err(|_| "question_output_unavailable".into())
}
impl NativeOwner for Questions {
fn execution_basis(&self) -> Option<Value> {
Some(serde_json::json!({"root": self.0.owned_root().ok()?.root_id()}))
}
fn prepare_retirement(
&self,
) -> Option<Box<dyn maka_host_runtime::application::NativeRetirement>> {
Some(Box::new(()))
}
fn close(&self) -> BoxFuture<'static, Result<(), String>> {
// No detached work: native Host calls retain actual request custody and
// revoke/settle their Scope before the provider can finish retirement.
Box::pin(async { Ok(()) })
}
}