Skip to content

Commit ad41194

Browse files
committed
Implement unified context system with knowledge integration
- Add unified context management system - Integrate knowledge store with context handling - Add new unified context modules and tests - Rename knowledge tool to context tool - Update tool manager and prompt commands - Add planning documentation for unified context system
1 parent cfa9e7c commit ad41194

30 files changed

+2851
-510
lines changed

crates/chat-cli/src/cli/agent/hook.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,16 @@ impl Default for Source {
4747
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Hash)]
4848
pub struct Hook {
4949
/// The command to run when the hook is triggered
50-
pub command: String,
50+
#[serde(skip_serializing_if = "Option::is_none")]
51+
pub command: Option<String>,
52+
53+
/// The tool name to execute when the hook is triggered
54+
#[serde(skip_serializing_if = "Option::is_none")]
55+
pub tool_name: Option<String>,
56+
57+
/// Arguments to pass to the tool
58+
#[serde(skip_serializing_if = "Option::is_none")]
59+
pub tool_args: Option<serde_json::Value>,
5160

5261
/// Max time the hook can run before it throws a timeout error
5362
#[serde(default = "Hook::default_timeout_ms")]
@@ -69,7 +78,21 @@ pub struct Hook {
6978
impl Hook {
7079
pub fn new(command: String, source: Source) -> Self {
7180
Self {
72-
command,
81+
command: Some(command),
82+
tool_name: None,
83+
tool_args: None,
84+
timeout_ms: Self::default_timeout_ms(),
85+
max_output_size: Self::default_max_output_size(),
86+
cache_ttl_seconds: Self::default_cache_ttl_seconds(),
87+
source,
88+
}
89+
}
90+
91+
pub fn new_tool(tool_name: String, tool_args: Option<serde_json::Value>, source: Source) -> Self {
92+
Self {
93+
command: None,
94+
tool_name: Some(tool_name),
95+
tool_args,
7396
timeout_ms: Self::default_timeout_ms(),
7497
max_output_size: Self::default_max_output_size(),
7598
cache_ttl_seconds: Self::default_cache_ttl_seconds(),

crates/chat-cli/src/cli/agent/legacy/hooks.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ impl LegacyHook {
7676
impl From<LegacyHook> for Option<Hook> {
7777
fn from(value: LegacyHook) -> Self {
7878
Some(Hook {
79-
command: value.command?,
79+
command: Some(value.command?),
80+
tool_name: None,
81+
tool_args: None,
8082
timeout_ms: value.timeout_ms,
8183
max_output_size: value.max_output_size,
8284
cache_ttl_seconds: value.cache_ttl_seconds,

crates/chat-cli/src/cli/chat/cli/context.rs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,20 @@ impl ContextSubcommand {
9696
session.stderr,
9797
style::SetAttribute(Attribute::Bold),
9898
style::SetForegroundColor(Color::Magenta),
99-
style::Print(format!("👤 Agent ({}):\n", context_manager.current_profile)),
99+
style::Print(format!(" 👤 Agent ({}):\n", context_manager.current_profile)),
100100
style::SetAttribute(Attribute::Reset),
101101
)?;
102102

103103
if agent_owned_list.is_empty() {
104104
execute!(
105105
session.stderr,
106106
style::SetForegroundColor(Color::DarkGrey),
107-
style::Print(" <none>\n\n"),
107+
style::Print(" <none>\n\n"),
108108
style::SetForegroundColor(Color::Reset)
109109
)?;
110110
} else {
111111
for path in &agent_owned_list {
112-
execute!(session.stderr, style::Print(format!(" {} ", path.get_path_as_str())))?;
112+
execute!(session.stderr, style::Print(format!(" {} ", path.get_path_as_str())))?;
113113
if let Ok(context_files) = context_manager
114114
.get_context_files_by_path(os, path.get_path_as_str())
115115
.await
@@ -136,20 +136,20 @@ impl ContextSubcommand {
136136
session.stderr,
137137
style::SetAttribute(Attribute::Bold),
138138
style::SetForegroundColor(Color::Magenta),
139-
style::Print("💬 Session (temporary):\n"),
139+
style::Print(" 💬 Session (temporary):\n"),
140140
style::SetAttribute(Attribute::Reset),
141141
)?;
142142

143143
if session_owned_list.is_empty() {
144144
execute!(
145145
session.stderr,
146146
style::SetForegroundColor(Color::DarkGrey),
147-
style::Print(" <none>\n\n"),
147+
style::Print(" <none>\n\n"),
148148
style::SetForegroundColor(Color::Reset)
149149
)?;
150150
} else {
151151
for path in &session_owned_list {
152-
execute!(session.stderr, style::Print(format!(" {} ", path.get_path_as_str())))?;
152+
execute!(session.stderr, style::Print(format!(" {} ", path.get_path_as_str())))?;
153153
if let Ok(context_files) = context_manager
154154
.get_context_files_by_path(os, path.get_path_as_str())
155155
.await
@@ -361,14 +361,4 @@ impl ContextSubcommand {
361361
skip_printing_tools: true,
362362
})
363363
}
364-
365-
pub fn name(&self) -> &'static str {
366-
match self {
367-
ContextSubcommand::Show { .. } => "show",
368-
ContextSubcommand::Add { .. } => "add",
369-
ContextSubcommand::Remove { .. } => "remove",
370-
ContextSubcommand::Clear => "clear",
371-
ContextSubcommand::Hooks => "hooks",
372-
}
373-
}
374364
}

crates/chat-cli/src/cli/chat/cli/hooks.rs

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,20 @@ impl HookExecutor {
122122
}
123123

124124
if let Err(err) = &result {
125+
let hook_desc = if let Some(tool_name) = &hook.1.tool_name {
126+
format!("tool:{}", tool_name)
127+
} else if let Some(command) = &hook.1.command {
128+
command.clone()
129+
} else {
130+
"unknown hook".to_string()
131+
};
132+
125133
queue!(
126134
output,
127135
style::SetForegroundColor(style::Color::Red),
128136
style::Print("✗ "),
129137
style::SetForegroundColor(style::Color::Blue),
130-
style::Print(&hook.1.command),
138+
style::Print(&hook_desc),
131139
style::ResetColor,
132140
style::Print(" failed after "),
133141
style::SetForegroundColor(style::Color::Yellow),
@@ -189,8 +197,94 @@ impl HookExecutor {
189197
) -> ((HookTrigger, Hook), Result<String>, Duration) {
190198
let start_time = Instant::now();
191199

192-
let command = &hook.1.command;
200+
let result = if let Some(tool_name) = &hook.1.tool_name {
201+
// Execute tool
202+
self.execute_tool(tool_name, &hook.1.tool_args, prompt).await
203+
} else if let Some(command) = &hook.1.command {
204+
// Execute shell command
205+
self.execute_command(command, &hook.1, prompt).await
206+
} else {
207+
Err(eyre!("Hook must have either command or tool_name"))
208+
};
209+
210+
(hook, result, start_time.elapsed())
211+
}
212+
213+
async fn execute_tool(
214+
&self,
215+
tool_name: &str,
216+
tool_args: &Option<serde_json::Value>,
217+
prompt: Option<&str>,
218+
) -> Result<String> {
219+
// Replace ${USER_PROMPT} in tool_args if prompt is provided
220+
let mut substituted_args = tool_args.clone();
221+
if let (Some(args), Some(user_prompt)) = (&mut substituted_args, prompt) {
222+
if let Some(obj) = args.as_object_mut() {
223+
for (_, value) in obj {
224+
if let Some(s) = value.as_str() {
225+
*value = serde_json::Value::String(s.replace("${USER_PROMPT}", user_prompt));
226+
}
227+
}
228+
}
229+
}
230+
use crate::cli::chat::tools::Tool;
231+
use crate::os::Os;
232+
use std::collections::HashMap;
233+
234+
// Create OS instance for tool execution
235+
let os = Os::new().await?;
236+
237+
// Parse tool arguments
238+
let default_args = serde_json::Value::Object(serde_json::Map::new());
239+
let args = substituted_args.as_ref().unwrap_or(&default_args);
240+
241+
// Create tool instance based on name and arguments
242+
let tool = match tool_name {
243+
"context" => {
244+
let context_tool: crate::cli::chat::tools::context::Context =
245+
serde_json::from_value(args.clone())?;
246+
Tool::Context(context_tool)
247+
},
248+
"fs_read" => {
249+
let fs_read_tool: crate::cli::chat::tools::fs_read::FsRead =
250+
serde_json::from_value(args.clone())?;
251+
Tool::FsRead(fs_read_tool)
252+
},
253+
"fs_write" => {
254+
let fs_write_tool: crate::cli::chat::tools::fs_write::FsWrite =
255+
serde_json::from_value(args.clone())?;
256+
Tool::FsWrite(fs_write_tool)
257+
},
258+
"execute_bash" | "execute_cmd" => {
259+
let execute_tool: crate::cli::chat::tools::execute::ExecuteCommand =
260+
serde_json::from_value(args.clone())?;
261+
Tool::ExecuteCommand(execute_tool)
262+
},
263+
"use_aws" => {
264+
let aws_tool: crate::cli::chat::tools::use_aws::UseAws =
265+
serde_json::from_value(args.clone())?;
266+
Tool::UseAws(aws_tool)
267+
},
268+
_ => return Err(eyre!("Unsupported tool: {}", tool_name)),
269+
};
270+
271+
// Execute the tool
272+
let mut output = Vec::new();
273+
let mut line_tracker = HashMap::new();
274+
let invoke_result = tool.invoke(&os, &mut output, &mut line_tracker, None).await?;
275+
276+
let result = invoke_result.as_str().to_string();
277+
278+
// Return the tool result as string
279+
Ok(result)
280+
}
193281

282+
async fn execute_command(
283+
&self,
284+
command: &str,
285+
hook: &Hook,
286+
prompt: Option<&str>,
287+
) -> Result<String> {
194288
#[cfg(unix)]
195289
let mut cmd = tokio::process::Command::new("bash");
196290
#[cfg(unix)]
@@ -211,7 +305,7 @@ impl HookExecutor {
211305
.stdout(Stdio::piped())
212306
.stderr(Stdio::piped());
213307

214-
let timeout = Duration::from_millis(hook.1.timeout_ms);
308+
let timeout = Duration::from_millis(hook.timeout_ms);
215309

216310
// Set USER_PROMPT environment variable if provided
217311
if let Some(prompt) = prompt {
@@ -223,14 +317,14 @@ impl HookExecutor {
223317
let command_future = cmd.output();
224318

225319
// Run with timeout
226-
let result = match tokio::time::timeout(timeout, command_future).await {
320+
match tokio::time::timeout(timeout, command_future).await {
227321
Ok(Ok(result)) => {
228322
if result.status.success() {
229323
let stdout = result.stdout.to_str_lossy();
230324
let stdout = format!(
231325
"{}{}",
232-
truncate_safe(&stdout, hook.1.max_output_size),
233-
if stdout.len() > hook.1.max_output_size {
326+
truncate_safe(&stdout, hook.max_output_size),
327+
if stdout.len() > hook.max_output_size {
234328
" ... truncated"
235329
} else {
236330
""
@@ -243,9 +337,7 @@ impl HookExecutor {
243337
},
244338
Ok(Err(err)) => Err(eyre!("failed to execute command: {}", err)),
245339
Err(_) => Err(eyre!("command timed out after {} ms", timeout.as_millis())),
246-
};
247-
248-
(hook, result, start_time.elapsed())
340+
}
249341
}
250342

251343
/// Will return a cached hook's output if it exists and isn't expired.
@@ -303,7 +395,14 @@ impl HooksArgs {
303395
true => writeln!(&mut out, "<none>")?,
304396
false => {
305397
for hook in hooks {
306-
writeln!(&mut out, " - {}", hook.command)?;
398+
let hook_desc = if let Some(tool_name) = &hook.tool_name {
399+
format!("tool: {}", tool_name)
400+
} else if let Some(command) = &hook.command {
401+
command.clone()
402+
} else {
403+
"unknown hook".to_string()
404+
};
405+
writeln!(&mut out, " - {}", hook_desc)?;
307406
}
308407
},
309408
}

0 commit comments

Comments
 (0)