Skip to content

Commit 9338032

Browse files
authored
Fix: Resolve agentic behaviour bug bash items (#654)
1 parent 652148d commit 9338032

File tree

4 files changed

+57
-7
lines changed

4 files changed

+57
-7
lines changed

crates/q_cli/src/cli/chat/parser.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ pub struct ResponseParser {
5353
assistant_text: String,
5454
/// Tool uses requested by the model.
5555
tool_uses: Vec<ToolUse>,
56+
/// Buffered line required in case we need to discard a code reference event
57+
buffered_line: Option<String>,
58+
/// Short circuit and return early since we simply need to clear our buffered line
59+
short_circuit: bool,
5660
}
5761

5862
impl ResponseParser {
@@ -63,17 +67,39 @@ impl ResponseParser {
6367
message_id: None,
6468
assistant_text: String::new(),
6569
tool_uses: Vec::new(),
70+
buffered_line: None,
71+
short_circuit: false,
6672
}
6773
}
6874

6975
/// Consumes the associated [ConverseStreamResponse] until a valid [ResponseEvent] is parsed.
7076
pub async fn recv(&mut self) -> Result<ResponseEvent> {
77+
if self.short_circuit {
78+
let message = Message(ChatMessage::AssistantResponseMessage(AssistantResponseMessage {
79+
message_id: self.message_id.take(),
80+
content: std::mem::take(&mut self.assistant_text),
81+
tool_uses: if self.tool_uses.is_empty() {
82+
None
83+
} else {
84+
Some(self.tool_uses.clone().into_iter().map(Into::into).collect())
85+
},
86+
}));
87+
return Ok(ResponseEvent::EndStream { message });
88+
}
89+
7190
loop {
7291
match self.next().await {
7392
Ok(Some(output)) => match output {
7493
ChatResponseStream::AssistantResponseEvent { content } => {
7594
self.assistant_text.push_str(&content);
76-
return Ok(ResponseEvent::AssistantText(content));
95+
let text = self.buffered_line.take();
96+
self.buffered_line = Some(content);
97+
if let Some(text) = text {
98+
return Ok(ResponseEvent::AssistantText(text));
99+
}
100+
},
101+
ChatResponseStream::CodeReferenceEvent(_) => {
102+
self.buffered_line = None;
77103
},
78104
ChatResponseStream::InvalidStateEvent { reason, message } => {
79105
error!(%reason, %message, "invalid state event");
@@ -102,6 +128,11 @@ impl ResponseParser {
102128
_ => {},
103129
},
104130
Ok(None) => {
131+
if let Some(text) = self.buffered_line.take() {
132+
self.short_circuit = true;
133+
return Ok(ResponseEvent::AssistantText(text));
134+
}
135+
105136
let message = Message(ChatMessage::AssistantResponseMessage(AssistantResponseMessage {
106137
message_id: self.message_id.take(),
107138
content: std::mem::take(&mut self.assistant_text),

crates/q_cli/src/cli/chat/tools/execute_bash.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use super::{
2222
#[derive(Debug, Deserialize)]
2323
pub struct ExecuteBash {
2424
pub command: String,
25+
pub interactive: bool,
2526
}
2627

2728
impl ExecuteBash {
@@ -34,11 +35,17 @@ impl ExecuteBash {
3435
style::Print("\n"),
3536
)?;
3637

38+
let (stdin, stdout, stderr) = match self.interactive {
39+
true => (Stdio::inherit(), Stdio::inherit(), Stdio::inherit()),
40+
false => (Stdio::piped(), Stdio::piped(), Stdio::piped()),
41+
};
42+
3743
let output = tokio::process::Command::new("bash")
3844
.arg("-c")
3945
.arg(&self.command)
40-
.stdout(Stdio::piped())
41-
.stderr(Stdio::piped())
46+
.stdin(stdin)
47+
.stdout(stdout)
48+
.stderr(stderr)
4249
.spawn()
4350
.wrap_err_with(|| format!("Unable to spawn command '{}'", &self.command))?
4451
.wait_with_output()
@@ -82,7 +89,8 @@ mod tests {
8289

8390
// Verifying stdout
8491
let v = serde_json::json!({
85-
"command": "echo Hello, world!"
92+
"command": "echo Hello, world!",
93+
"interactive": false
8694
});
8795
let out = serde_json::from_value::<ExecuteBash>(v)
8896
.unwrap()
@@ -100,7 +108,8 @@ mod tests {
100108

101109
// Verifying stderr
102110
let v = serde_json::json!({
103-
"command": "echo Hello, world! 1>&2"
111+
"command": "echo Hello, world! 1>&2",
112+
"interactive": false
104113
});
105114
let out = serde_json::from_value::<ExecuteBash>(v)
106115
.unwrap()
@@ -118,7 +127,8 @@ mod tests {
118127

119128
// Verifying exit code
120129
let v = serde_json::json!({
121-
"command": "exit 1"
130+
"command": "exit 1",
131+
"interactive": false
122132
});
123133
let out = serde_json::from_value::<ExecuteBash>(v)
124134
.unwrap()

crates/q_cli/src/cli/chat/tools/fs_read.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crossterm::style::{
1111
};
1212
use eyre::{
1313
Result,
14+
bail,
1415
eyre,
1516
};
1617
use fig_os_shim::Context;
@@ -233,6 +234,10 @@ impl FsRead {
233234
}
234235

235236
pub async fn validate(&mut self, ctx: &Context) -> Result<()> {
237+
if !PathBuf::from(&self.path).exists() {
238+
bail!("'{}' does not exist", self.path);
239+
}
240+
236241
let is_file = ctx.fs().symlink_metadata(&self.path).await?.is_file();
237242
self.ty = Some(is_file);
238243

crates/q_cli/src/cli/chat/tools/tool_index.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
[
22
{
33
"name": "execute_bash",
4-
"description": "Execute the specified bash command",
4+
"description": "Execute the specified bash command.",
55
"input_schema": {
66
"type": "object",
77
"properties": {
88
"command": {
99
"type": "string",
1010
"description": "Bash command to execute"
11+
},
12+
"interactive": {
13+
"type": "boolean",
14+
"description": "Whether or not the command is interactive. Interactive commands like nano will overtake our conversation until exited. On exit, they will have produced no stderr or stdout."
1115
}
1216
},
1317
"required": ["command"]

0 commit comments

Comments
 (0)