everruns_builtins/
human_intent.rs1use std::sync::Arc;
2
3use crate::capabilities::{Capability, CapabilityLocalization, ToolCallHook, ToolDefinitionHook};
4use crate::tool_narration::ToolNarrationPhase;
5use crate::tool_types::{
6 ToolCall, ToolDefinition, add_human_intent_to_tool_definitions, human_intent,
7};
8
9pub const HUMAN_INTENT_CAPABILITY_ID: &str = "human_intent";
10
11pub struct HumanIntentCapability;
12
13impl Capability for HumanIntentCapability {
14 fn id(&self) -> &'static str {
15 HUMAN_INTENT_CAPABILITY_ID
16 }
17
18 fn name(&self) -> &'static str {
19 "Human Intent"
20 }
21
22 fn description(&self) -> &'static str {
23 "Adds model-authored human_intent narration to every active tool call for UI rendering."
24 }
25
26 fn localizations(&self) -> Vec<CapabilityLocalization> {
27 vec![CapabilityLocalization::text(
28 "uk",
29 "Людський намір",
30 "Додає до кожного активного виклику інструмента написаний моделлю опис наміру human_intent для відображення в інтерфейсі.",
31 )]
32 }
33
34 fn category(&self) -> Option<&'static str> {
35 Some("Core")
36 }
37
38 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
39 vec![Arc::new(HumanIntentToolDefinitionHook)]
40 }
41
42 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
43 vec![Arc::new(HumanIntentToolCallHook)]
44 }
45}
46
47struct HumanIntentToolDefinitionHook;
48
49impl ToolDefinitionHook for HumanIntentToolDefinitionHook {
50 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition> {
51 add_human_intent_to_tool_definitions(&tools)
52 }
53}
54
55struct HumanIntentToolCallHook;
56
57impl ToolCallHook for HumanIntentToolCallHook {
58 fn narration(
59 &self,
60 _tool_def: Option<&ToolDefinition>,
61 tool_call: &ToolCall,
62 _phase: ToolNarrationPhase,
63 _locale: Option<&str>,
64 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
65 ) -> Option<String> {
66 human_intent(&tool_call.arguments).map(truncate_intent)
67 }
68
69 fn transform_for_execution(&self, mut tool_call: ToolCall) -> ToolCall {
70 tool_call.arguments = tool_call.execution_arguments();
71 tool_call
72 }
73}
74
75fn truncate_intent(intent: &str) -> String {
76 const MAX_LEN: usize = 120;
77 const ELLIPSIS: &str = "...";
78 let clean = intent.trim();
79 if clean.chars().count() <= MAX_LEN {
80 return clean.to_string();
81 }
82
83 let truncated: String = clean
84 .chars()
85 .take(MAX_LEN - ELLIPSIS.chars().count())
86 .collect();
87 format!("{truncated}{ELLIPSIS}")
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
94 use serde_json::json;
95
96 #[test]
97 fn human_intent_capability_adds_optional_schema_argument() {
98 let capability = HumanIntentCapability;
99 let hook = capability.tool_definition_hooks().pop().unwrap();
100 let tool = ToolDefinition::Builtin(BuiltinTool {
101 name: "manage_harnesses".to_string(),
102 display_name: Some("Manage Harnesses".to_string()),
103 description: "Manage harnesses".to_string(),
104 parameters: json!({
105 "type": "object",
106 "properties": {
107 "operation": { "type": "string" }
108 },
109 "required": ["operation"],
110 "additionalProperties": false
111 }),
112 policy: ToolPolicy::Auto,
113 category: None,
114 deferrable: DeferrablePolicy::default(),
115 hints: Default::default(),
116 full_parameters: None,
117 });
118
119 let original = serde_json::to_value(&tool).unwrap();
120 let transformed = hook.transform(vec![tool]);
121 assert_eq!(transformed.len(), 1);
122 let mut restored = serde_json::to_value(&transformed[0]).unwrap();
123 restored["parameters"]["properties"]
124 .as_object_mut()
125 .unwrap()
126 .remove("human_intent");
127 assert_eq!(restored, original);
128 let params = transformed[0].parameters();
129
130 assert_eq!(params["properties"]["human_intent"]["type"], "string");
131 assert_eq!(params["properties"]["human_intent"]["maxLength"], 120);
132 assert!(
133 !params["required"]
134 .as_array()
135 .unwrap()
136 .iter()
137 .any(|item| item.as_str() == Some("human_intent"))
138 );
139 assert_eq!(params["additionalProperties"], false);
140 }
141
142 #[test]
143 fn narration_trims_unicode_boundaries_and_execution_preserves_original_call() {
144 let hook = HumanIntentCapability.tool_call_hooks().pop().unwrap();
145 for (intent, expected) in [
146 (json!(null), None),
147 (json!(17), None),
148 (json!(" "), None),
149 (
150 json!(" Listing harnesses "),
151 Some("Listing harnesses".to_owned()),
152 ),
153 (json!("界".repeat(119)), Some("界".repeat(119))),
154 (json!("界".repeat(120)), Some("界".repeat(120))),
155 (
156 json!(format!(" {} ", "界".repeat(121))),
157 Some(format!("{}...", "界".repeat(117))),
158 ),
159 ] {
160 let call = ToolCall {
161 id: "call-original".into(),
162 name: "manage_harnesses".into(),
163 arguments: json!({"operation":"list","nested":{"keep":true},"human_intent":intent}),
164 };
165 for phase in [
166 ToolNarrationPhase::Started,
167 ToolNarrationPhase::Waiting,
168 ToolNarrationPhase::Completed,
169 ToolNarrationPhase::Failed,
170 ] {
171 assert_eq!(
172 hook.narration(None, &call, phase, Some("uk-UA"), Default::default()),
173 expected
174 );
175 }
176 let execution = hook.transform_for_execution(call);
177 assert_eq!(
178 serde_json::to_value(execution).unwrap(),
179 json!({"id":"call-original","name":"manage_harnesses","arguments":{"operation":"list","nested":{"keep":true}}})
180 );
181 }
182 let call = ToolCall {
183 id: "no-intent".into(),
184 name: "plain".into(),
185 arguments: json!({"operation":"list"}),
186 };
187 assert_eq!(
188 hook.narration(
189 None,
190 &call,
191 ToolNarrationPhase::Started,
192 None,
193 Default::default()
194 ),
195 None
196 );
197 assert_eq!(
198 serde_json::to_value(hook.transform_for_execution(call.clone())).unwrap(),
199 serde_json::to_value(call).unwrap()
200 );
201 }
202}