|
18 | 18 |
|
19 | 19 | use crate::agent::{AgentConfig, AgentEvent, AgentLoop, AgentResult}; |
20 | 20 | use crate::commands::{ |
21 | | - CommandContext, CommandRegistry, CronCancelCommand, CronListCommand, LoopCommand, |
| 21 | + CommandAction, CommandContext, CommandRegistry, CronCancelCommand, CronListCommand, LoopCommand, |
22 | 22 | }; |
23 | 23 | use crate::config::CodeConfig; |
24 | 24 | use crate::error::{read_or_recover, write_or_recover, Result}; |
@@ -1333,6 +1333,24 @@ impl Agent { |
1333 | 1333 | } |
1334 | 1334 | } |
1335 | 1335 |
|
| 1336 | +// ============================================================================ |
| 1337 | +// BtwResult |
| 1338 | +// ============================================================================ |
| 1339 | + |
| 1340 | +/// Result of a `/btw` ephemeral side question. |
| 1341 | +/// |
| 1342 | +/// The answer is never added to conversation history. |
| 1343 | +/// Returned by [`AgentSession::btw()`]. |
| 1344 | +#[derive(Debug, Clone)] |
| 1345 | +pub struct BtwResult { |
| 1346 | + /// The original question. |
| 1347 | + pub question: String, |
| 1348 | + /// The LLM's answer. |
| 1349 | + pub answer: String, |
| 1350 | + /// Token usage for this ephemeral call. |
| 1351 | + pub usage: crate::llm::TokenUsage, |
| 1352 | +} |
| 1353 | + |
1336 | 1354 | // ============================================================================ |
1337 | 1355 | // AgentSession |
1338 | 1356 | // ============================================================================ |
@@ -1509,7 +1527,21 @@ impl AgentSession { |
1509 | 1527 | // Slash command interception |
1510 | 1528 | if CommandRegistry::is_command(prompt) { |
1511 | 1529 | let ctx = self.build_command_context(); |
1512 | | - if let Some(output) = self.command_registry().dispatch(prompt, &ctx) { |
| 1530 | + let output = self.command_registry().dispatch(prompt, &ctx); |
| 1531 | + // Drop the MutexGuard before any async operations |
| 1532 | + if let Some(output) = output { |
| 1533 | + // BtwQuery requires an async LLM call — handle it here. |
| 1534 | + if let Some(CommandAction::BtwQuery(ref question)) = output.action { |
| 1535 | + let result = self.btw(question).await?; |
| 1536 | + return Ok(AgentResult { |
| 1537 | + text: result.answer, |
| 1538 | + messages: history |
| 1539 | + .map(|h| h.to_vec()) |
| 1540 | + .unwrap_or_else(|| read_or_recover(&self.history).clone()), |
| 1541 | + tool_calls_count: 0, |
| 1542 | + usage: result.usage, |
| 1543 | + }); |
| 1544 | + } |
1513 | 1545 | return Ok(AgentResult { |
1514 | 1546 | text: output.text, |
1515 | 1547 | messages: history |
@@ -1590,6 +1622,53 @@ impl AgentSession { |
1590 | 1622 | Ok(result) |
1591 | 1623 | } |
1592 | 1624 |
|
| 1625 | + /// Ask an ephemeral side question without affecting conversation history. |
| 1626 | + /// |
| 1627 | + /// Takes a read-only snapshot of the current history, makes a separate LLM |
| 1628 | + /// call with no tools, and returns the answer. History is never modified. |
| 1629 | + /// |
| 1630 | + /// Safe to call concurrently with an ongoing [`send()`](Self::send) — the |
| 1631 | + /// snapshot only acquires a read lock on the internal history. |
| 1632 | + /// |
| 1633 | + /// # Example |
| 1634 | + /// |
| 1635 | + /// ```rust,no_run |
| 1636 | + /// # async fn run(session: &a3s_code_core::AgentSession) -> anyhow::Result<()> { |
| 1637 | + /// let result = session.btw("what file was that error in?").await?; |
| 1638 | + /// println!("{}", result.answer); |
| 1639 | + /// # Ok(()) |
| 1640 | + /// # } |
| 1641 | + /// ``` |
| 1642 | + pub async fn btw(&self, question: &str) -> Result<BtwResult> { |
| 1643 | + let question = question.trim(); |
| 1644 | + if question.is_empty() { |
| 1645 | + return Err(crate::error::CodeError::Session( |
| 1646 | + "btw: question cannot be empty".to_string(), |
| 1647 | + )); |
| 1648 | + } |
| 1649 | + |
| 1650 | + // Snapshot current history — read-only, does not block send(). |
| 1651 | + let history_snapshot = read_or_recover(&self.history).clone(); |
| 1652 | + |
| 1653 | + // Append the side question as a temporary user turn. |
| 1654 | + let mut messages = history_snapshot; |
| 1655 | + messages.push(Message::user(question)); |
| 1656 | + |
| 1657 | + let response = self |
| 1658 | + .llm_client |
| 1659 | + .complete(&messages, Some(crate::prompts::BTW_SYSTEM), &[]) |
| 1660 | + .await |
| 1661 | + .map_err(|e| { |
| 1662 | + crate::error::CodeError::Llm(format!("btw: ephemeral LLM call failed: {e}")) |
| 1663 | + })?; |
| 1664 | + |
| 1665 | + Ok(BtwResult { |
| 1666 | + question: question.to_string(), |
| 1667 | + answer: response.text(), |
| 1668 | + usage: response.usage, |
| 1669 | + }) |
| 1670 | + } |
| 1671 | + |
1593 | 1672 | /// Send a prompt with image attachments and wait for the complete response. |
1594 | 1673 | /// |
1595 | 1674 | /// Images are included as multi-modal content blocks in the user message. |
@@ -1673,8 +1752,51 @@ impl AgentSession { |
1673 | 1752 | // Slash command interception for streaming |
1674 | 1753 | if CommandRegistry::is_command(prompt) { |
1675 | 1754 | let ctx = self.build_command_context(); |
1676 | | - if let Some(output) = self.command_registry().dispatch(prompt, &ctx) { |
| 1755 | + let output = self.command_registry().dispatch(prompt, &ctx); |
| 1756 | + // Drop the MutexGuard before spawning async tasks |
| 1757 | + if let Some(output) = output { |
1677 | 1758 | let (tx, rx) = mpsc::channel(256); |
| 1759 | + |
| 1760 | + // BtwQuery: make the ephemeral call and emit BtwAnswer event. |
| 1761 | + if let Some(CommandAction::BtwQuery(question)) = output.action { |
| 1762 | + // Snapshot history and clone the client before entering the task. |
| 1763 | + let llm_client = self.llm_client.clone(); |
| 1764 | + let history_snapshot = read_or_recover(&self.history).clone(); |
| 1765 | + let handle = tokio::spawn(async move { |
| 1766 | + let mut messages = history_snapshot; |
| 1767 | + messages.push(Message::user(&question)); |
| 1768 | + match llm_client |
| 1769 | + .complete(&messages, Some(crate::prompts::BTW_SYSTEM), &[]) |
| 1770 | + .await |
| 1771 | + { |
| 1772 | + Ok(response) => { |
| 1773 | + let answer = response.text(); |
| 1774 | + let _ = tx |
| 1775 | + .send(AgentEvent::BtwAnswer { |
| 1776 | + question: question.clone(), |
| 1777 | + answer: answer.clone(), |
| 1778 | + usage: response.usage, |
| 1779 | + }) |
| 1780 | + .await; |
| 1781 | + let _ = tx |
| 1782 | + .send(AgentEvent::End { |
| 1783 | + text: answer, |
| 1784 | + usage: crate::llm::TokenUsage::default(), |
| 1785 | + }) |
| 1786 | + .await; |
| 1787 | + } |
| 1788 | + Err(e) => { |
| 1789 | + let _ = tx |
| 1790 | + .send(AgentEvent::Error { |
| 1791 | + message: format!("btw failed: {e}"), |
| 1792 | + }) |
| 1793 | + .await; |
| 1794 | + } |
| 1795 | + } |
| 1796 | + }); |
| 1797 | + return Ok((rx, handle)); |
| 1798 | + } |
| 1799 | + |
1678 | 1800 | let handle = tokio::spawn(async move { |
1679 | 1801 | let _ = tx |
1680 | 1802 | .send(AgentEvent::TextDelta { |
|
0 commit comments