everruns_core/capabilities/
human_intent.rs1use 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}