use crate::events::{EventContext, EventRequest};
use everruns_contracts::typed_id::{MessageId, TurnId};
use crate::host::runtime_host::{RuntimeHostAdapter, RuntimeSessionLifecycle};
impl<A: RuntimeHostAdapter> RuntimeSessionLifecycle<A> {
pub async fn resolve_ask_user_unattended(
&self,
turn_id: Option<TurnId>,
input_message_id: MessageId,
calls: Vec<(String, serde_json::Value)>,
) -> everruns_contracts::error::Result<()> {
let locale = self
.adapter
.session_store(self.org_id)
.get_session(self.session_id)
.await
.ok()
.flatten()
.and_then(|session| session.locale);
let registry = self.adapter.capability_registry();
for (tool_call_id, arguments) in calls {
let result = everruns_contracts::unattended_ask_user_result(&arguments);
let data = unattended_completion(
®istry,
tool_call_id.clone(),
&arguments,
&result,
locale.as_deref(),
);
let context = match turn_id {
Some(turn_id) => EventContext::turn(turn_id, input_message_id),
None => EventContext::empty(),
};
tracing::info!(
session_id = %self.session_id,
%tool_call_id,
"no client declared it renders questions; answering with declared defaults"
);
self.adapter
.event_emitter()
.emit(EventRequest::new(self.session_id, context, data))
.await?;
}
Ok(())
}
}
fn unattended_completion(
registry: &crate::capabilities::CapabilityRegistry,
tool_call_id: String,
arguments: &serde_json::Value,
result: &serde_json::Value,
locale: Option<&str>,
) -> crate::events::ToolCompletedData {
use crate::tool_narration::{
ToolNarrationContext, ToolNarrationPhase, render_tool_narration_with_locale,
};
let call = everruns_contracts::tool_types::ToolCall {
id: tool_call_id.clone(),
name: everruns_contracts::ASK_USER_TOOL_NAME.into(),
arguments: arguments.clone(),
};
let narration = registry
.get("ask_user")
.and_then(|capability| {
capability.narrate(
None,
&call,
ToolNarrationPhase::Completed,
locale,
ToolNarrationContext::default(),
)
})
.unwrap_or_else(|| {
render_tool_narration_with_locale(None, &call, ToolNarrationPhase::Completed, locale)
});
crate::events::ToolCompletedData::success(
tool_call_id,
everruns_contracts::ASK_USER_TOOL_NAME.into(),
vec![crate::message::ContentPart::tool_result_text(result)],
None,
)
.with_narration(Some(narration))
}
#[cfg(all(test, feature = "builtins"))]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn unattended_completion_preserves_owned_narration() {
let mut registry = crate::capabilities::CapabilityRegistry::new();
registry.register(crate::builtins::AskUserCapability::client_side());
let arguments = json!({"questions":[{"header":"Release scope","question":"PRIVATE_BODY"}]});
let result = json!({"answers":[{"other_text":"PRIVATE_ANSWER"}]});
for (locale, expected) in [
(None, "Asked user: Release scope"),
(Some("uk-UA"), "Запитав користувача: Release scope"),
] {
let data = unattended_completion(®istry, "call".into(), &arguments, &result, locale);
assert_eq!(data.narration.as_deref(), Some(expected));
assert!(data.success);
}
}
}