Skip to content

Commit 8b8590d

Browse files
fix: prompt caching, and adding docs references (#2421)
1 parent dc0974f commit 8b8590d

File tree

9 files changed

+118
-57
lines changed

9 files changed

+118
-57
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ security-framework = "3.2.0"
9292
semantic_search_client = { path = "crates/semantic-search-client" }
9393
semver = { version = "1.0.26", features = ["serde"] }
9494
serde = { version = "1.0.219", features = ["derive", "rc"] }
95-
serde_json = "1.0.140"
95+
serde_json = { version = "1.0.140", features = ["preserve_order"] }
9696
sha2 = "0.10.9"
9797
shell-color = "1.0.0"
9898
shell-words = "1.1.0"

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use crossterm::{
1010
style,
1111
};
1212

13-
use crate::cli::chat::consts::CONTEXT_FILES_MAX_SIZE;
13+
use crate::cli::chat::consts::{
14+
AGENT_FORMAT_HOOKS_DOC_URL,
15+
CONTEXT_FILES_MAX_SIZE,
16+
};
1417
use crate::cli::chat::token_counter::TokenCounter;
1518
use crate::cli::chat::util::drop_matched_context_files;
1619
use crate::cli::chat::{
@@ -281,12 +284,13 @@ impl ContextSubcommand {
281284
execute!(
282285
session.stderr,
283286
style::SetForegroundColor(Color::Yellow),
284-
style::Print("The /context hooks command is deprecated. Use "),
287+
style::Print(
288+
"The /context hooks command is deprecated.\n\nConfigure hooks directly with your agent instead: "
289+
),
285290
style::SetForegroundColor(Color::Green),
286-
style::Print("/hooks"),
287-
style::SetForegroundColor(Color::Yellow),
288-
style::Print(" instead.\n\n"),
289-
style::SetForegroundColor(Color::Reset)
291+
style::Print(AGENT_FORMAT_HOOKS_DOC_URL),
292+
style::SetForegroundColor(Color::Reset),
293+
style::Print("\n"),
290294
)?;
291295
},
292296
}

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use bstr::ByteSlice;
1010
use clap::Args;
1111
use crossterm::style::{
1212
self,
13+
Attribute,
1314
Color,
1415
Stylize,
1516
};
@@ -36,6 +37,7 @@ use crate::cli::agent::hook::{
3637
Hook,
3738
HookTrigger,
3839
};
40+
use crate::cli::chat::consts::AGENT_FORMAT_HOOKS_DOC_URL;
3941
use crate::cli::chat::util::truncate_safe;
4042
use crate::cli::chat::{
4143
ChatError,
@@ -67,7 +69,7 @@ impl HookExecutor {
6769
/// If `updates` is `Some`, progress on hook execution will be written to it.
6870
/// Errors encountered with write operations to `updates` are ignored.
6971
///
70-
/// Note: [`HookTrigger::ConversationStart`] hooks never leave the cache.
72+
/// Note: [`HookTrigger::AgentSpawn`] hooks never leave the cache.
7173
pub async fn run_hooks(
7274
&mut self,
7375
hooks: HashMap<HookTrigger, Vec<Hook>>,
@@ -175,6 +177,8 @@ impl HookExecutor {
175177
});
176178
}
177179

180+
results.append(&mut cached);
181+
178182
Ok(results)
179183
}
180184

@@ -275,6 +279,8 @@ fn sanitize_user_prompt(input: &str) -> String {
275279
before_long_help = "Use context hooks to specify shell commands to run. The output from these
276280
commands will be appended to the prompt to Amazon Q.
277281
282+
Refer to the documentation for how to configure hooks with your agent: https://github.com/aws/amazon-q-developer-cli/blob/main/docs/agent-format.md#hooks-field
283+
278284
Notes:
279285
• Hooks are executed in parallel
280286
• 'conversation_start' hooks run on the first user prompt and are attached once to the conversation history sent to Amazon Q
@@ -290,18 +296,34 @@ impl HooksArgs {
290296
});
291297
};
292298

