Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
697f583
fix(compaction): align manual compress reserve with the auto trigger
xavierforge Jun 28, 2026
698c8eb
fix(compaction): announce only when compaction runs and let /compress…
xavierforge Jun 28, 2026
88c4556
fix(compaction): drop stale "prompt cleared" notice from compress
xavierforge Jun 28, 2026
0ca24a6
fix(statusline): show context percentage honestly past 100%
xavierforge Jun 28, 2026
292be5a
fix(compaction): reword mid-turn notice to match what it does
xavierforge Jun 28, 2026
90e147b
fix(agent): forward reasoning deltas
fayalalebrun Jun 21, 2026
f11b6d8
Persist tool outputs in sessions
fayalalebrun Jun 22, 2026
51c9bb2
Add show_reasoning config option
fayalalebrun Jun 28, 2026
10ac927
Merge pull request #141 from fayalalebrun/upstream-pr/03a-forward-rea…
gi-dellav Jun 28, 2026
c27d48c
Merge pull request #143 from fayalalebrun/upstream-pr/04a-persist-too…
gi-dellav Jun 28, 2026
831e203
Merge pull request #145 from xavierforge/fix/context-meter-over-100
gi-dellav Jun 28, 2026
a52bea4
Merge pull request #144 from xavierforge/fix/compaction-announce-and-…
gi-dellav Jun 28, 2026
5158b34
formatted + fixed warnings
gi-dellav Jun 28, 2026
16ac680
updated deps
gi-dellav Jun 28, 2026
d930373
don't show reasoning by default
gi-dellav Jun 28, 2026
69627ed
push to v1.5.1-rc2
gi-dellav Jun 28, 2026
e9132ca
Store long tool outputs as sidecar artifacts
fayalalebrun Jun 28, 2026
9b22dfc
Flush atomic temp files before rename
fayalalebrun Jun 28, 2026
3514cfb
Merge pull request #150 from fayalalebrun/upstream-pr/00-fix-ci-tests
gi-dellav Jun 28, 2026
6a701cd
Merge pull request #148 from fayalalebrun/upstream-pr/04b-long-tool-o…
gi-dellav Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
973 changes: 464 additions & 509 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "zerostack"
version = "1.5.1-rc1"
version = "1.5.1-rc2"
edition = "2024"
authors = ["Giuseppe Della Vedova"]
description = "Minimalistic coding agent written in Rust, optimized for memory footprint and performance"
Expand Down Expand Up @@ -32,8 +32,8 @@ pdf = ["multimodal", "rig/pdf"]
advisor = []

