Skip to main content

everruns_core/capabilities/
human_intent.rs

1use std::sync::Arc;
2
3use crate::capabilities::{Capability, 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 category(&self) -> Option<&'static str> {
27        Some("Core")
28    }
29
30    fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
31        vec![Arc::new(HumanIntentToolDefinitionHook)]
32    }
33
34    fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
35        vec![Arc::new(HumanIntentToolCallHook)]
36    }
37}
38
39struct HumanIntentToolDefinitionHook;
40
41impl ToolDefinitionHook for HumanIntentToolDefinitionHook {
42    fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition> {
43        add_human_intent_to_tool_definitions(&tools)
44    }
45}
46
47struct HumanIntentToolCallHook;
48
49impl ToolCallHook for HumanIntentToolCallHook {
50    fn narration(
51        &self,
52        _tool_def: Option<&ToolDefinition>,
53        tool_call: &ToolCall,
54        _phase: ToolNarrationPhase,
55        _locale: Option<&str>,
56    ) -> Option<String> {
57        human_intent(&tool_call.arguments).map(truncate_intent)
58    }
59
60    fn transform_for_execution(&self, mut tool_call: ToolCall) -> ToolCall {
61        tool_call.arguments = tool_call.execution_arguments();
62        tool_call
63    }
64}
65
66fn truncate_intent(intent: &str) -> String {
67    const MAX_LEN: usize = 120;
68    const ELLIPSIS: &str = "...";
69    let clean = intent.trim();
70    if clean.chars().count() <= MAX_LEN {
71        return clean.to_string();
72    }
73
74    let truncated: String = clean
75        .chars()
76        .take(MAX_LEN - ELLIPSIS.chars().count())
77        .collect();
78    format!("{truncated}{ELLIPSIS}")
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
85    use serde_json::json;
86
87    #[test]
88    fn human_intent_capability_adds_optional_schema_argument() {
89        let capability = HumanIntentCapability;
90        let hook = capability.tool_definition_hooks().pop().unwrap();
91        let tool = ToolDefinition::Builtin(BuiltinTool {
92            name: "manage_harnesses".to_string(),
93            display_name: Some("Manage Harnesses".to_string()),
94            description: "Manage harnesses".to_string(),
95            parameters: json!({
96                "type": "object",
97                "properties": {
98                    "operation": { "type": "string" }
99                },
100                "required": ["operation"],
101                "additionalProperties": false
102            }),
103            policy: ToolPolicy::Auto,
104            category: None,
105            deferrable: DeferrablePolicy::default(),
106            hints: Default::default(),
107            full_parameters: None,
108        });
109
110        let transformed = hook.transform(vec![tool]);
111        let params = transformed[0].parameters();
112
113        assert_eq!(params["properties"]["human_intent"]["type"], "string");
114        assert_eq!(params["properties"]["human_intent"]["maxLength"], 120);
115        assert!(
116            !params["required"]
117                .as_array()
118                .unwrap()
119                .iter()
120                .any(|item| item.as_str() == Some("human_intent"))
121        );
122        assert_eq!(params["additionalProperties"], false);
123    }
124
125    #[test]
126    fn human_intent_tool_call_hook_reads_and_strips_argument() {
127        let capability = HumanIntentCapability;
128        let hook = capability.tool_call_hooks().pop().unwrap();
129        let tool_call = ToolCall {
130            id: "call_1".to_string(),
131            name: "manage_harnesses".to_string(),
132            arguments: json!({
133                "operation": "list",
134                "human_intent": "Listing all harnesses"
135            }),
136        };
137
138        assert_eq!(
139            hook.narration(None, &tool_call, ToolNarrationPhase::Started, None),
140            Some("Listing all harnesses".to_string())
141        );
142
143        let execution_call = hook.transform_for_execution(tool_call);
144        assert_eq!(execution_call.arguments, json!({ "operation": "list" }));
145    }
146
147    #[test]
148    fn truncate_intent_stays_within_cap() {
149        let long_intent = "x".repeat(130);
150        let truncated = truncate_intent(&long_intent);
151
152        assert_eq!(truncated.chars().count(), 120);
153        assert!(truncated.ends_with("..."));
154    }
155}