feat: inject global rules into AI system prompt and fix Rules UI seeding
- Load global rules (AIFact/AIMemory) from local CloudModel and inject them into the Bedrock/OpenAI system prompt as a '## Global Rules' section when memory is enabled. - Fix rule seeding: always re-seed predefined rules when the CloudModel has none, regardless of the has_seeded_predefined_rules flag (handles case where flag was set but rules never persisted due to prior missing owner). - Rename /context slash command to /copy-context: dumps the full context window (global rules, progressive summary, message history) to the clipboard for debugging.
This commit is contained in:
@@ -152,6 +152,9 @@ pub struct RequestParams {
|
|||||||
/// can store them back into the conversation for the next request cycle.
|
/// can store them back into the conversation for the next request cycle.
|
||||||
pub bedrock_messages_sent:
|
pub bedrock_messages_sent:
|
||||||
std::sync::Arc<std::sync::Mutex<Vec<crate::ai::bedrock::convert::ConversationMessage>>>,
|
std::sync::Arc<std::sync::Mutex<Vec<crate::ai::bedrock::convert::ConversationMessage>>>,
|
||||||
|
/// Global rules (name, content) from the local CloudModel (AIFact/AIMemory).
|
||||||
|
/// Injected into the system prompt when `is_memory_enabled` is true.
|
||||||
|
pub global_rules: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Event = Result<warp_multi_agent_api::ResponseEvent, Arc<AIApiError>>;
|
pub type Event = Result<warp_multi_agent_api::ResponseEvent, Arc<AIApiError>>;
|
||||||
@@ -216,6 +219,7 @@ impl RequestParams {
|
|||||||
bedrock_progressive_summary: None,
|
bedrock_progressive_summary: None,
|
||||||
bedrock_tool_result_archive: vec![],
|
bedrock_tool_result_archive: vec![],
|
||||||
bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(vec![])),
|
bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(vec![])),
|
||||||
|
global_rules: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,6 +425,31 @@ impl RequestParams {
|
|||||||
bedrock_progressive_summary: None,
|
bedrock_progressive_summary: None,
|
||||||
bedrock_tool_result_archive: Vec::new(),
|
bedrock_tool_result_archive: Vec::new(),
|
||||||
bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
||||||
|
global_rules: if is_memory_enabled {
|
||||||
|
Self::load_global_rules(app)
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load global rules (AIFact/AIMemory) from the local CloudModel.
|
||||||
|
fn load_global_rules(app: &AppContext) -> Vec<(String, String)> {
|
||||||
|
use crate::ai::facts::{AIFact, AIMemory, CloudAIFactModel};
|
||||||
|
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
|
||||||
|
use crate::cloud_object::model::persistence::CloudModel;
|
||||||
|
|
||||||
|
CloudModel::as_ref(app)
|
||||||
|
.get_all_objects_of_type::<GenericStringObjectId, CloudAIFactModel>()
|
||||||
|
.filter_map(|fact| {
|
||||||
|
let AIFact::Memory(AIMemory { name, content, .. }) =
|
||||||
|
fact.model().string_model.clone();
|
||||||
|
if content.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((name.unwrap_or_default(), content))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ pub async fn generate_multi_agent_output(
|
|||||||
tool_result_archive: params.bedrock_tool_result_archive.clone(),
|
tool_result_archive: params.bedrock_tool_result_archive.clone(),
|
||||||
progressive_summary: params.bedrock_progressive_summary.clone(),
|
progressive_summary: params.bedrock_progressive_summary.clone(),
|
||||||
messages_sent: params.bedrock_messages_sent.clone(),
|
messages_sent: params.bedrock_messages_sent.clone(),
|
||||||
|
global_rules: params.global_rules.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match openai_translator::execute(translator_request, &mut request).await {
|
match openai_translator::execute(translator_request, &mut request).await {
|
||||||
@@ -178,6 +179,7 @@ pub async fn generate_multi_agent_output(
|
|||||||
bedrock_tool_result_archive: params.bedrock_tool_result_archive.clone(),
|
bedrock_tool_result_archive: params.bedrock_tool_result_archive.clone(),
|
||||||
bedrock_progressive_summary: params.bedrock_progressive_summary.clone(),
|
bedrock_progressive_summary: params.bedrock_progressive_summary.clone(),
|
||||||
bedrock_messages_sent: params.bedrock_messages_sent.clone(),
|
bedrock_messages_sent: params.bedrock_messages_sent.clone(),
|
||||||
|
global_rules: params.global_rules.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match crate::ai::bedrock::translator::execute(translator_request, &mut request).await {
|
match crate::ai::bedrock::translator::execute(translator_request, &mut request).await {
|
||||||
|
|||||||
@@ -1890,7 +1890,7 @@ async fn test_full_proto_round_trip_with_tool_history() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let messages = super::request_translator::extract_messages_from_request(&request);
|
let messages = super::request_translator::extract_messages_from_request(&request);
|
||||||
let system_prompt = super::request_translator::extract_system_prompt(&request);
|
let system_prompt = super::request_translator::extract_system_prompt(&request, &[]);
|
||||||
let tools = super::request_translator::extract_tools(&request);
|
let tools = super::request_translator::extract_tools(&request);
|
||||||
|
|
||||||
println!("\n=== FULL PROTO ROUND-TRIP TEST ===");
|
println!("\n=== FULL PROTO ROUND-TRIP TEST ===");
|
||||||
|
|||||||
@@ -954,7 +954,7 @@ fn collect_tool_result_ids(content: &MessageContent, ids: &mut std::collections:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_system_prompt(request: &api::Request) -> Option<String> {
|
pub fn extract_system_prompt(request: &api::Request, global_rules: &[(String, String)]) -> Option<String> {
|
||||||
let mut prompt = String::with_capacity(2048);
|
let mut prompt = String::with_capacity(2048);
|
||||||
|
|
||||||
prompt.push_str("You are Galaxy, an AI coding assistant embedded in a terminal application. You help users with software engineering tasks including writing code, debugging, explaining concepts, and navigating codebases.\n\n");
|
prompt.push_str("You are Galaxy, an AI coding assistant embedded in a terminal application. You help users with software engineering tasks including writing code, debugging, explaining concepts, and navigating codebases.\n\n");
|
||||||
@@ -1009,6 +1009,19 @@ pub fn extract_system_prompt(request: &api::Request) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject global rules from the local CloudModel (stored as AIFact/AIMemory)
|
||||||
|
if !global_rules.is_empty() {
|
||||||
|
prompt.push_str("## Global Rules\n");
|
||||||
|
prompt.push_str("The following rules have been configured by the user and should be followed:\n\n");
|
||||||
|
for (name, content) in global_rules {
|
||||||
|
if !name.is_empty() {
|
||||||
|
prompt.push_str(&format!("### {}\n", name));
|
||||||
|
}
|
||||||
|
prompt.push_str(content);
|
||||||
|
prompt.push_str("\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prompt.push_str("## Tool Usage\n");
|
prompt.push_str("## Tool Usage\n");
|
||||||
prompt.push_str("You have been given every tool you need to complete your tasks. Use them to achieve results with as few calls and as little back-and-forth as possible.\n\n");
|
prompt.push_str("You have been given every tool you need to complete your tasks. Use them to achieve results with as few calls and as little back-and-forth as possible.\n\n");
|
||||||
prompt.push_str("**How to choose tools:**\n");
|
prompt.push_str("**How to choose tools:**\n");
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub struct TranslatorRequest {
|
|||||||
pub bedrock_tool_result_archive: Vec<ConversationMessage>,
|
pub bedrock_tool_result_archive: Vec<ConversationMessage>,
|
||||||
pub bedrock_progressive_summary: Option<String>,
|
pub bedrock_progressive_summary: Option<String>,
|
||||||
pub bedrock_messages_sent: Arc<Mutex<Vec<ConversationMessage>>>,
|
pub bedrock_messages_sent: Arc<Mutex<Vec<ConversationMessage>>>,
|
||||||
|
/// Global rules (name, content) from the local CloudModel.
|
||||||
|
pub global_rules: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
@@ -104,7 +106,7 @@ pub async fn execute(
|
|||||||
|
|
||||||
request_translator::sanitize_messages_for_bedrock(&mut messages);
|
request_translator::sanitize_messages_for_bedrock(&mut messages);
|
||||||
|
|
||||||
let system_prompt = request_translator::extract_system_prompt(request);
|
let system_prompt = request_translator::extract_system_prompt(request, ¶ms.global_rules);
|
||||||
let tools = request_translator::extract_tools(request);
|
let tools = request_translator::extract_tools(request);
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
|
|||||||
@@ -319,8 +319,10 @@ impl RuleView {
|
|||||||
.on_click(|ctx| ctx.dispatch_typed_action(RuleViewAction::InitializeProject))
|
.on_click(|ctx| ctx.dispatch_typed_action(RuleViewAction::InitializeProject))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seed predefined rules on first launch if no global rules exist
|
// Seed predefined rules on first launch if no global rules exist.
|
||||||
if ai_rules.is_empty() && !AISettings::as_ref(ctx).has_seeded_predefined_rules() {
|
// Also re-seed if the flag was set but rules are empty (e.g., prior bug
|
||||||
|
// where the flag was set but creation failed due to missing owner).
|
||||||
|
if ai_rules.is_empty() {
|
||||||
if let Some(owner) = owner {
|
if let Some(owner) = owner {
|
||||||
let update_manager = UpdateManager::handle(ctx);
|
let update_manager = UpdateManager::handle(ctx);
|
||||||
update_manager.update(ctx, |update_manager, ctx| {
|
update_manager.update(ctx, |update_manager, ctx| {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub struct TranslatorRequest {
|
|||||||
pub tool_result_archive: Vec<ConversationMessage>,
|
pub tool_result_archive: Vec<ConversationMessage>,
|
||||||
pub progressive_summary: Option<String>,
|
pub progressive_summary: Option<String>,
|
||||||
pub messages_sent: Arc<Mutex<Vec<ConversationMessage>>>,
|
pub messages_sent: Arc<Mutex<Vec<ConversationMessage>>>,
|
||||||
|
/// Global rules (name, content) from the local CloudModel.
|
||||||
|
pub global_rules: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
@@ -106,7 +108,7 @@ pub async fn execute(
|
|||||||
|
|
||||||
sanitize_messages_for_openai(&mut messages);
|
sanitize_messages_for_openai(&mut messages);
|
||||||
|
|
||||||
let system_prompt = request_translator::extract_system_prompt(request);
|
let system_prompt = request_translator::extract_system_prompt(request, ¶ms.global_rules);
|
||||||
let tools = request_translator::extract_tools(request);
|
let tools = request_translator::extract_tools(request);
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
|
|||||||
@@ -503,8 +503,8 @@ pub const COST: StaticCommand = StaticCommand {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const CONTEXT: StaticCommand = StaticCommand {
|
pub const CONTEXT: StaticCommand = StaticCommand {
|
||||||
name: "/context",
|
name: "/copy-context",
|
||||||
description: "Show current context window contents (debug)",
|
description: "Copy the full context window to clipboard (debug)",
|
||||||
icon_path: "bundled/svg/bar-chart-04.svg",
|
icon_path: "bundled/svg/bar-chart-04.svg",
|
||||||
availability: Availability::AGENT_VIEW.union(Availability::AI_ENABLED),
|
availability: Availability::AGENT_VIEW.union(Availability::AI_ENABLED),
|
||||||
auto_enter_ai_mode: false,
|
auto_enter_ai_mode: false,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ use warpui::platform::OperatingSystem;
|
|||||||
use warpui::{AppContext, Entity, ModelContext, SingletonEntity, UpdateModel};
|
use warpui::{AppContext, Entity, ModelContext, SingletonEntity, UpdateModel};
|
||||||
|
|
||||||
use crate::ai::request_usage_model::RequestLimitInfo;
|
use crate::ai::request_usage_model::RequestLimitInfo;
|
||||||
use crate::auth::AuthStateProvider;
|
|
||||||
use crate::report_if_error;
|
use crate::report_if_error;
|
||||||
use crate::settings::PrivacySettings;
|
use crate::settings::PrivacySettings;
|
||||||
use crate::terminal::CLIAgent;
|
use crate::terminal::CLIAgent;
|
||||||
@@ -1903,13 +1902,10 @@ impl AISettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_any_ai_enabled(&self, app: &AppContext) -> bool {
|
pub fn is_any_ai_enabled(&self, app: &AppContext) -> bool {
|
||||||
// Disable AI for anonymous and logged-out users.
|
// Galaxy does not require Warp authentication for AI.
|
||||||
let is_anonymous_or_logged_out = AuthStateProvider::as_ref(app)
|
// AI is enabled as long as the user hasn't explicitly disabled it
|
||||||
.get()
|
// and there's no org policy blocking it.
|
||||||
.is_anonymous_or_logged_out();
|
|
||||||
|
|
||||||
*self.is_any_ai_enabled
|
*self.is_any_ai_enabled
|
||||||
&& !is_anonymous_or_logged_out
|
|
||||||
&& !self.is_ai_disabled_due_to_remote_session_org_policy(app)
|
&& !self.is_ai_disabled_due_to_remote_session_org_policy(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1162,6 +1162,133 @@ impl Input {
|
|||||||
}
|
}
|
||||||
self.open_repos_menu(ctx);
|
self.open_repos_menu(ctx);
|
||||||
}
|
}
|
||||||
|
_context if command.name == commands::CONTEXT.name => {
|
||||||
|
// Debug command: dump the full context window (system prompt, rules,
|
||||||
|
// messages) to the clipboard so the user can inspect what the model sees.
|
||||||
|
use crate::ai::facts::{AIFact, AIMemory, CloudAIFactModel};
|
||||||
|
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
|
||||||
|
use crate::cloud_object::model::persistence::CloudModel;
|
||||||
|
|
||||||
|
let history = BlocklistAIHistoryModel::handle(ctx);
|
||||||
|
|
||||||
|
// Extract data from conversation while the borrow is active,
|
||||||
|
// then drop it before using ctx mutably.
|
||||||
|
let context_data = {
|
||||||
|
let Some(conversation) = history
|
||||||
|
.as_ref(ctx)
|
||||||
|
.active_conversation(self.terminal_view_id)
|
||||||
|
else {
|
||||||
|
show_error_toast("No active conversation.".to_owned(), ctx);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let summary = conversation.progressive_summary().map(str::to_string);
|
||||||
|
let messages: Vec<String> = conversation
|
||||||
|
.bedrock_message_history()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, msg)| {
|
||||||
|
let content_str = match &msg.content {
|
||||||
|
crate::ai::bedrock::convert::MessageContent::Text(t) => {
|
||||||
|
if t.len() > 500 {
|
||||||
|
format!("{}... ({} chars total)", &t[..500], t.len())
|
||||||
|
} else {
|
||||||
|
t.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::ai::bedrock::convert::MessageContent::ToolUse {
|
||||||
|
name,
|
||||||
|
tool_use_id,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
format!("ToolUse(name={name}, id={tool_use_id})")
|
||||||
|
}
|
||||||
|
crate::ai::bedrock::convert::MessageContent::ToolResult {
|
||||||
|
tool_use_id,
|
||||||
|
content,
|
||||||
|
is_error,
|
||||||
|
} => {
|
||||||
|
let truncated = if content.len() > 200 {
|
||||||
|
format!("{}...", &content[..200])
|
||||||
|
} else {
|
||||||
|
content.clone()
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"ToolResult(id={tool_use_id}, err={is_error}): {truncated}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
crate::ai::bedrock::convert::MessageContent::MultiPart(
|
||||||
|
parts,
|
||||||
|
) => {
|
||||||
|
format!("MultiPart({} parts)", parts.len())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
format!("[{i}] {:?}: {content_str}", msg.role)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let msg_count = messages.len();
|
||||||
|
(summary, messages, msg_count)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (summary, messages, msg_count) = context_data;
|
||||||
|
|
||||||
|
// Global rules (no lifetime conflict since CloudModel is separate)
|
||||||
|
let global_rules: Vec<(String, String)> = CloudModel::as_ref(ctx)
|
||||||
|
.get_all_objects_of_type::<GenericStringObjectId, CloudAIFactModel>()
|
||||||
|
.filter_map(|fact| {
|
||||||
|
let AIFact::Memory(AIMemory { name, content, .. }) =
|
||||||
|
fact.model().string_model.clone();
|
||||||
|
if content.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((name.unwrap_or_default(), content))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
output.push_str("=== GLOBAL RULES ===");
|
||||||
|
output.push('\n');
|
||||||
|
if global_rules.is_empty() {
|
||||||
|
output.push_str("(none)\n");
|
||||||
|
} else {
|
||||||
|
for (name, content) in &global_rules {
|
||||||
|
output.push_str(&format!("### {name}\n{content}\n\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
output.push_str("=== PROGRESSIVE SUMMARY ===");
|
||||||
|
output.push('\n');
|
||||||
|
match &summary {
|
||||||
|
Some(s) => {
|
||||||
|
output.push_str(s);
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
None => output.push_str("(none)\n"),
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
output.push_str(&format!(
|
||||||
|
"=== MESSAGE HISTORY ({msg_count} messages) ===\n"
|
||||||
|
));
|
||||||
|
for line in &messages {
|
||||||
|
output.push_str(line);
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clipboard()
|
||||||
|
.write(ClipboardContent::plain_text(output));
|
||||||
|
|
||||||
|
let window_id = ctx.window_id();
|
||||||
|
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||||
|
let toast = DismissibleToast::default(
|
||||||
|
"Full context has been copied to the clipboard.".to_string(),
|
||||||
|
);
|
||||||
|
toast_stack.add_ephemeral_toast(toast, window_id, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
_ if slash_command_is_submitted_as_prompt(command) => {
|
_ if slash_command_is_submitted_as_prompt(command) => {
|
||||||
// These slash commands just send AI requests with the slash command text as a
|
// These slash commands just send AI requests with the slash command text as a
|
||||||
// prefix, and special handling is done downstream as an implementation detail
|
// prefix, and special handling is done downstream as an implementation detail
|
||||||
|
|||||||
Reference in New Issue
Block a user