Add hierarchical workspace instructions
This commit is contained in:
@@ -5,7 +5,7 @@ const TITLE_INSTRUCTION: &str = "Give this conversation a short title of at most
|
||||
Reply with the title alone: no quotes, no trailing period, no explanation.";
|
||||
const TITLE_MAX_TOKENS: i32 = 48;
|
||||
const TITLE_MAX_CHARS: usize = 60;
|
||||
const AGENTS_PREFIX: &str = "Project instructions from AGENTS.md:\n\n";
|
||||
const LEGACY_AGENTS_PREFIX: &str = "Project instructions from AGENTS.md:\n\n";
|
||||
|
||||
/// A one-shot generation that produces a session title. It runs against the
|
||||
/// transient KV cache, so it never creates or touches a stored session.
|
||||
@@ -23,7 +23,8 @@ pub(super) struct TitleRequest {
|
||||
pub(super) enum PendingContinuation {
|
||||
None,
|
||||
User(String),
|
||||
Tool(String),
|
||||
Tool(crate::agent::ToolRunResult),
|
||||
DurableTool(Vec<PathBuf>),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -36,15 +37,17 @@ pub(super) struct CompactionRequest {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Clone, Copy)]
|
||||
enum ToolCheckStage {
|
||||
Initial,
|
||||
AfterCompaction,
|
||||
BoundedError,
|
||||
ResultInitial,
|
||||
ResultAfterCompaction,
|
||||
BoundedResult,
|
||||
Instructions,
|
||||
InstructionsAfterCompaction,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) struct ToolResultCheck {
|
||||
pub(super) active: ActiveGeneration,
|
||||
result: String,
|
||||
result: crate::agent::ToolRunResult,
|
||||
stage: ToolCheckStage,
|
||||
}
|
||||
|
||||
@@ -63,6 +66,7 @@ pub(crate) struct ChatMessage {
|
||||
pub(super) content: String,
|
||||
pub(super) model_content: Option<String>,
|
||||
pub(super) tool_approval_reasons: Vec<Option<String>>,
|
||||
pub(super) instruction_metadata: Option<String>,
|
||||
pub(super) markdown: markdown::Content,
|
||||
pub(super) transcript: text_editor::Content,
|
||||
pub(super) a2ui_lines_processed: usize,
|
||||
@@ -235,6 +239,7 @@ impl From<StoredMessage> for ChatMessage {
|
||||
.as_deref()
|
||||
.and_then(|reasons| serde_json::from_str(reasons).ok())
|
||||
.unwrap_or_default(),
|
||||
instruction_metadata: message.instruction_metadata,
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
transcript: iced::widget::text_editor::Content::new(),
|
||||
a2ui_lines_processed: 0,
|
||||
@@ -364,16 +369,6 @@ fn has_chat_after_last_compaction(messages: &[ChatMessage]) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn project_agents(path: &Path) -> Result<Option<String>, String> {
|
||||
let path = path.join("AGENTS.md");
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(content) if content.is_empty() => Ok(None),
|
||||
Ok(content) => Ok(Some(content)),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(format!("Could not read {}: {error}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn title_context(messages: impl IntoIterator<Item = ChatTurn>) -> Vec<ChatTurn> {
|
||||
let mut started = false;
|
||||
@@ -388,30 +383,13 @@ fn title_context(messages: impl IntoIterator<Item = ChatTurn>) -> Vec<ChatTurn>
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn agents_prompt_for_turn(
|
||||
opening_turn: bool,
|
||||
opening_prompt: Option<String>,
|
||||
stored_prompt: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if opening_turn {
|
||||
opening_prompt
|
||||
} else {
|
||||
stored_prompt.map(str::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn chat_system_prompt(&self, model: ModelChoice, prompt: &str, agents: Option<&str>) -> String {
|
||||
fn chat_system_prompt(&self, model: ModelChoice, prompt: &str) -> String {
|
||||
let mut prompt = crate::agent::system_prompt(model, prompt, self.config.dev_brain.enabled);
|
||||
if self.config.a2ui_enabled {
|
||||
prompt.push_str("\n\n");
|
||||
prompt.push_str(crate::a2ui::SYSTEM_PROMPT);
|
||||
}
|
||||
if let Some(agents) = agents {
|
||||
prompt.push_str("\n\n");
|
||||
prompt.push_str(agents);
|
||||
}
|
||||
if let Some(skills) = crate::agent::agent_skills_prompt() {
|
||||
prompt.push_str("\n\n");
|
||||
prompt.push_str(&skills);
|
||||
@@ -419,6 +397,47 @@ impl App {
|
||||
prompt
|
||||
}
|
||||
|
||||
fn workspace_instruction_messages(
|
||||
&mut self,
|
||||
touched_paths: &[PathBuf],
|
||||
opening: bool,
|
||||
) -> Vec<SystemMessage> {
|
||||
let Some(root) = self
|
||||
.projects
|
||||
.iter()
|
||||
.find(|project| Some(project.project.id) == self.selected_project)
|
||||
.map(|project| PathBuf::from(&project.project.path))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let visible_start = compacted_context_start(&self.conversation);
|
||||
let reconciliation = crate::instructions::reconcile(
|
||||
&root,
|
||||
&application_support_path().join("AGENTS.md"),
|
||||
self.conversation
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, message)| {
|
||||
message.instruction_metadata.as_deref().map(|metadata| {
|
||||
crate::instructions::HistoryEntry {
|
||||
metadata,
|
||||
visible: index >= visible_start,
|
||||
}
|
||||
})
|
||||
}),
|
||||
touched_paths,
|
||||
opening,
|
||||
);
|
||||
if let Some(diagnostic) = reconciliation.diagnostic {
|
||||
self.context_notice = Some(diagnostic);
|
||||
}
|
||||
reconciliation
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|message| SystemMessage::instruction(message.content, message.metadata))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn dev_brain_skills_prompt(&self) -> Option<String> {
|
||||
self.config.dev_brain.enabled.then(|| {
|
||||
let projects = self
|
||||
@@ -434,13 +453,6 @@ impl App {
|
||||
})
|
||||
}
|
||||
|
||||
fn session_agents_prompt(&self) -> Option<&str> {
|
||||
self.conversation
|
||||
.iter()
|
||||
.find(|message| message.system && message.content.starts_with(AGENTS_PREFIX))
|
||||
.map(|message| message.content.as_str())
|
||||
}
|
||||
|
||||
pub(super) fn can_compact_session(&self, session_id: i32) -> bool {
|
||||
!self.generating
|
||||
&& self.selected_session == Some(session_id)
|
||||
@@ -486,31 +498,6 @@ impl App {
|
||||
self.reset_agent_tool_repeats();
|
||||
#[cfg(target_os = "macos")]
|
||||
let opening_turn = self.selected_session.is_none();
|
||||
#[cfg(target_os = "macos")]
|
||||
let opening_agents = if opening_turn {
|
||||
let Some(project) = self
|
||||
.projects
|
||||
.iter()
|
||||
.find(|project| Some(project.project.id) == self.selected_project)
|
||||
else {
|
||||
self.error = Some("The selected project is unavailable.".into());
|
||||
return;
|
||||
};
|
||||
match project_agents(Path::new(&project.project.path)) {
|
||||
Ok(agents) => agents.map(|agents| format!("{AGENTS_PREFIX}{agents}")),
|
||||
Err(error) => {
|
||||
self.error = Some(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let agents =
|
||||
agents_prompt_for_turn(opening_turn, opening_agents, self.session_agents_prompt());
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let agents = None::<String>;
|
||||
self.a2ui_auto_switch_pending = true;
|
||||
self.context_notice = None;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -542,7 +529,7 @@ impl App {
|
||||
}
|
||||
};
|
||||
effective.turn.system_prompt =
|
||||
self.chat_system_prompt(model, &effective.turn.system_prompt, agents.as_deref());
|
||||
self.chat_system_prompt(model, &effective.turn.system_prompt);
|
||||
effective.turn.system_prompt = crate::compaction::summary_system_prompt(
|
||||
&effective.turn.system_prompt,
|
||||
self.compaction_summary(),
|
||||
@@ -559,18 +546,21 @@ impl App {
|
||||
prompt.clone()
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut injected_system = Vec::new();
|
||||
let mut injected_system = self.workspace_instruction_messages(&[], opening_turn);
|
||||
#[cfg(target_os = "macos")]
|
||||
if opening_turn {
|
||||
injected_system.extend(agents.clone());
|
||||
injected_system.extend(self.dev_brain_skills_prompt());
|
||||
injected_system.push(crate::agent::datetime_context());
|
||||
injected_system.extend(self.dev_brain_skills_prompt().map(SystemMessage::plain));
|
||||
injected_system.push(SystemMessage::plain(crate::agent::datetime_context()));
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
let reminder_injected = self.system_prompt_reminder_due();
|
||||
#[cfg(target_os = "macos")]
|
||||
if reminder_injected {
|
||||
injected_system.extend(self.system_prompt_reminders(model));
|
||||
injected_system.extend(
|
||||
self.system_prompt_reminders(model)
|
||||
.into_iter()
|
||||
.map(SystemMessage::plain),
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut messages = self
|
||||
@@ -582,15 +572,15 @@ impl App {
|
||||
messages.extend(
|
||||
injected_system
|
||||
.iter()
|
||||
.skip(usize::from(opening_turn && agents.is_some()))
|
||||
.map(|content| ChatTurn {
|
||||
.filter(|message| !message.content.is_empty())
|
||||
.map(|message| ChatTurn {
|
||||
user: false,
|
||||
tool: false,
|
||||
system: true,
|
||||
skip_previous_eos: false,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
content: content.clone(),
|
||||
content: message.content.clone(),
|
||||
}),
|
||||
);
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -736,9 +726,6 @@ impl App {
|
||||
if !self.config.system_prompt.trim().is_empty() {
|
||||
reminders.push(self.config.system_prompt.clone());
|
||||
}
|
||||
if let Some(agents) = self.session_agents_prompt() {
|
||||
reminders.push(agents.to_owned());
|
||||
}
|
||||
reminders
|
||||
}
|
||||
|
||||
@@ -843,7 +830,7 @@ impl App {
|
||||
"queued manual compaction",
|
||||
)
|
||||
} else {
|
||||
self.start_tool_result_check(result, ToolCheckStage::Initial)
|
||||
self.start_tool_result_check(result, ToolCheckStage::ResultInitial)
|
||||
};
|
||||
if let Err(error) = continuation {
|
||||
self.generating = false;
|
||||
@@ -1170,7 +1157,13 @@ impl App {
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(feedback) = continuation_feedback
|
||||
&& let Err(error) = self.continue_after_tool_result(&feedback)
|
||||
&& let Err(error) = self.start_tool_result_check(
|
||||
crate::agent::ToolRunResult {
|
||||
content: feedback,
|
||||
touched_paths: Vec::new(),
|
||||
},
|
||||
ToolCheckStage::ResultInitial,
|
||||
)
|
||||
{
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
@@ -1288,9 +1281,13 @@ impl App {
|
||||
&child_turn.system_prompt,
|
||||
self.config.dev_brain.enabled,
|
||||
);
|
||||
if let Some(agents) = self.session_agents_prompt() {
|
||||
for instruction in self
|
||||
.model_chat_messages()
|
||||
.into_iter()
|
||||
.filter(|message| message.instruction_metadata.is_some())
|
||||
{
|
||||
child_turn.system_prompt.push_str("\n\n");
|
||||
child_turn.system_prompt.push_str(agents);
|
||||
child_turn.system_prompt.push_str(&instruction.content);
|
||||
}
|
||||
if let Some(skills) = self.dev_brain_skills_prompt() {
|
||||
child_turn.system_prompt.push_str("\n\n");
|
||||
@@ -1333,7 +1330,7 @@ impl App {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn continue_after_tool_result(&mut self, result: &str) -> Result<(), String> {
|
||||
fn continue_after_tool_result(&mut self) -> Result<(), String> {
|
||||
let session_id = self
|
||||
.selected_session
|
||||
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
|
||||
@@ -1343,11 +1340,8 @@ impl App {
|
||||
let runtime = self.config.runtime_for(model);
|
||||
let mut effective =
|
||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
|
||||
effective.turn.system_prompt = self.chat_system_prompt(
|
||||
model,
|
||||
&effective.turn.system_prompt,
|
||||
self.session_agents_prompt(),
|
||||
);
|
||||
effective.turn.system_prompt =
|
||||
self.chat_system_prompt(model, &effective.turn.system_prompt);
|
||||
effective.turn.system_prompt = crate::compaction::summary_system_prompt(
|
||||
&effective.turn.system_prompt,
|
||||
self.compaction_summary(),
|
||||
@@ -1363,15 +1357,19 @@ impl App {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let system_messages = reminders
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(SystemMessage::plain)
|
||||
.collect::<Vec<_>>();
|
||||
let mut saved = self
|
||||
.database
|
||||
.as_mut()
|
||||
.ok_or_else(|| "The project database is unavailable.".to_owned())?
|
||||
.continue_tool_turn(
|
||||
.continue_durable_tool_turn(
|
||||
session_id,
|
||||
result,
|
||||
queued.as_deref(),
|
||||
&reminders,
|
||||
&system_messages,
|
||||
assistant_reasoning,
|
||||
)
|
||||
.map_err(|error| format!("Could not save the tool turn: {error}"))?;
|
||||
@@ -1414,6 +1412,41 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn persist_tool_result(&mut self, result: &str) -> Result<(), String> {
|
||||
let session_id = self
|
||||
.selected_session
|
||||
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
|
||||
let stored = self
|
||||
.database
|
||||
.as_mut()
|
||||
.ok_or_else(|| "The project database is unavailable.".to_owned())?
|
||||
.record_tool_result(session_id, result)
|
||||
.map_err(|error| format!("Could not save the tool result: {error}"))?;
|
||||
self.conversation.push(ChatMessage::from(stored));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn persist_workspace_instructions(&mut self, touched_paths: &[PathBuf]) -> Result<(), String> {
|
||||
let session_id = self
|
||||
.selected_session
|
||||
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
|
||||
let messages = self.workspace_instruction_messages(touched_paths, false);
|
||||
if messages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let stored = self
|
||||
.database
|
||||
.as_mut()
|
||||
.ok_or_else(|| "The project database is unavailable.".to_owned())?
|
||||
.record_system_messages(session_id, &messages)
|
||||
.map_err(|error| format!("Could not save workspace instructions: {error}"))?;
|
||||
self.conversation
|
||||
.extend(stored.into_iter().map(ChatMessage::from));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn stop_agent_jobs(&mut self) {
|
||||
let Some((_, tools)) = &self.agent_tools else {
|
||||
@@ -1431,10 +1464,10 @@ impl App {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn start_tool_result_check(
|
||||
&mut self,
|
||||
result: String,
|
||||
result: crate::agent::ToolRunResult,
|
||||
stage: ToolCheckStage,
|
||||
) -> Result<(), String> {
|
||||
if matches!(stage, ToolCheckStage::Initial)
|
||||
if matches!(stage, ToolCheckStage::ResultInitial)
|
||||
&& crate::compaction::should_compact(self.context_used, self.context_limit)
|
||||
{
|
||||
return self.start_compaction(
|
||||
@@ -1442,34 +1475,45 @@ impl App {
|
||||
"soft limit before tool continuation",
|
||||
);
|
||||
}
|
||||
if matches!(stage, ToolCheckStage::Instructions)
|
||||
&& crate::compaction::should_compact(self.context_used, self.context_limit)
|
||||
{
|
||||
return self.start_compaction(
|
||||
PendingContinuation::DurableTool(result.touched_paths),
|
||||
"soft limit before instruction continuation",
|
||||
);
|
||||
}
|
||||
let model = self.config.model;
|
||||
let generation = self.config.active_generation();
|
||||
let runtime = self.config.runtime_for(model);
|
||||
let mut effective =
|
||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
|
||||
effective.turn.system_prompt = self.chat_system_prompt(
|
||||
model,
|
||||
&effective.turn.system_prompt,
|
||||
self.session_agents_prompt(),
|
||||
);
|
||||
effective.turn.system_prompt =
|
||||
self.chat_system_prompt(model, &effective.turn.system_prompt);
|
||||
effective.turn.system_prompt = crate::compaction::summary_system_prompt(
|
||||
&effective.turn.system_prompt,
|
||||
self.compaction_summary(),
|
||||
);
|
||||
let instruction_stage = matches!(
|
||||
stage,
|
||||
ToolCheckStage::Instructions | ToolCheckStage::InstructionsAfterCompaction
|
||||
);
|
||||
let mut messages = self
|
||||
.model_chat_messages()
|
||||
.into_iter()
|
||||
.map(chat_turn)
|
||||
.collect::<Vec<_>>();
|
||||
messages.push(ChatTurn {
|
||||
user: false,
|
||||
tool: true,
|
||||
system: false,
|
||||
skip_previous_eos: false,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
content: result.clone(),
|
||||
});
|
||||
if !instruction_stage {
|
||||
messages.push(ChatTurn {
|
||||
user: false,
|
||||
tool: true,
|
||||
system: false,
|
||||
skip_previous_eos: false,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
content: result.content.clone(),
|
||||
});
|
||||
}
|
||||
let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
|
||||
let active = self
|
||||
.generation_service
|
||||
@@ -1502,7 +1546,9 @@ impl App {
|
||||
}
|
||||
Ok(GenerationEvent::Measured(Ok(projected))) => {
|
||||
let check = self.active_tool_check.take().unwrap();
|
||||
if self.manual_compaction_queued && matches!(check.stage, ToolCheckStage::Initial) {
|
||||
if self.manual_compaction_queued
|
||||
&& matches!(check.stage, ToolCheckStage::ResultInitial)
|
||||
{
|
||||
self.manual_compaction_queued = false;
|
||||
if let Err(error) = self.start_compaction(
|
||||
PendingContinuation::Tool(check.result),
|
||||
@@ -1514,20 +1560,56 @@ impl App {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
let reserve = if matches!(check.stage, ToolCheckStage::BoundedError) {
|
||||
if self.manual_compaction_queued
|
||||
&& matches!(check.stage, ToolCheckStage::Instructions)
|
||||
{
|
||||
self.manual_compaction_queued = false;
|
||||
if let Err(error) = self.start_compaction(
|
||||
PendingContinuation::DurableTool(check.result.touched_paths),
|
||||
"queued manual compaction",
|
||||
) {
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
self.error = Some(error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
let reserve = if matches!(check.stage, ToolCheckStage::BoundedResult) {
|
||||
16
|
||||
} else {
|
||||
crate::compaction::tool_result_reserve(self.context_limit)
|
||||
};
|
||||
if crate::compaction::tool_result_fits(projected, self.context_limit, reserve) {
|
||||
if let Err(error) = self.continue_after_tool_result(&check.result) {
|
||||
let continuation = if matches!(
|
||||
check.stage,
|
||||
ToolCheckStage::ResultInitial
|
||||
| ToolCheckStage::ResultAfterCompaction
|
||||
| ToolCheckStage::BoundedResult
|
||||
) {
|
||||
self.persist_tool_result(&check.result.content)
|
||||
.and_then(|()| {
|
||||
self.persist_workspace_instructions(&check.result.touched_paths)
|
||||
})
|
||||
.and_then(|()| {
|
||||
self.start_tool_result_check(
|
||||
crate::agent::ToolRunResult {
|
||||
content: String::new(),
|
||||
touched_paths: check.result.touched_paths,
|
||||
},
|
||||
ToolCheckStage::Instructions,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
self.continue_after_tool_result()
|
||||
};
|
||||
if let Err(error) = continuation {
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
self.error = Some(error);
|
||||
}
|
||||
} else {
|
||||
match check.stage {
|
||||
ToolCheckStage::Initial => {
|
||||
ToolCheckStage::ResultInitial => {
|
||||
if let Err(error) = self.start_compaction(
|
||||
PendingContinuation::Tool(check.result),
|
||||
"tool result would exceed context",
|
||||
@@ -1537,25 +1619,40 @@ impl App {
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
ToolCheckStage::AfterCompaction => {
|
||||
let error = crate::compaction::bounded_tool_error(
|
||||
ToolCheckStage::ResultAfterCompaction => {
|
||||
let content = crate::compaction::bounded_tool_error(
|
||||
projected,
|
||||
self.context_limit,
|
||||
reserve,
|
||||
);
|
||||
let result = crate::agent::ToolRunResult {
|
||||
content,
|
||||
touched_paths: check.result.touched_paths,
|
||||
};
|
||||
if let Err(error) =
|
||||
self.start_tool_result_check(error, ToolCheckStage::BoundedError)
|
||||
self.start_tool_result_check(result, ToolCheckStage::BoundedResult)
|
||||
{
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
ToolCheckStage::BoundedError => {
|
||||
ToolCheckStage::BoundedResult
|
||||
| ToolCheckStage::InstructionsAfterCompaction => {
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
self.error = Some("context full after compaction".into());
|
||||
}
|
||||
ToolCheckStage::Instructions => {
|
||||
if let Err(error) = self.start_compaction(
|
||||
PendingContinuation::DurableTool(check.result.touched_paths),
|
||||
"workspace instructions would exceed context",
|
||||
) {
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
@@ -1598,11 +1695,8 @@ impl App {
|
||||
let runtime = self.config.runtime_for(model);
|
||||
let mut effective =
|
||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
|
||||
effective.turn.system_prompt = self.chat_system_prompt(
|
||||
model,
|
||||
&effective.turn.system_prompt,
|
||||
self.session_agents_prompt(),
|
||||
);
|
||||
effective.turn.system_prompt =
|
||||
self.chat_system_prompt(model, &effective.turn.system_prompt);
|
||||
let rebuild_system_prompt = effective.turn.system_prompt.clone();
|
||||
effective.turn.system_prompt = crate::compaction::summary_system_prompt(
|
||||
&effective.turn.system_prompt,
|
||||
@@ -1685,13 +1779,31 @@ impl App {
|
||||
PendingContinuation::Tool(result) => {
|
||||
if let Err(error) = self.start_tool_result_check(
|
||||
result,
|
||||
ToolCheckStage::AfterCompaction,
|
||||
ToolCheckStage::ResultAfterCompaction,
|
||||
) {
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
PendingContinuation::DurableTool(touched_paths) => {
|
||||
let continuation = self
|
||||
.persist_workspace_instructions(&touched_paths)
|
||||
.and_then(|()| {
|
||||
self.start_tool_result_check(
|
||||
crate::agent::ToolRunResult {
|
||||
content: String::new(),
|
||||
touched_paths,
|
||||
},
|
||||
ToolCheckStage::InstructionsAfterCompaction,
|
||||
)
|
||||
});
|
||||
if let Err(error) = continuation {
|
||||
self.generating = false;
|
||||
self.activity = Some("Failed".into());
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1816,7 +1928,8 @@ impl App {
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
!message.compaction
|
||||
&& !(message.system && message.content.starts_with(AGENTS_PREFIX))
|
||||
&& !(message.system && message.content.starts_with(LEGACY_AGENTS_PREFIX))
|
||||
&& !(message.instruction_metadata.is_some() && message.content.is_empty())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -2007,10 +2120,10 @@ pub(super) fn session_title(reply: &str) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ChatMessage, TOOL_PROTOCOL_CORRECTION, TurnSummary, agents_prompt_for_turn, chat_turn,
|
||||
compacted_context_start, correction_already_sent, has_chat_after_last_compaction,
|
||||
has_misplaced_tool_call, is_empty_response, project_agents, promote_legacy_turn_summaries,
|
||||
queued_prompt, sync_a2ui_message, title_context,
|
||||
ChatMessage, TOOL_PROTOCOL_CORRECTION, TurnSummary, chat_turn, compacted_context_start,
|
||||
correction_already_sent, has_chat_after_last_compaction, has_misplaced_tool_call,
|
||||
is_empty_response, promote_legacy_turn_summaries, queued_prompt, sync_a2ui_message,
|
||||
title_context,
|
||||
};
|
||||
use crate::engine::ChatTurn;
|
||||
use crate::model::ModelChoice;
|
||||
@@ -2030,6 +2143,7 @@ mod tests {
|
||||
content: content.to_owned(),
|
||||
model_content: None,
|
||||
tool_approval_reasons: Vec::new(),
|
||||
instruction_metadata: None,
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
transcript: iced::widget::text_editor::Content::new(),
|
||||
a2ui_lines_processed: 0,
|
||||
@@ -2051,35 +2165,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_agents_is_optional_and_preserves_the_file() {
|
||||
let directory =
|
||||
std::env::temp_dir().join(format!("ds4-server-agents-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
assert_eq!(project_agents(&directory).unwrap(), None);
|
||||
|
||||
let instructions = "# Instructions\n\nKeep this exact.\n";
|
||||
std::fs::write(directory.join("AGENTS.md"), instructions).unwrap();
|
||||
assert_eq!(
|
||||
project_agents(&directory).unwrap().as_deref(),
|
||||
Some(instructions)
|
||||
);
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_continuations_reuse_the_stored_agents_prompt() {
|
||||
let stored = "Project AGENTS.md instructions:\nkeep this exact";
|
||||
assert_eq!(
|
||||
agents_prompt_for_turn(false, None, Some(stored)).as_deref(),
|
||||
Some(stored)
|
||||
);
|
||||
assert_eq!(
|
||||
agents_prompt_for_turn(true, Some("opening".into()), Some(stored)).as_deref(),
|
||||
Some("opening")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_summary_accumulates_model_continuations() {
|
||||
let mut summary = TurnSummary::new();
|
||||
@@ -2226,6 +2311,7 @@ mod tests {
|
||||
.to_owned(),
|
||||
model_content: None,
|
||||
tool_approval_reasons: Vec::new(),
|
||||
instruction_metadata: None,
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
transcript: iced::widget::text_editor::Content::new(),
|
||||
a2ui_lines_processed: 0,
|
||||
@@ -2294,6 +2380,7 @@ mod tests {
|
||||
content: format!("message {id}"),
|
||||
model_content: None,
|
||||
tool_approval_reasons: Vec::new(),
|
||||
instruction_metadata: None,
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
transcript: iced::widget::text_editor::Content::new(),
|
||||
a2ui_lines_processed: 0,
|
||||
@@ -2334,6 +2421,7 @@ mod tests {
|
||||
content: format!("message {id}"),
|
||||
model_content: None,
|
||||
tool_approval_reasons: Vec::new(),
|
||||
instruction_metadata: None,
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
transcript: iced::widget::text_editor::Content::new(),
|
||||
a2ui_lines_processed: 0,
|
||||
|
||||
@@ -826,6 +826,7 @@ mod tests {
|
||||
content: content.to_owned(),
|
||||
model_content: None,
|
||||
tool_approval_reasons: Vec::new(),
|
||||
instruction_metadata: None,
|
||||
markdown: markdown::Content::new(),
|
||||
transcript: text_editor::Content::new(),
|
||||
a2ui_lines_processed: 0,
|
||||
|
||||
Reference in New Issue
Block a user