1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//! The unattended `ask_user` resolution, split out of `host.rs` to keep that
//! file under the size ratchet (EVE-1057). It is a method on the same
//! `RuntimeSessionLifecycle`, in its own impl block.
use crate::events::{EventContext, EventRequest};
use everruns_contracts::typed_id::{MessageId, TurnId};
use crate::host::runtime_host::{RuntimeHostAdapter, RuntimeSessionLifecycle};
impl<A: RuntimeHostAdapter> RuntimeSessionLifecycle<A> {
/// Answer `ask_user` calls the client structurally cannot be asked.
///
/// Emitted instead of parking when the session never declared the
/// `ask_user` hint. Each call is completed with the options the model
/// itself marked as defaults and `answered_by: "unattended"`, so the model
/// gets a truthful account in the same turn: it asked, nobody could be
/// asked, the defaults applied (EVE-1057).
///
/// Not an error, and not a timeout. Telling `unattended` from `timeout` is
/// how the model learns whether waiting would ever have helped.
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<()> {
for (tool_call_id, arguments) in calls {
let result = everruns_contracts::unattended_ask_user_result(&arguments);
let data = crate::events::ToolCompletedData::success(
tool_call_id.clone(),
everruns_contracts::ASK_USER_TOOL_NAME.to_string(),
vec![crate::message::ContentPart::tool_result_text(&result)],
None,
);
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(())
}
}