Skip to main content

everruns_builtins/
human_intent.rs

1use 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}