everruns_core/capabilities/
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 transformed = hook.transform(vec![tool]);
120 let params = transformed[0].parameters();
121
122 assert_eq!(params["properties"]["human_intent"]["type"], "string");
123 assert_eq!(params["properties"]["human_intent"]["maxLength"], 120);
124 assert!(
125 !params["required"]
126 .as_array()
127 .unwrap()
128 .iter()
129 .any(|item| item.as_str() == Some("human_intent"))
130 );
131 assert_eq!(params["additionalProperties"], false);
132 }
133
134 #[test]
135 fn human_intent_tool_call_hook_reads_and_strips_argument() {
136 let capability = HumanIntentCapability;
137 let hook = capability.tool_call_hooks().pop().unwrap();
138 let tool_call = ToolCall {
139 id: "call_1".to_string(),
140 name: "manage_harnesses".to_string(),
141 arguments: json!({
142 "operation": "list",
143 "human_intent": "Listing all harnesses"
144 }),
145 };
146
147 assert_eq!(
148 hook.narration(
149 None,
150 &tool_call,
151 ToolNarrationPhase::Started,
152 None,
153 Default::default()
154 ),
155 Some("Listing all harnesses".to_string())
156 );
157
158 let execution_call = hook.transform_for_execution(tool_call);
159 assert_eq!(execution_call.arguments, json!({ "operation": "list" }));
160 }
161
162 #[test]
163 fn truncate_intent_stays_within_cap() {
164 let long_intent = "x".repeat(130);
165 let truncated = truncate_intent(&long_intent);
166
167 assert_eq!(truncated.chars().count(), 120);
168 assert!(truncated.ends_with("..."));
169 }
170}