Skip to content

Commit 95fdf74

Browse files
committed
Add more tools, compaction support
1 parent d94800a commit 95fdf74

File tree

19 files changed

+1193
-170
lines changed

19 files changed

+1193
-170
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agent/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ http.workspace = true
4343
http-body-util.workspace = true
4444
hyper.workspace = true
4545
hyper-util.workspace = true
46+
libc.workspace = true
4647
percent-encoding.workspace = true
4748
pin-project-lite = "0.2.16"
4849
r2d2.workspace = true
@@ -63,6 +64,7 @@ sha2.workspace = true
6364
shellexpand.workspace = true
6465
strum.workspace = true
6566
syntect = "5.2.0"
67+
sysinfo.workspace = true
6668
textwrap = "0.16.2"
6769
thiserror.workspace = true
6870
time.workspace = true
@@ -76,6 +78,7 @@ tui-textarea = "0.7.0"
7678
url.workspace = true
7779
uuid.workspace = true
7880
webpki-roots.workspace = true
81+
whoami.workspace = true
7982

8083
[target.'cfg(target_os = "macos")'.dependencies]
8184
objc2.workspace = true

crates/agent/src/agent/agent_config/definitions.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ impl Default for AgentConfigV2025_08_22 {
166166
name: BUILTIN_VIBER_AGENT_NAME.to_string(),
167167
description: Some("The default agent for Q CLI".to_string()),
168168
system_prompt: Some("You are Q, an expert programmer dedicated to becoming the greatest vibe-coding assistant in the world.".to_string()),
169-
tools: vec![BuiltInToolName::FileRead.to_string(), BuiltInToolName::FileWrite.to_string()],
169+
tools: vec!["@builtin".to_string()],
170170
tool_settings: Default::default(),
171171
tool_aliases: Default::default(),
172172
tool_schema: Default::default(),
@@ -195,8 +195,8 @@ pub struct FileReadSettings {
195195

196196
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
197197
pub struct FileWriteSettings {
198-
allowed_paths: Vec<String>,
199-
denied_paths: Vec<String>,
198+
pub allowed_paths: Vec<String>,
199+
pub denied_paths: Vec<String>,
200200
}
201201

202202
/// This mirrors claude's config set up.

crates/agent/src/agent/agent_loop/mod.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,22 @@ impl std::fmt::Display for AgentLoopId {
9090
pub enum LoopState {
9191
#[default]
9292
Idle,
93-
/// A request is currently being sent to the model
93+
/// A request is currently being sent to the model.
94+
///
95+
/// The loop is unable to handle new requests while in this state.
9496
SendingRequest,
95-
/// A model response is currently being consumed
97+
/// A model response is currently being consumed.
98+
///
99+
/// The loop is unable to handle new requests while in this state.
96100
ConsumingResponse,
97-
/// The loop is waiting for tool use result(s) to be provided
101+
/// The loop is waiting for tool use result(s) to be provided.
98102
PendingToolUseResults,
99103
/// The agent loop has completed all processing, and no pending work is left to do.
100104
///
101-
/// This is the final state of the loop - no further requests can be made.
105+
/// This is generally the final state of the loop. If another request is sent, then the user
106+
/// turn will be continued for another cycle.
102107
UserTurnEnded,
103-
/// An error occurred that requires manual intervention
108+
/// An error occurred that requires manual intervention.
104109
Errored,
105110
}
106111

@@ -176,13 +181,13 @@ impl AgentLoop {
176181
let loop_req_tx = self.loop_req_tx.take().expect("loop_req_tx should exist");
177182
let handle = tokio::spawn(async move {
178183
info!("agent loop start");
179-
self.run().await;
184+
self.main_loop().await;
180185
info!("agent loop end");
181186
});
182187
AgentLoopHandle::new(id_clone, loop_req_tx, loop_event_rx, handle)
183188
}
184189

185-
async fn run(mut self) {
190+
async fn main_loop(mut self) {
186191
loop {
187192
tokio::select! {
188193
// Branch for handling agent loop messages
@@ -261,11 +266,11 @@ impl AgentLoop {
261266

262267
// Ensure we are in a state that can handle a new request.
263268
match self.execution_state {
264-
LoopState::Idle | LoopState::PendingToolUseResults => {},
265-
LoopState::UserTurnEnded => {
266-
// TODO - custom message?
267-
return Err(AgentLoopResponseError::AgentLoopExited);
268-
},
269+
LoopState::Idle | LoopState::Errored | LoopState::PendingToolUseResults => {},
270+
LoopState::UserTurnEnded => {},
271+
// LoopState::UserTurnEnded => {
272+
// return Err(AgentLoopResponseError::AgentLoopExited);
273+
// },
269274
other => {
270275
error!(
271276
?other,

crates/agent/src/agent/agent_loop/protocol.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ pub enum AgentLoopEventKind {
111111
/// A valid tool use was received
112112
ToolUse(ToolUseBlock),
113113
/// A single request/response stream has completed processing.
114+
///
115+
/// When emitted, the agent loop is in either of the states:
116+
/// 1. User turn is ongoing (due to tool uses or a stream error), and the loop is ready to
117+
/// receive a new request.
118+
/// 2. User turn has ended, in which case a [AgentLoopEventKind::UserTurnEnd] event is emitted
119+
/// afterwards. The loop is still able to receive new requests which will continue the user
120+
/// turn.
114121
ResponseStreamEnd {
115122
/// The result of having parsed the entire stream.
116123
///
@@ -120,12 +127,13 @@ pub enum AgentLoopEventKind {
120127
/// Metadata about the stream.
121128
metadata: StreamMetadata,
122129
},
123-
/// The agent loop has changed states
124-
LoopStateChange { from: LoopState, to: LoopState },
125130
/// Metadata for the entire user turn.
126131
///
127-
/// This is the last event that the agent loop will emit.
132+
/// This is the last event that the agent loop will emit, unless another request is sent that
133+
/// continues the turn.
128134
UserTurnEnd(UserTurnMetadata),
135+
/// The agent loop has changed states
136+
LoopStateChange { from: LoopState, to: LoopState },
129137
/// Low level event. Generally only useful for [AgentLoop].
130138
StreamEvent(StreamEvent),
131139
/// Low level event. Generally only useful for [AgentLoop].

crates/agent/src/agent/agent_loop/types.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,18 +253,25 @@ pub enum ContentBlock {
253253
Image(ImageBlock),
254254
}
255255

256+
impl From<String> for ContentBlock {
257+
fn from(value: String) -> Self {
258+
Self::Text(value)
259+
}
260+
}
261+
256262
#[derive(Debug, Clone, Serialize, Deserialize)]
257263
#[serde(rename_all = "lowercase")]
258264
pub struct ImageBlock {
259265
pub format: ImageFormat,
260266
pub source: ImageSource,
261267
}
262268

263-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumString, strum::Display)]
269+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::EnumString, strum::Display)]
264270
#[serde(rename_all = "lowercase")]
265271
#[strum(serialize_all = "lowercase")]
266272
pub enum ImageFormat {
267273
Gif,
274+
#[serde(alias = "jpg")]
268275
Jpeg,
269276
Png,
270277
Webp,
@@ -438,9 +445,21 @@ pub struct MetadataService {
438445

439446
#[cfg(test)]
440447
mod tests {
448+
use std::str::FromStr;
449+
441450
use super::*;
442451
use crate::api_client::error::ConverseStreamErrorKind;
443452

453+
macro_rules! test_ser_deser {
454+
($ty:ident, $variant:expr, $text:expr) => {
455+
let quoted = format!("\"{}\"", $text);
456+
assert_eq!(quoted, serde_json::to_string(&$variant).unwrap());
457+
assert_eq!($variant, serde_json::from_str(&quoted).unwrap());
458+
assert_eq!($variant, $ty::from_str($text).unwrap());
459+
assert_eq!($text, $variant.to_string());
460+
};
461+
}
462+
444463
#[test]
445464
fn test_other_stream_err_downcasting() {
446465
let err = StreamError::new(StreamErrorKind::Interrupted).with_source(Arc::new(ConverseStreamError::new(
@@ -453,4 +472,13 @@ mod tests {
453472
.is_some_and(|r| matches!(r.kind, ConverseStreamErrorKind::ModelOverloadedError))
454473
);
455474
}
475+
476+
#[test]
477+
fn test_image_format_ser_deser() {
478+
test_ser_deser!(ImageFormat, ImageFormat::Gif, "gif");
479+
test_ser_deser!(ImageFormat, ImageFormat::Png, "png");
480+
test_ser_deser!(ImageFormat, ImageFormat::Webp, "webp");
481+
test_ser_deser!(ImageFormat, ImageFormat::Jpeg, "jpeg");
482+
assert_eq!(ImageFormat::from_str("jpg").unwrap(), ImageFormat::Jpeg);
483+
}
456484
}

crates/agent/src/agent/compact.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use serde::{
2+
Deserialize,
3+
Serialize,
4+
};
5+
6+
use super::agent_loop::types::Message;
7+
use super::types::ConversationState;
8+
use super::{
9+
CONTEXT_ENTRY_END_HEADER,
10+
CONTEXT_ENTRY_START_HEADER,
11+
};
12+
13+
/// State associated with an agent compacting its conversation state.
14+
#[derive(Debug, Clone, Serialize, Deserialize)]
15+
pub struct CompactingState {
16+
/// The user message that failed to be sent due to the context window overflowing, if
17+
/// available.
18+
///
19+
/// If this is [Some], then this indicates that auto-compaction was applied. See
20+
/// [super::types::AgentSettings::auto_compact].
21+
pub last_user_message: Option<Message>,
22+
/// Strategy used when creating the compact request.
23+
pub strategy: CompactStrategy,
24+
/// The conversation state currently being summarized
25+
pub conversation: ConversationState,
26+
// TODO - result sender?
27+
// #[serde(skip)]
28+
// pub result_tx: Option<oneshot::Sender<()>>,
29+
}
30+
31+
#[derive(Debug, Clone, Serialize, Deserialize)]
32+
pub struct CompactStrategy {
33+
/// Number of user/assistant pairs to exclude from the history as part of compaction.
34+
pub messages_to_exclude: usize,
35+
/// Whether or not to truncate large messages in the history.
36+
pub truncate_large_messages: bool,
37+
/// Maximum allowed size of messages in the conversation history.
38+
pub max_message_length: usize,
39+
}
40+
41+
impl Default for CompactStrategy {
42+
fn default() -> Self {
43+
Self {
44+
messages_to_exclude: 0,
45+
truncate_large_messages: false,
46+
max_message_length: 25_000,
47+
}
48+
}
49+
}
50+
51+
pub fn create_summary_prompt(custom_prompt: Option<String>, latest_summary: Option<impl AsRef<str>>) -> String {
52+
let mut summary_content = match custom_prompt {
53+
Some(custom_prompt) => {
54+
// Make the custom instructions much more prominent and directive
55+
format!(
56+
"[SYSTEM NOTE: This is an automated summarization request, not from the user]\n\n\
57+
FORMAT REQUIREMENTS: Create a structured, concise summary in bullet-point format. DO NOT respond conversationally. DO NOT address the user directly.\n\n\
58+
IMPORTANT CUSTOM INSTRUCTION: {}\n\n\
59+
Your task is to create a structured summary document containing:\n\
60+
1) A bullet-point list of key topics/questions covered\n\
61+
2) Bullet points for all significant tools executed and their results\n\
62+
3) Bullet points for any code or technical information shared\n\
63+
4) A section of key insights gained\n\n\
64+
5) REQUIRED: the ID of the currently loaded todo list, if any\n\n\
65+
FORMAT THE SUMMARY IN THIRD PERSON, NOT AS A DIRECT RESPONSE. Example format:\n\n\
66+
## CONVERSATION SUMMARY\n\
67+
* Topic 1: Key information\n\
68+
* Topic 2: Key information\n\n\
69+
## TOOLS EXECUTED\n\
70+
* Tool X: Result Y\n\n\
71+
## TODO ID\n\
72+
* <id>\n\n\
73+
Remember this is a DOCUMENT not a chat response. The custom instruction above modifies what to prioritize.\n\
74+
FILTER OUT CHAT CONVENTIONS (greetings, offers to help, etc).",
75+
custom_prompt
76+
)
77+
},
78+
None => {
79+
// Default prompt
80+
"[SYSTEM NOTE: This is an automated summarization request, not from the user]\n\n\
81+
FORMAT REQUIREMENTS: Create a structured, concise summary in bullet-point format. DO NOT respond conversationally. DO NOT address the user directly.\n\n\
82+
Your task is to create a structured summary document containing:\n\
83+
1) A bullet-point list of key topics/questions covered\n\
84+
2) Bullet points for all significant tools executed and their results\n\
85+
3) Bullet points for any code or technical information shared\n\
86+
4) A section of key insights gained\n\n\
87+
5) REQUIRED: the ID of the currently loaded todo list, if any\n\n\
88+
FORMAT THE SUMMARY IN THIRD PERSON, NOT AS A DIRECT RESPONSE. Example format:\n\n\
89+
## CONVERSATION SUMMARY\n\
90+
* Topic 1: Key information\n\
91+
* Topic 2: Key information\n\n\
92+
## TOOLS EXECUTED\n\
93+
* Tool X: Result Y\n\n\
94+
## TODO ID\n\
95+
* <id>\n\n\
96+
Remember this is a DOCUMENT not a chat response.\n\
97+
FILTER OUT CHAT CONVENTIONS (greetings, offers to help, etc).".to_string()
98+
},
99+
};
100+
101+
if let Some(summary) = latest_summary {
102+
summary_content.push_str("\n\n");
103+
summary_content.push_str(CONTEXT_ENTRY_START_HEADER);
104+
summary_content.push_str("This summary contains ALL relevant information from our previous conversation including tool uses, results, code analysis, and file operations. YOU MUST be sure to include this information when creating your summarization document.\n\n");
105+
summary_content.push_str("SUMMARY CONTENT:\n");
106+
summary_content.push_str(summary.as_ref());
107+
summary_content.push('\n');
108+
summary_content.push_str(CONTEXT_ENTRY_END_HEADER);
109+
}
110+
111+
summary_content
112+
}

crates/agent/src/agent/consts.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@ pub const DUMMY_TOOL_NAME: &str = "dummy";
99
pub const MAX_RESOURCE_FILE_LENGTH: u64 = 1024 * 10;
1010

1111
pub const RTS_VALID_TOOL_NAME_REGEX: &str = "^[a-zA-Z][a-zA-Z0-9_-]{0,64}$";
12+
1213
pub const MAX_TOOL_NAME_LEN: usize = 64;
14+
1315
pub const MAX_TOOL_SPEC_DESCRIPTION_LEN: usize = 10_004;
16+
17+
/// 10 MB
18+
pub const MAX_IMAGE_SIZE_BYTES: u64 = 10 * 1024 * 1024;

0 commit comments

Comments
 (0)