[dependencies]
rig = { version = "0.38", features = ["rmcp"] }
rmcp = { version = "1.7", optional = true, default-features = false, features = [
rig = { version = "0.39", features = ["rmcp"] }
rmcp = { version = "1.8", optional = true, default-features = false, features = [
"client",
"transport-child-process",
"transport-streamable-http-client-reqwest",
Expand Down
1 change: 1 addition & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ Accepted top-level keys:
| `sandbox` | boolean | Run bash commands in the bubblewrap sandbox. Default: `false`. |
| `default_permission_mode` | string | Permission mode when no mode boolean/CLI flag is set. Accepts: `standard` (default), `restrictive`, `readonly`, `guarded`, `yolo`. |
| `show_tool_details` | boolean or integer | Show tool-result previews in the TUI. `false` hides output, `true` shows all lines, an integer limits to that many lines (e.g. `3`). Default: `3`. |
| `show_reasoning` | boolean | Show streamed reasoning text in the TUI. Can still be toggled at runtime with `Ctrl+R` or `/reasoning`. Default: `false`. |
| `statusline` | table | Configurable status bar (up to 3 lines of colored segments). When absent, a built-in default layout is used. See Status bar below. |
| `chat_left_margin` | integer | Left padding (columns) for the chat area only; input and status rows are unaffected. Default: `0`. |
| `default_prompt` | string | Prompt name to activate on startup. Default: `code`. If the prompt file has a `%%mode=<mode>` first-line directive, the security mode is set automatically (see Prompt directives below). |
Expand Down
2 changes: 1 addition & 1 deletion packaging/aur/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Upstream: https://github.com/gi-dellav/zerostack

pkgname=zerostack-bin
pkgver=1.5.1-rc1
pkgver=1.5.1-rc2
pkgrel=1
pkgdesc="Minimalistic coding agent written in Rust, optimized for memory footprint and performance"
arch=('x86_64' 'aarch64')
Expand Down
2 changes: 1 addition & 1 deletion packaging/conda/zerostack-bin/meta.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% set name = "zerostack-bin" %}
{% set version = "1.5.1-rc1" %}
{% set version = "1.5.1-rc2" %}

package:
name: {{ name }}
Expand Down
2 changes: 1 addition & 1 deletion packaging/conda/zerostack/meta.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% set name = "zerostack" %}
{% set version = "1.5.1-rc1" %}
{% set version = "1.5.1-rc2" %}

package:
name: {{ name }}
Expand Down
10 changes: 5 additions & 5 deletions packaging/homebrew/zerostack.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
class Zerostack < Formula
desc "Minimalistic coding agent written in Rust, optimized for memory footprint and performance"
homepage "https://github.com/gi-dellav/zerostack"
version "1.5.1-rc1"
version "1.5.1-rc2"
license "GPL-3.0-only"

on_macos do
if Hardware::CPU.intel?
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc1/zerostack-x86_64-apple-darwin.tar.gz"
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc2/zerostack-x86_64-apple-darwin.tar.gz"
sha256 "0c5ce2d6cc251bb6dd782f250a321bbb13084d0e88a9536d9d86d7b04a5779e0"
else
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc1/zerostack-aarch64-apple-darwin.tar.gz"
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc2/zerostack-aarch64-apple-darwin.tar.gz"
sha256 "100c1a7182343d916e126b342e3cc32bf12bf44e0fa17e392d441171b31c3ebc"
end
end

on_linux do
if Hardware::CPU.intel?
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc1/zerostack-x86_64-unknown-linux-musl.tar.gz"
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc2/zerostack-x86_64-unknown-linux-musl.tar.gz"
sha256 "b8b35c4afdc5866ec137e1c15d3dfe47382bc05be17c1360fff1c0382a912aff"
else
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc1/zerostack-aarch64-unknown-linux-musl.tar.gz"
url "https://github.com/gi-dellav/zerostack/releases/download/v1.5.1-rc2/zerostack-aarch64-unknown-linux-musl.tar.gz"
sha256 "0d81ceef899f2a8be800857dca6165fbf3bee682ba12f0f9ccd291abaca7ec0c"
end
end
Expand Down
127 changes: 90 additions & 37 deletions src/agent/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ pub struct BtwRunner {
pub abort_handle: tokio::task::AbortHandle,
}

fn streamed_reasoning_text<R>(content: &StreamedAssistantContent<R>) -> Option<CompactString> {
match content {
StreamedAssistantContent::Reasoning(reasoning) => {
Some(CompactString::new(reasoning.display_text()))
}
StreamedAssistantContent::ReasoningDelta { reasoning, .. } => {
if reasoning.is_empty() {
None
} else {
Some(CompactString::from(reasoning.as_str()))
}
}
_ => None,
}
}

/// Spawn an isolated, single-turn, tool-less side-question run. The full result
/// is delivered as a single [`BtwEvent::Done`] (or [`BtwEvent::Error`]) tagged
/// with `id`. Unlike [`spawn_agent`], it never registers a subagent event sink
Expand Down Expand Up @@ -122,12 +138,22 @@ pub fn convert_history(session: &Session) -> Vec<Message> {
match msg.role {
MessageRole::User => messages.push(Message::user(msg.content.to_string())),
MessageRole::Assistant => messages.push(Message::assistant(msg.content.to_string())),
// Convert any persisted System messages to Assistant for the
// Convert non-user transcript records to Assistant for the
// same reason as the summary above: the templates that reject
// mid-stream System tolerate Assistant, and code-symmetry with
// mid-stream System/tool roles tolerate Assistant, and code-symmetry with
// the summary push keeps the resumed-conversation shape
// consistent.
MessageRole::System => messages.push(Message::assistant(msg.content.to_string())),
MessageRole::ToolCall => {
messages.push(Message::assistant(format!("[ToolCall]: {}", msg.content)))
}
MessageRole::ToolResult => {
messages.push(Message::assistant(format!("[ToolResult]: {}", msg.content)))
}
MessageRole::SubagentToolCall => messages.push(Message::assistant(format!(
"[SubagentToolCall]: {}",
msg.content
))),
}
}

Expand Down Expand Up @@ -261,31 +287,30 @@ where
loop {
while let Some(item) = stream.next().await {
match item {
Ok(MultiTurnStreamItem::StreamAssistantItem(
StreamedAssistantContent::Text(text),
)) => {
let _ = event_tx
.send(AgentEvent::Token(CompactString::from(text.text)))
.await;
}
Ok(MultiTurnStreamItem::StreamAssistantItem(
StreamedAssistantContent::Reasoning(r),
)) => {
let _ = event_tx
.send(AgentEvent::Reasoning(CompactString::new(r.display_text())))
.await;
}
Ok(MultiTurnStreamItem::StreamAssistantItem(
StreamedAssistantContent::ToolCall { tool_call, .. },
)) => {
last_tool_name = Some(tool_call.function.name.clone());
tool_interactions.push(tool_call.clone().into());
let _ = event_tx
.send(AgentEvent::ToolCall {
name: CompactString::from(tool_call.function.name),
args: tool_call.function.arguments,
})
.await;
Ok(MultiTurnStreamItem::StreamAssistantItem(content)) => {
if let Some(reasoning) = streamed_reasoning_text(&content) {
let _ = event_tx.send(AgentEvent::Reasoning(reasoning)).await;
continue;
}

match content {
StreamedAssistantContent::Text(text) => {
let _ = event_tx
.send(AgentEvent::Token(CompactString::from(text.text)))
.await;
}
StreamedAssistantContent::ToolCall { tool_call, .. } => {
last_tool_name = Some(tool_call.function.name.clone());
tool_interactions.push(tool_call.clone().into());
let _ = event_tx
.send(AgentEvent::ToolCall {
name: CompactString::from(tool_call.function.name),
args: tool_call.function.arguments,
})
.await;
}
_ => {}
}
}
Ok(MultiTurnStreamItem::StreamUserItem(StreamedUserContent::ToolResult {
tool_result,
Expand Down Expand Up @@ -327,16 +352,15 @@ where
break;
}
Ok(MultiTurnStreamItem::CompletionCall(call)) => {
if let Some(usage) = call.usage {
let _ = event_tx
.send(AgentEvent::CompletionCall {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
cached_input_tokens: usage.cached_input_tokens,
cache_creation_input_tokens: usage.cache_creation_input_tokens,
})
.await;
}
let usage = call.usage;
let _ = event_tx
.send(AgentEvent::CompletionCall {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
cached_input_tokens: usage.cached_input_tokens,
cache_creation_input_tokens: usage.cache_creation_input_tokens,
})
.await;
}
Err(e) => {
let _ = event_tx
Expand Down Expand Up @@ -532,3 +556,32 @@ where

Ok(full_response)
}

#[cfg(test)]
mod tests {
use super::streamed_reasoning_text;
use rig::streaming::StreamedAssistantContent;

#[test]
fn streamed_reasoning_delta_is_forwardable_as_reasoning_text() {
let content = StreamedAssistantContent::<()>::ReasoningDelta {
id: Some("rs_demo".to_string()),
reasoning: "thinking in progress".to_string(),
};

assert_eq!(
streamed_reasoning_text(&content).as_deref(),
Some("thinking in progress")
);
}

#[test]
fn empty_reasoning_delta_is_ignored() {
let content = StreamedAssistantContent::<()>::ReasoningDelta {
id: None,
reasoning: String::new(),
};

assert!(streamed_reasoning_text(&content).is_none());
}
}
6 changes: 6 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ pub struct Config {
pub permission_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_tool_details: Option<ShowToolDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_reasoning: Option<bool>,
/// Configurable status-bar (up to 3 lines). When absent, a built-in
/// default layout is used.
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -382,6 +384,10 @@ impl Config {
self.always_show_welcome.unwrap_or(false)
}

pub fn resolve_show_reasoning(&self) -> bool {
self.show_reasoning.unwrap_or(false)
}

pub fn resolve_auto_update_prompts(&self) -> Option<bool> {
self.auto_update_prompts
}
Expand Down
3 changes: 3 additions & 0 deletions src/extras/advisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ pub(crate) fn format_conversation(msgs: &[SessionMessage], kilobytes_limit: u32)
MessageRole::User => "User",
MessageRole::Assistant => "Assistant",
MessageRole::System => "System",
MessageRole::ToolCall => "ToolCall",
MessageRole::ToolResult => "ToolResult",
MessageRole::SubagentToolCall => "SubagentToolCall",
};
format!("[{role}]: {}", msg.content)
}
Expand Down
1 change: 1 addition & 0 deletions src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub async fn atomic_write(
let write_result = async {
let mut f = tokio::fs::File::create(&tmp_path).await?;
f.write_all(contents.as_ref()).await?;
f.flush().await?;
Ok::<(), std::io::Error>(())
}
.await;
Expand Down
3 changes: 3 additions & 0 deletions src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,9 @@ pub(crate) fn serialize_conversation(messages: &[SessionMessage]) -> String {
crate::session::MessageRole::User => "User",
crate::session::MessageRole::Assistant => "Assistant",
crate::session::MessageRole::System => "System",
crate::session::MessageRole::ToolCall => "ToolCall",
crate::session::MessageRole::ToolResult => "ToolResult",
crate::session::MessageRole::SubagentToolCall => "SubagentToolCall",
};
result.push_str(&format!("[{}]: {}\n\n", role_tag, msg.content));
}
Expand Down
6 changes: 3 additions & 3 deletions src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,9 @@ impl Sandbox {
})
.await
.is_err()
&& let Some(pid) = guard.pid
{
if let Some(pid) = guard.pid {
kill_process_group(pid);
}
kill_process_group(pid);
}
let stdout = stdout.lock().unwrap_or_else(|e| e.into_inner()).clone();
let stderr = stderr.lock().unwrap_or_else(|e| e.into_inner()).clone();
Expand All @@ -223,6 +222,7 @@ impl Sandbox {
}
}

#[allow(dead_code)]
pub fn active_group_count(&self) -> usize {
self.active_groups
.lock()
Expand Down
Loading
Loading