299+
let mut out = Vec::new();
293300
for (trigger, hooks) in &context_manager.hooks {
294-
writeln!(session.stdout, "{trigger}:")?;
301+
writeln!(&mut out, "{trigger}:")?;
295302
match hooks.is_empty() {
296-
true => writeln!(session.stdout, "<none>")?,
303+
true => writeln!(&mut out, "<none>")?,
297304
false => {
298305
for hook in hooks {
299-
writeln!(session.stdout, " - {}", hook.command)?;
306+
writeln!(&mut out, " - {}", hook.command)?;
300307
}
301308
},
302309
}
303310
}
304311

312+
if out.is_empty() {
313+
queue!(
314+
session.stderr,
315+
style::Print(
316+
"No hooks are configured.\n\nRefer to the documentation for how to add hooks to your agent: "
317+
),
318+
style::SetForegroundColor(Color::Green),
319+
style::Print(AGENT_FORMAT_HOOKS_DOC_URL),
320+
style::SetAttribute(Attribute::Reset),
321+
style::Print("\n"),
322+
)?;
323+
} else {
324+
session.stdout.write_all(&out)?;
325+
}
326+
305327
Ok(ChatState::PromptUser {
306328
skip_printing_tools: true,
307329
})

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ pub enum SlashCommand {
6262
PromptEditor(EditorArgs),
6363
/// Summarize the conversation to free up context space
6464
Compact(CompactArgs),
65-
/// View and manage tools and permissions
65+
/// View tools and permissions
6666
Tools(ToolsArgs),
6767
/// Create a new Github issue or make a feature request
6868
Issue(issue::IssueArgs),
6969
/// View and retrieve prompts
7070
Prompts(PromptsArgs),
71-
/// View and manage context hooks
71+
/// View context hooks
7272
Hooks(HooksArgs),
7373
/// Show current session's context window usage
7474
Usage(UsageArgs),

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ use crossterm::{
1919

2020
use crate::api_client::model::Tool as FigTool;
2121
use crate::cli::agent::Agent;
22-
use crate::cli::chat::consts::DUMMY_TOOL_NAME;
22+
use crate::cli::chat::consts::{
23+
AGENT_FORMAT_TOOLS_DOC_URL,
24+
DUMMY_TOOL_NAME,
25+
};
2326
use crate::cli::chat::tools::ToolOrigin;
2427
use crate::cli::chat::{
2528
ChatError,
@@ -150,19 +153,19 @@ impl ToolsArgs {
150153
}
151154
}
152155

153-
queue!(
154-
session.stderr,
155-
style::Print("\nTrusted tools will run without confirmation."),
156-
style::SetForegroundColor(Color::DarkGrey),
157-
style::Print(format!("\n{}\n", "* Default settings")),
158-
style::Print("\n💡 Use "),
159-
style::SetForegroundColor(Color::Green),
160-
style::Print("/tools help"),
161-
style::SetForegroundColor(Color::Reset),
162-
style::SetForegroundColor(Color::DarkGrey),
163-
style::Print(" to edit permissions.\n\n"),
164-
style::SetForegroundColor(Color::Reset),
165-
)?;
156+
if origin_tools.is_empty() {
157+
queue!(
158+
session.stderr,
159+
style::Print(
160+
"\nNo tools are currently enabled.\n\nRefer to the documentation for how to add tools to your agent: "
161+
),
162+
style::SetForegroundColor(Color::Green),
163+
style::Print(AGENT_FORMAT_TOOLS_DOC_URL),
164+
style::SetForegroundColor(Color::Reset),
165+
style::Print("\n"),
166+
style::SetForegroundColor(Color::Reset),
167+
)?;
168+
}
166169

167170
Ok(ChatState::default())
168171
}
@@ -176,7 +179,9 @@ impl ToolsArgs {
176179
#[derive(Debug, PartialEq, Subcommand)]
177180
#[command(
178181
before_long_help = "By default, Amazon Q will ask for your permission to use certain tools. You can control which tools you
179-
trust so that no confirmation is required. These settings will last only for this session."
182+
trust so that no confirmation is required.
183+
184+
Refer to the documentation for how to configure tools with your agent: https://github.com/aws/amazon-q-developer-cli/blob/main/docs/agent-format.md#tools-field"
180185
)]
181186
pub enum ToolsSubcommand {
182187
/// Show the input schema for all available tools

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,9 @@ pub const MAX_NUMBER_OF_IMAGES_PER_REQUEST: usize = 10;
2626

2727
/// In bytes - 10 MB
2828
pub const MAX_IMAGE_SIZE: usize = 10 * 1024 * 1024;
29+
30+
pub const AGENT_FORMAT_HOOKS_DOC_URL: &str =
31+
"https://github.com/aws/amazon-q-developer-cli/blob/main/docs/agent-format.md#hooks-field";
32+
33+
pub const AGENT_FORMAT_TOOLS_DOC_URL: &str =
34+
"https://github.com/aws/amazon-q-developer-cli/blob/main/docs/agent-format.md#tools-field";

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,13 @@ impl ContextManager {
176176
/// A vector containing pairs of a [`Hook`] definition and its execution output
177177
pub async fn run_hooks(
178178
&mut self,
179+
trigger: HookTrigger,
179180
output: &mut impl Write,
180181
prompt: Option<&str>,
181182
) -> Result<Vec<((HookTrigger, Hook), String)>, ChatError> {
182-
self.hook_executor.run_hooks(self.hooks.clone(), output, prompt).await
183+
let mut hooks = self.hooks.clone();
184+
hooks.retain(|t, _| *t == trigger);
185+
self.hook_executor.run_hooks(hooks, output, prompt).await
183186
}
184187
}
185188

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

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -328,14 +328,14 @@ impl ConversationState {
328328
&mut self,
329329
os: &Os,
330330
stderr: &mut impl Write,
331-
run_hooks: bool,
331+
run_perprompt_hooks: bool,
332332
) -> Result<FigConversationState, ChatError> {
333333
debug_assert!(self.next_message.is_some());
334334
self.enforce_conversation_invariants();
335335
self.history.drain(self.valid_history_range.1..);
336336
self.history.drain(..self.valid_history_range.0);
337337

338-
let context = self.backend_conversation_state(os, run_hooks, stderr).await?;
338+
let context = self.backend_conversation_state(os, run_perprompt_hooks, stderr).await?;
339339
if !context.dropped_context_files.is_empty() {
340340
execute!(
341341
stderr,
@@ -389,28 +389,30 @@ impl ConversationState {
389389
pub async fn backend_conversation_state(
390390
&mut self,
391391
os: &Os,
392-
run_hooks: bool,
392+
run_perprompt_hooks: bool,
393393
output: &mut impl Write,
394394
) -> Result<BackendConversationState<'_>, ChatError> {
395395
self.update_state(false).await;
396396
self.enforce_conversation_invariants();
397397

398398
// Run hooks and add to conversation start and next user message.
399-
let mut conversation_start_context = None;
400-
if let (true, Some(cm)) = (run_hooks, self.context_manager.as_mut()) {
401-
// Get the user prompt from next_message if available
399+
let mut agent_spawn_context = None;
400+
if let Some(cm) = self.context_manager.as_mut() {
402401
let user_prompt = self.next_message.as_ref().and_then(|m| m.prompt());
403-
let hook_results = cm.run_hooks(output, user_prompt).await?;
404-
405-
conversation_start_context = Some(format_hook_context(&hook_results, HookTrigger::AgentSpawn));
406-
407-
// add per prompt content to next_user_message if available
408-
if let Some(next_message) = self.next_message.as_mut() {
409-
next_message.additional_context = format_hook_context(&hook_results, HookTrigger::UserPromptSubmit);
402+
let agent_spawn = cm.run_hooks(HookTrigger::AgentSpawn, output, user_prompt).await?;
403+
agent_spawn_context = format_hook_context(&agent_spawn, HookTrigger::AgentSpawn);
404+
405+
if let (true, Some(next_message)) = (run_perprompt_hooks, self.next_message.as_mut()) {
406+
let per_prompt = cm
407+
.run_hooks(HookTrigger::UserPromptSubmit, output, next_message.prompt())
408+
.await?;
409+
if let Some(ctx) = format_hook_context(&per_prompt, HookTrigger::UserPromptSubmit) {
410+
next_message.additional_context = ctx;
411+
}
410412
}
411413
}
412414

413-
let (context_messages, dropped_context_files) = self.context_messages(os, conversation_start_context).await;
415+
let (context_messages, dropped_context_files) = self.context_messages(os, agent_spawn_context).await;
414416

415417
Ok(BackendConversationState {
416418
conversation_id: self.conversation_id.as_str(),
@@ -556,7 +558,7 @@ impl ConversationState {
556558
async fn context_messages(
557559
&mut self,
558560
os: &Os,
559-
conversation_start_context: Option<String>,
561+
additional_context: Option<String>,
560562
) -> (Option<Vec<HistoryEntry>>, Vec<(String, String)>) {
561563
let mut context_content = String::new();
562564
let mut dropped_context_files = Vec::new();
@@ -591,7 +593,7 @@ impl ConversationState {
591593
}
592594
}
593595

594-
if let Some(context) = conversation_start_context {
596+
if let Some(context) = additional_context {
595597
context_content.push_str(&context);
596598
}
597599

@@ -765,7 +767,17 @@ impl From<InputSchema> for ToolInputSchema {
765767
}
766768
}
767769

768-
fn format_hook_context(hook_results: &[((HookTrigger, Hook), String)], trigger: HookTrigger) -> String {
770+
/// Formats hook output to be used within context blocks (e.g., in context messages or in new user
771+
/// prompts).
772+
///
773+
/// # Returns
774+
/// [Option::Some] if `hook_results` is not empty and at least one hook has content. Otherwise,
775+
/// [Option::None]
776+
fn format_hook_context(hook_results: &[((HookTrigger, Hook), String)], trigger: HookTrigger) -> Option<String> {
777+
if hook_results.iter().all(|(_, content)| content.is_empty()) {
778+
return None;
779+
}
780+
769781
let mut context_content = String::new();
770782

771783
context_content.push_str(CONTEXT_ENTRY_START_HEADER);
@@ -779,7 +791,7 @@ fn format_hook_context(hook_results: &[((HookTrigger, Hook), String)], trigger:
779791
context_content.push_str(&format!("{output}\n\n"));
780792
}
781793
context_content.push_str(CONTEXT_ENTRY_END_HEADER);
782-
context_content
794+
Some(context_content)
783795
}
784796

785797
fn enforce_conversation_invariants(

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,10 @@ impl UserMessage {
152152
/// Converts this message into a [UserInputMessage] to be stored in the history of
153153
/// [api_client::model::ConversationState].
154154
pub fn into_history_entry(self) -> UserInputMessage {
155+
let content = self.content_with_context();
155156
UserInputMessage {
156157
images: self.images.clone(),
157-
content: self.prompt().unwrap_or_default().to_string(),
158+
content,
158159
user_input_message_context: Some(UserInputMessageContext {
159160
env_state: self.env_context.env_state,
160161
tool_results: match self.content {
@@ -179,17 +180,10 @@ impl UserMessage {
179180
model_id: Option<String>,
180181
tools: &HashMap<ToolOrigin, Vec<Tool>>,
181182
) -> UserInputMessage {
182-
let formatted_prompt = match self.prompt() {
183-
Some(prompt) if !prompt.is_empty() => {
184-
format!("{}{}{}", USER_ENTRY_START_HEADER, prompt, USER_ENTRY_END_HEADER)
185-
},
186-
_ => String::new(),
187-
};
183+
let content = self.content_with_context();
188184
UserInputMessage {
189185
images: self.images,
190-
content: format!("{} {}", self.additional_context, formatted_prompt)
191-
.trim()
192-
.to_string(),
186+
content,
193187
user_input_message_context: Some(UserInputMessageContext {
194188
env_state: self.env_context.env_state,
195189
tool_results: match self.content {
@@ -270,6 +264,21 @@ impl UserMessage {
270264
self.content = UserMessageContent::Prompt { prompt };
271265
}
272266
}
267+
268+
/// Returns a formatted [String] containing [Self::additional_context] and [Self::prompt].
269+
fn content_with_context(&self) -> String {
270+
match (self.additional_context.is_empty(), self.prompt()) {
271+
// Only add special delimiters if we have both a prompt and additional context
272+
(false, Some(prompt)) => format!(
273+
"{} {}{}{}",
274+
self.additional_context, USER_ENTRY_START_HEADER, prompt, USER_ENTRY_END_HEADER
275+
),
276+
(true, Some(prompt)) => prompt.to_string(),
277+
_ => self.additional_context.clone(),
278+
}
279+
.trim()
280+
.to_string()
281+
}
273282
}
274283

275284
#[derive(Debug, Clone, Serialize, Deserialize)]

0 commit comments

Comments
 (0)