feat: record agent approvals
This commit is contained in:
3
assets/icons/robot.svg
Normal file
3
assets/icons/robot.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10 2.5v2M7.5 2.5h5M4 7.5A2.5 2.5 0 0 1 6.5 5h7A2.5 2.5 0 0 1 16 7.5v6a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 4 13.5zM4 10H2.5M17.5 10H16M7.5 9.5h.01M12.5 9.5h.01M7.5 13h5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 346 B |
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE messages DROP COLUMN tool_approval_reasons;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE messages ADD COLUMN tool_approval_reasons TEXT;
|
||||||
87
src/agent.rs
87
src/agent.rs
@@ -129,6 +129,7 @@ pub(crate) struct ToolCard {
|
|||||||
pub(crate) call: ToolCall,
|
pub(crate) call: ToolCall,
|
||||||
pub(crate) state: ToolLifecycle,
|
pub(crate) state: ToolLifecycle,
|
||||||
pub(crate) result: Option<String>,
|
pub(crate) result: Option<String>,
|
||||||
|
pub(crate) approval_reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolCard {
|
impl ToolCard {
|
||||||
@@ -137,6 +138,7 @@ impl ToolCard {
|
|||||||
call,
|
call,
|
||||||
state: ToolLifecycle::Parsing,
|
state: ToolLifecycle::Parsing,
|
||||||
result: None,
|
result: None,
|
||||||
|
approval_reason: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,24 +181,38 @@ impl ShellApprovalMode {
|
|||||||
call: &ToolCall,
|
call: &ToolCall,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
cancel: &AtomicBool,
|
cancel: &AtomicBool,
|
||||||
) -> Option<ApprovalPrompt> {
|
) -> (Option<ApprovalPrompt>, Option<String>) {
|
||||||
let command = (call.name == "bash").then(|| string(call, "command"))??;
|
let Some(command) = (call.name == "bash")
|
||||||
let reason = match self {
|
.then(|| string(call, "command"))
|
||||||
Self::Heuristic => risky_shell_reason(command, root).map(str::to_owned),
|
.flatten()
|
||||||
|
else {
|
||||||
|
return (None, None);
|
||||||
|
};
|
||||||
|
let (reason, ai_reason) = match self {
|
||||||
|
Self::Heuristic => (risky_shell_reason(command, root).map(str::to_owned), None),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
Self::Ai(classifier) => match classifier.assess(command, root, cancel) {
|
Self::Ai(classifier) => match classifier.assess(command, root, cancel) {
|
||||||
Ok(assessment) if !assessment.risky => None,
|
Ok(assessment) => {
|
||||||
Ok(assessment) => Some(assessment.reason),
|
let reason = (!assessment.reason.is_empty()).then_some(assessment.reason);
|
||||||
Err(error) => Some(format!(
|
let approval = assessment.risky.then(|| reason.clone()).flatten();
|
||||||
"The AI risk check could not complete ({error}); approval is required."
|
(approval, reason)
|
||||||
)),
|
}
|
||||||
|
Err(error) => (
|
||||||
|
Some(format!(
|
||||||
|
"The AI risk check could not complete ({error}); approval is required."
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}?;
|
};
|
||||||
Some(ApprovalPrompt {
|
(
|
||||||
title: "Allow shell command?".into(),
|
reason.map(|reason| ApprovalPrompt {
|
||||||
detail: format!("{reason}\n\n{command}"),
|
title: "Allow shell command?".into(),
|
||||||
working_directory: root.to_owned(),
|
detail: format!("{reason}\n\n{command}"),
|
||||||
})
|
working_directory: root.to_owned(),
|
||||||
|
}),
|
||||||
|
ai_reason,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +331,10 @@ pub(crate) enum ToolEvent {
|
|||||||
prompt: ApprovalPrompt,
|
prompt: ApprovalPrompt,
|
||||||
decision: Sender<bool>,
|
decision: Sender<bool>,
|
||||||
},
|
},
|
||||||
|
ApprovalReason {
|
||||||
|
index: usize,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BashJob {
|
struct BashJob {
|
||||||
@@ -1225,9 +1245,16 @@ pub(crate) fn execute_async(
|
|||||||
if approval_mode.uses_ai(call) {
|
if approval_mode.uses_ai(call) {
|
||||||
send_state(&event_sender, index, ToolLifecycle::AssessingRisk, None);
|
send_state(&event_sender, index, ToolLifecycle::AssessingRisk, None);
|
||||||
}
|
}
|
||||||
let prompt = tools
|
let browser_prompt = tools.browser_approval(call);
|
||||||
.browser_approval(call)
|
let (shell_prompt, ai_reason) = if browser_prompt.is_none() {
|
||||||
.or_else(|| approval_mode.approval(call, &tools.root, &worker_cancel));
|
approval_mode.approval(call, &tools.root, &worker_cancel)
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
if let Some(reason) = ai_reason {
|
||||||
|
let _ = event_sender.send(ToolEvent::ApprovalReason { index, reason });
|
||||||
|
}
|
||||||
|
let prompt = browser_prompt.or(shell_prompt);
|
||||||
if let Some(prompt) = prompt {
|
if let Some(prompt) = prompt {
|
||||||
match request_approval(&event_sender, index, prompt, &worker_cancel) {
|
match request_approval(&event_sender, index, prompt, &worker_cancel) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
@@ -1458,6 +1485,7 @@ pub(crate) fn stored_tool_cards(
|
|||||||
model: ModelChoice,
|
model: ModelChoice,
|
||||||
assistant: &str,
|
assistant: &str,
|
||||||
result: Option<&str>,
|
result: Option<&str>,
|
||||||
|
approval_reasons: &[Option<String>],
|
||||||
) -> Vec<ToolCard> {
|
) -> Vec<ToolCard> {
|
||||||
let calls = parse_tool_calls(model, assistant)
|
let calls = parse_tool_calls(model, assistant)
|
||||||
.map(|(_, calls)| calls)
|
.map(|(_, calls)| calls)
|
||||||
@@ -1479,6 +1507,7 @@ pub(crate) fn stored_tool_cards(
|
|||||||
call,
|
call,
|
||||||
state,
|
state,
|
||||||
result,
|
result,
|
||||||
|
approval_reason: approval_reasons.get(index).cloned().flatten(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -2054,6 +2083,26 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_tool_cards_restore_ai_approval_reasons_by_call() {
|
||||||
|
let assistant = r#"<|DSML|tool_calls>
|
||||||
|
<|DSML|invoke name="bash"><|DSML|parameter name="command" string="true">pwd</|DSML|parameter></|DSML|invoke>
|
||||||
|
<|DSML|invoke name="read"><|DSML|parameter name="path" string="true">README.md</|DSML|parameter></|DSML|invoke>
|
||||||
|
</|DSML|tool_calls>"#;
|
||||||
|
let cards = stored_tool_cards(
|
||||||
|
ModelChoice::DeepSeekV4Flash,
|
||||||
|
assistant,
|
||||||
|
None,
|
||||||
|
&[Some("Only reads the working directory.".into()), None],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cards[0].approval_reason.as_deref(),
|
||||||
|
Some("Only reads the working directory.")
|
||||||
|
);
|
||||||
|
assert_eq!(cards[1].approval_reason, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn denial_and_stop_cancel_commands_awaiting_approval() {
|
fn denial_and_stop_cancel_commands_awaiting_approval() {
|
||||||
let directory = std::env::temp_dir().join(format!(
|
let directory = std::env::temp_dir().join(format!(
|
||||||
@@ -2082,6 +2131,7 @@ mod tests {
|
|||||||
break decision;
|
break decision;
|
||||||
}
|
}
|
||||||
ToolEvent::State { .. } => {}
|
ToolEvent::State { .. } => {}
|
||||||
|
ToolEvent::ApprovalReason { .. } => {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
decision.send(false).unwrap();
|
decision.send(false).unwrap();
|
||||||
@@ -2108,6 +2158,7 @@ mod tests {
|
|||||||
break decision;
|
break decision;
|
||||||
}
|
}
|
||||||
ToolEvent::State { .. } => {}
|
ToolEvent::State { .. } => {}
|
||||||
|
ToolEvent::ApprovalReason { .. } => {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
active.cancel.store(true, Ordering::Relaxed);
|
active.cancel.store(true, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -2729,6 +2729,7 @@ mod tests {
|
|||||||
reasoning_open: true,
|
reasoning_open: true,
|
||||||
content: String::new(),
|
content: String::new(),
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: Vec::new(),
|
||||||
markdown: markdown::Content::new(),
|
markdown: markdown::Content::new(),
|
||||||
transcript: text_editor::Content::new(),
|
transcript: text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
@@ -2760,6 +2761,7 @@ mod tests {
|
|||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: content.into(),
|
content: content.into(),
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: Vec::new(),
|
||||||
markdown: markdown::Content::new(),
|
markdown: markdown::Content::new(),
|
||||||
transcript: text_editor::Content::new(),
|
transcript: text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ pub(crate) struct ChatMessage {
|
|||||||
pub(super) reasoning_open: bool,
|
pub(super) reasoning_open: bool,
|
||||||
pub(super) content: String,
|
pub(super) content: String,
|
||||||
pub(super) model_content: Option<String>,
|
pub(super) model_content: Option<String>,
|
||||||
|
pub(super) tool_approval_reasons: Vec<Option<String>>,
|
||||||
pub(super) markdown: markdown::Content,
|
pub(super) markdown: markdown::Content,
|
||||||
pub(super) transcript: text_editor::Content,
|
pub(super) transcript: text_editor::Content,
|
||||||
pub(super) a2ui_lines_processed: usize,
|
pub(super) a2ui_lines_processed: usize,
|
||||||
@@ -229,6 +230,11 @@ impl From<StoredMessage> for ChatMessage {
|
|||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
model_content: message.model_content,
|
model_content: message.model_content,
|
||||||
|
tool_approval_reasons: message
|
||||||
|
.tool_approval_reasons
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|reasons| serde_json::from_str(reasons).ok())
|
||||||
|
.unwrap_or_default(),
|
||||||
markdown: iced::widget::markdown::Content::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
transcript: iced::widget::text_editor::Content::new(),
|
transcript: iced::widget::text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
@@ -789,6 +795,26 @@ impl App {
|
|||||||
self.pending_tool_approval = Some((prompt, decision));
|
self.pending_tool_approval = Some((prompt, decision));
|
||||||
self.activity = Some(format!("Tool {} · Awaiting approval", index + 1));
|
self.activity = Some(format!("Tool {} · Awaiting approval", index + 1));
|
||||||
}
|
}
|
||||||
|
crate::agent::ToolEvent::ApprovalReason { index, reason } => {
|
||||||
|
if let Some(card) = self.tool_cards.get_mut(index) {
|
||||||
|
card.approval_reason = Some(reason);
|
||||||
|
}
|
||||||
|
let reasons = self
|
||||||
|
.tool_cards
|
||||||
|
.iter()
|
||||||
|
.map(|card| card.approval_reason.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if let Some(message) = self.conversation.last_mut() {
|
||||||
|
message.tool_approval_reasons = reasons.clone();
|
||||||
|
if let Some(database) = &mut self.database
|
||||||
|
&& let Err(error) = database
|
||||||
|
.update_message_tool_approval_reasons(message.id, &reasons)
|
||||||
|
{
|
||||||
|
self.error =
|
||||||
|
Some(format!("Could not save tool approval reasons: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1961,6 +1987,7 @@ mod tests {
|
|||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: content.to_owned(),
|
content: content.to_owned(),
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: Vec::new(),
|
||||||
markdown: iced::widget::markdown::Content::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
transcript: iced::widget::text_editor::Content::new(),
|
transcript: iced::widget::text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
@@ -2156,6 +2183,7 @@ mod tests {
|
|||||||
content: "### Core / Setup\n\n| File | Lines |\n|---|---:|\n| `src/app.rs` | **1,750** |\n| `src/engine.rs` | 2,400 |\n\n### Summary\n\nDone."
|
content: "### Core / Setup\n\n| File | Lines |\n|---|---:|\n| `src/app.rs` | **1,750** |\n| `src/engine.rs` | 2,400 |\n\n### Summary\n\nDone."
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: Vec::new(),
|
||||||
markdown: iced::widget::markdown::Content::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
transcript: iced::widget::text_editor::Content::new(),
|
transcript: iced::widget::text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
@@ -2223,6 +2251,7 @@ mod tests {
|
|||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: format!("message {id}"),
|
content: format!("message {id}"),
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: Vec::new(),
|
||||||
markdown: iced::widget::markdown::Content::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
transcript: iced::widget::text_editor::Content::new(),
|
transcript: iced::widget::text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
@@ -2262,6 +2291,7 @@ mod tests {
|
|||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: format!("message {id}"),
|
content: format!("message {id}"),
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: Vec::new(),
|
||||||
markdown: iced::widget::markdown::Content::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
transcript: iced::widget::text_editor::Content::new(),
|
transcript: iced::widget::text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const ICON_MORE: &[u8] = include_bytes!("../../assets/icons/more.svg");
|
|||||||
const ICON_PAPERCLIP: &[u8] = include_bytes!("../../assets/icons/paperclip.svg");
|
const ICON_PAPERCLIP: &[u8] = include_bytes!("../../assets/icons/paperclip.svg");
|
||||||
const ICON_SEND: &[u8] = include_bytes!("../../assets/icons/send.svg");
|
const ICON_SEND: &[u8] = include_bytes!("../../assets/icons/send.svg");
|
||||||
const ICON_MODEL: &[u8] = include_bytes!("../../assets/icons/model.svg");
|
const ICON_MODEL: &[u8] = include_bytes!("../../assets/icons/model.svg");
|
||||||
|
const ICON_ROBOT: &[u8] = include_bytes!("../../assets/icons/robot.svg");
|
||||||
const ICON_SPARK: &[u8] = include_bytes!("../../assets/icons/spark.svg");
|
const ICON_SPARK: &[u8] = include_bytes!("../../assets/icons/spark.svg");
|
||||||
const ICON_PIN: &[u8] = include_bytes!("../../assets/icons/pin.svg");
|
const ICON_PIN: &[u8] = include_bytes!("../../assets/icons/pin.svg");
|
||||||
const ICON_SIDEBAR: &[u8] = include_bytes!("../../assets/icons/sidebar.svg");
|
const ICON_SIDEBAR: &[u8] = include_bytes!("../../assets/icons/sidebar.svg");
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ impl App {
|
|||||||
self.config.model,
|
self.config.model,
|
||||||
&self.conversation[index - 1].content,
|
&self.conversation[index - 1].content,
|
||||||
None,
|
None,
|
||||||
|
&self.conversation[index - 1].tool_approval_reasons,
|
||||||
)
|
)
|
||||||
.is_empty()
|
.is_empty()
|
||||||
{
|
{
|
||||||
@@ -191,6 +192,7 @@ impl App {
|
|||||||
self.config.model,
|
self.config.model,
|
||||||
&message.content,
|
&message.content,
|
||||||
stored_result,
|
stored_result,
|
||||||
|
&message.tool_approval_reasons,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if !cards.is_empty() {
|
if !cards.is_empty() {
|
||||||
@@ -540,9 +542,25 @@ fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
|
|||||||
.gap(6),
|
.gap(6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let mut name = row![text(card.call.name).size(13)]
|
||||||
|
.spacing(5)
|
||||||
|
.align_y(Alignment::Center);
|
||||||
|
if let Some(reason) = &card.approval_reason {
|
||||||
|
name = name.push(
|
||||||
|
tooltip(
|
||||||
|
icon(ICON_ROBOT, 13),
|
||||||
|
container(text(reason.clone()).size(12))
|
||||||
|
.padding(8)
|
||||||
|
.max_width(320)
|
||||||
|
.style(preference_group_style),
|
||||||
|
tooltip::Position::Top,
|
||||||
|
)
|
||||||
|
.gap(5),
|
||||||
|
);
|
||||||
|
}
|
||||||
let mut content = column![
|
let mut content = column![
|
||||||
row![
|
row![
|
||||||
text(card.call.name).size(13),
|
name,
|
||||||
Space::new().width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text(card.state.label()).size(11).color(muted_text()),
|
text(card.state.label()).size(11).color(muted_text()),
|
||||||
actions,
|
actions,
|
||||||
|
|||||||
@@ -825,6 +825,7 @@ mod tests {
|
|||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: content.to_owned(),
|
content: content.to_owned(),
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: Vec::new(),
|
||||||
markdown: markdown::Content::new(),
|
markdown: markdown::Content::new(),
|
||||||
transcript: text_editor::Content::new(),
|
transcript: text_editor::Content::new(),
|
||||||
a2ui_lines_processed: 0,
|
a2ui_lines_processed: 0,
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ pub struct StoredMessage {
|
|||||||
pub reasoning_complete: bool,
|
pub reasoning_complete: bool,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub model_content: Option<String>,
|
pub model_content: Option<String>,
|
||||||
|
pub tool_approval_reasons: Option<String>,
|
||||||
pub system: bool,
|
pub system: bool,
|
||||||
pub compaction: bool,
|
pub compaction: bool,
|
||||||
pub compaction_tail_start: Option<i32>,
|
pub compaction_tail_start: Option<i32>,
|
||||||
@@ -151,6 +152,7 @@ struct NewMessage<'a> {
|
|||||||
reasoning_complete: bool,
|
reasoning_complete: bool,
|
||||||
content: &'a str,
|
content: &'a str,
|
||||||
model_content: Option<&'a str>,
|
model_content: Option<&'a str>,
|
||||||
|
tool_approval_reasons: Option<&'a str>,
|
||||||
system: bool,
|
system: bool,
|
||||||
compaction: bool,
|
compaction: bool,
|
||||||
compaction_tail_start: Option<i32>,
|
compaction_tail_start: Option<i32>,
|
||||||
@@ -197,6 +199,7 @@ impl<'a> NewMessage<'a> {
|
|||||||
reasoning_complete: true,
|
reasoning_complete: true,
|
||||||
content,
|
content,
|
||||||
model_content: None,
|
model_content: None,
|
||||||
|
tool_approval_reasons: None,
|
||||||
system,
|
system,
|
||||||
compaction: false,
|
compaction: false,
|
||||||
compaction_tail_start: None,
|
compaction_tail_start: None,
|
||||||
@@ -599,6 +602,19 @@ impl Database {
|
|||||||
.map_err(|error| error.to_string())
|
.map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_message_tool_approval_reasons(
|
||||||
|
&mut self,
|
||||||
|
id: i32,
|
||||||
|
reasons: &[Option<String>],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let reasons = serde_json::to_string(reasons).map_err(|error| error.to_string())?;
|
||||||
|
diesel::update(messages::table.find(id))
|
||||||
|
.set(messages::tool_approval_reasons.eq(reasons))
|
||||||
|
.execute(&mut self.connection)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_message_generation_stats(
|
pub fn update_message_generation_stats(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: i32,
|
id: i32,
|
||||||
@@ -973,6 +989,12 @@ mod tests {
|
|||||||
database
|
database
|
||||||
.update_message(assistant.id, Some("Reasoning"), true, "Answer")
|
.update_message(assistant.id, Some("Reasoning"), true, "Answer")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
database
|
||||||
|
.update_message_tool_approval_reasons(
|
||||||
|
assistant.id,
|
||||||
|
&[Some("Reads repository metadata only.".into())],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
database
|
database
|
||||||
.update_message_generation_stats(assistant.id, 12_345, 1_024, 768, 256)
|
.update_message_generation_stats(assistant.id, 12_345, 1_024, 768, 256)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1013,6 +1035,10 @@ mod tests {
|
|||||||
assert_eq!(messages[2].reasoning.as_deref(), Some("Reasoning"));
|
assert_eq!(messages[2].reasoning.as_deref(), Some("Reasoning"));
|
||||||
assert!(messages[2].reasoning_complete);
|
assert!(messages[2].reasoning_complete);
|
||||||
assert_eq!(messages[2].content, "Answer");
|
assert_eq!(messages[2].content, "Answer");
|
||||||
|
assert_eq!(
|
||||||
|
messages[2].tool_approval_reasons.as_deref(),
|
||||||
|
Some(r#"["Reads repository metadata only."]"#)
|
||||||
|
);
|
||||||
assert_eq!(messages[2].generation_duration_ms, Some(12_345));
|
assert_eq!(messages[2].generation_duration_ms, Some(12_345));
|
||||||
assert_eq!(messages[2].input_tokens, Some(1_024));
|
assert_eq!(messages[2].input_tokens, Some(1_024));
|
||||||
assert_eq!(messages[2].cached_tokens, Some(768));
|
assert_eq!(messages[2].cached_tokens, Some(768));
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ diesel::table! {
|
|||||||
reasoning_complete -> Bool,
|
reasoning_complete -> Bool,
|
||||||
content -> Text,
|
content -> Text,
|
||||||
model_content -> Nullable<Text>,
|
model_content -> Nullable<Text>,
|
||||||
|
tool_approval_reasons -> Nullable<Text>,
|
||||||
system -> Bool,
|
system -> Bool,
|
||||||
compaction -> Bool,
|
compaction -> Bool,
|
||||||
compaction_tail_start -> Nullable<Integer>,
|
compaction_tail_start -> Nullable<Integer>,
|
||||||
|
|||||||
Reference in New Issue
Block a user