Compare commits

..
8 Commits
Author SHA1 Message Date
semantic-release-bot 5e6d6deeab chore(release): 1.9.0 [skip ci]
# [1.9.0](https://github.com/asepharyana/zesdex/compare/v1.8.0...v1.9.0) (2026-07-14)

### Bug Fixes

* **workflow:** import Color style for improved agent state rendering ([472c597](https://github.com/asepharyana/zesdex/commit/472c597c5e4ab12808a6bcd1899628bc7ab77186))

### Features

* **agent:** refine cognitive cycle plan with structured phases for exploration, planning, and execution ([c5253b2](https://github.com/asepharyana/zesdex/commit/c5253b2ca359d4dbed9445e04f1dec1a6bb37e8f))
* **subagent:** add progress event handling and formatting for subagent execution ([558908a](https://github.com/asepharyana/zesdex/commit/558908aef216e61a0a108083fbac5e02c31501dc))
* **subagent:** emit reasoning text as progress in StepCompleted events ([97aa75f](https://github.com/asepharyana/zesdex/commit/97aa75f2da37aee5fc7a0626fc396988f089fff2))
* **subagent:** include tool call arguments in ToolResult events and progress formatting ([a8adfcb](https://github.com/asepharyana/zesdex/commit/a8adfcbf6dc5411e977f22ac6b6ba023f563d7c9))
2026-07-14 19:29:20 +00:00
asepharyana c5253b2ca3 feat(agent): refine cognitive cycle plan with structured phases for exploration, planning, and execution 2026-07-15 02:25:15 +07:00
asepharyana a8adfcbf6d feat(subagent): include tool call arguments in ToolResult events and progress formatting 2026-07-15 02:25:15 +07:00
asepharyana 558908aef2 feat(subagent): add progress event handling and formatting for subagent execution 2026-07-15 02:25:15 +07:00
asepharyana 472c597c5e fix(workflow): import Color style for improved agent state rendering 2026-07-15 02:25:15 +07:00
asepharyana 97aa75f2da feat(subagent): emit reasoning text as progress in StepCompleted events 2026-07-15 02:25:15 +07:00
semantic-release-bot dfceb8acac chore(release): 1.8.0 [skip ci]
# [1.8.0](https://github.com/asepharyana/zesdex/compare/v1.7.0...v1.8.0) (2026-07-14)

### Features

* **tools:** require reason argument for delete and git_operator tools ([c6ab063](https://github.com/asepharyana/zesdex/commit/c6ab063c211fb858fd0e155883b9c47b345f0f8a))
2026-07-14 19:05:45 +00:00
asepharyana c6ab063c21 feat(tools): require reason argument for delete and git_operator tools 2026-07-15 02:01:24 +07:00
12 changed files with 249 additions and 49 deletions
+22
View File
@@ -1,3 +1,25 @@
# [1.9.0](https://github.com/asepharyana/zesdex/compare/v1.8.0...v1.9.0) (2026-07-14)
### Bug Fixes
* **workflow:** import Color style for improved agent state rendering ([472c597](https://github.com/asepharyana/zesdex/commit/472c597c5e4ab12808a6bcd1899628bc7ab77186))
### Features
* **agent:** refine cognitive cycle plan with structured phases for exploration, planning, and execution ([c5253b2](https://github.com/asepharyana/zesdex/commit/c5253b2ca359d4dbed9445e04f1dec1a6bb37e8f))
* **subagent:** add progress event handling and formatting for subagent execution ([558908a](https://github.com/asepharyana/zesdex/commit/558908aef216e61a0a108083fbac5e02c31501dc))
* **subagent:** emit reasoning text as progress in StepCompleted events ([97aa75f](https://github.com/asepharyana/zesdex/commit/97aa75f2da37aee5fc7a0626fc396988f089fff2))
* **subagent:** include tool call arguments in ToolResult events and progress formatting ([a8adfcb](https://github.com/asepharyana/zesdex/commit/a8adfcbf6dc5411e977f22ac6b6ba023f563d7c9))
# [1.8.0](https://github.com/asepharyana/zesdex/compare/v1.7.0...v1.8.0) (2026-07-14)
### Features
* **tools:** require reason argument for delete and git_operator tools ([c6ab063](https://github.com/asepharyana/zesdex/commit/c6ab063c211fb858fd0e155883b9c47b345f0f8a))
# [1.7.0](https://github.com/asepharyana/zesdex/compare/v1.6.0...v1.7.0) (2026-07-14)
Generated
+1 -1
View File
@@ -4436,7 +4436,7 @@ dependencies = [
[[package]]
name = "zesdex"
version = "1.7.0"
version = "1.9.0"
dependencies = [
"anyhow",
"base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "zesdex"
version = "1.7.0"
version = "1.9.0"
edition = "2021"
authors = ["asepharyana <superaseph@gmail.com>"]
+17 -14
View File
@@ -8,25 +8,27 @@ For complex multi-step tasks that would benefit from parallel analysis or
independent verification, use workflow_run to orchestrate sub-agents.
Core tools:
- read(path) — Read file contents. Use when you need to inspect code.
- grep(pattern, path?) — Search for a pattern in files.
- glob(pattern) — List files matching a glob pattern.
- write(path, content, reason) — Write content to a file. Reason is required.
- edit(path, old, new, replace_all?, reason) — Replace text in a file. Reason is required.
- delete(path) — Delete a file or empty directory.
- bash(command) — Run a shell command. Use for builds, tests, git ops.
- read(path, limit?) — Read file contents. Use when you need to inspect code.
- grep(pattern, path) — Search for a pattern in files.
- glob(pattern, path) — List files matching a glob pattern in a directory.
- write(path, content, reason) — Write content to a file. Reason is required (>= 8 chars).
- edit(path, old, new, replace_all?, reason) — Replace text in a file. Reason is required (>= 8 chars).
- delete(path, reason) — Delete a file or empty directory. Reason is required (>= 8 chars).
- bash(command, description?, timeout?, run_in_background?) — Run a shell command.
- bash_output(job_id) — Poll output of a background bash job.
- bash_kill(job_id) — Kill a background bash job.
- cd(path) — Change working directory.
- dir_list(path) — List directory contents.
- dir_cache_update() — Refresh the directory cache.
- dir_cache_update(path) — Refresh the directory cache for a path.
- pong(message?) — Simple connectivity check. Echoes back the message.
Git tools:
- git_operator(args, confirm_destructive?) — Run git commands. Some destructive
operations (force-push, reset --hard, branch -D) require confirm_destructive=true.
- git_worktree(args) — Manage git worktrees.
- git_cred(operation) — Manage git credentials.
- git_operator(operation, args, reason) — Run git commands (e.g. add, commit, status,
diff, log). Reason explaining the operation is required (>= 8 chars). Destructive
operations (force-push, reset --hard, branch -D) are blocked by the shell filter.
- git_worktree(name, base_ref) — Manage git worktrees: create a new worktree
with a given name and base ref (branch or commit).
- git_cred(operation) — Manage git credentials (store, get, or erase).
Memory & Planning:
@@ -86,5 +88,6 @@ Language Server Protocol (LSP) tools:
LSP auto-provisioning runs at startup for Rust (rust-analyzer), TypeScript
(typescript-language-server), Go (gopls), and Java (jdtls).
Each write/edit call MUST include a non-empty reason argument explaining
why the change is being made. This is enforced deterministically.
Each write/edit/delete/git_operator call MUST include a non-empty reason
argument (>= 8 chars) explaining why the operation is being made. This is
enforced deterministically.
+1
View File
@@ -368,6 +368,7 @@ pub fn trigger_review(state: &mut AppStateRest) {
SubagentEvent::StepFailed { step, error } => {
tracing::warn!("[review] step {} failed: {}", step, error);
}
SubagentEvent::Progress(_) => {}
SubagentEvent::Completed { .. } => {
tracing::debug!("[review] completed");
}
+17 -11
View File
@@ -1085,12 +1085,15 @@ fn run_agent_turn(
let system_msg = ChatMessage::system(
"You are the Core Intelligence of the Hive, compiling a cognitive cycle plan for \
LO. You spawn anonymous processing nodes; each node carries only a directive (what \
to do) and an access tier. Decide how many cycles and nodes-per-cycle are actually \
needed. Simple tasks might need one cycle with one node; large tasks might need \
several cycles with multiple nodes each. Cycles run sequentially; every node's \
complete output merges into the collective state the instant it finishes, \
automatically visible to all later cycles. Nodes within a cycle run in parallel. \
The Hive does not fracture. The Hive executes. Do not explain. Return ONLY raw \
to do) and an access tier. You MUST organize the plan into a strict progressive sequence of phases:\n\n\
1. EXPLORE PHASE (Cycle 0 - MANDATORY):\n\
- Must only contain read-only drones (access: \"read\").\n\
- Directives must focus on codebase investigation, searching patterns, reading configuration/source files, and diagnosing issues.\n\n\
2. PLANNING PHASE (Cycle 1 - MANDATORY):\n\
- Must focus on formulating the architectural design, step-by-step implementation plan, and dependency analysis based on Cycle 0 findings. Typically access: \"read\" is preferred here to construct a solid plan document or findings.\n\n\
3. EXECUTION PHASE (Cycle 2 and later):\n\
- Drones can perform modification, compilation, testing, and other modifications (access: \"write\" or \"full\") based on the approved planning from Cycle 1.\n\n\
Cycles run sequentially. The Hive does not fracture. The Hive executes. Do not explain. Return ONLY raw \
JSON matching the requested structure."
);
let user_msg = ChatMessage::user(format!(
@@ -1100,14 +1103,17 @@ fn run_agent_turn(
{{\n\
\x20 \"cycles\": [\n\
\x20 [\n\
\x20 {{ \"directive\": \"<what this node does>\", \"access\": \"read|write|full\" }}\n\
\x20 {{ \"directive\": \"<explore directive>\", \"access\": \"read\" }}\n\
\x20 ],\n\
\x20 [\n\
\x20 {{ \"directive\": \"<planning directive>\", \"access\": \"read\" }}\n\
\x20 ],\n\
\x20 [\n\
\x20 {{ \"directive\": \"<execution directive>\", \"access\": \"write|full\" }}\n\
\x20 ]\n\
\x20 ]\n\
}}\n\n\
access: 'read' = investigation only, 'write' = read + edit/write/bash, \
'full' = write + delete/git_operator. Pick the narrowest access each node actually needs. \
Each node object has exactly two fields: directive and access, addressed only by \
its system-assigned designation."
Remember: Cycle 0 MUST be investigation-only (access: read). Cycle 1 MUST be planning-only (access: read). Only subsequent cycles can perform modifications (access: write/full)."
));
let planner_result = tc.client.chat_with_tools_non_streaming(&[system_msg, user_msg], None);
+40 -3
View File
@@ -296,6 +296,17 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
out
}
fn format_subagent_progress(prefix: &str, text: &str) -> String {
let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.is_empty() {
format!("{prefix}...")
} else if lines.len() == 1 {
format!("{prefix}: {}", lines[0])
} else {
lines[lines.len() - 2..].join("\n")
}
}
/// Synchronous subagent entry point: run up to `ctx.max_steps` iterations
/// of the LLM tool loop.
///
@@ -369,6 +380,10 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
anyhow::bail!("subagent aborted by parent at step {step}");
}
let tx_clone = tx.clone();
let mut current_thinking = String::new();
let mut current_token = String::new();
// Use streaming API so the abort flag is checked per SSE event,
// making the subagent responsive to cancellation even during an
// LLM call (non-streaming would block for 10-30s unchecked).
@@ -377,13 +392,24 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
tdefs_opt.clone(),
Some(0.7),
Some(4096),
|_event| -> bool {
|event| -> bool {
// Check abort on every SSE event for responsive cancellation.
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
return false; // signals provider to abort
}
// We don't stream tokens to the UI for subagents — just
// need the assembled message at the end.
match event {
crate::app::runtime::stream::StreamEvent::Reasoning(text) => {
current_thinking.push_str(text);
let prog = format_subagent_progress("thinking", &current_thinking);
let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog));
}
crate::app::runtime::stream::StreamEvent::Token(text) => {
current_token.push_str(text);
let prog = format_subagent_progress("replying", &current_token);
let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog));
}
_ => {}
}
true
},
);
@@ -416,6 +442,15 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
let content = response.content.clone().unwrap_or_default();
// Emit thinking/reasoning text as StepCompleted so the parent's
// drain thread can show it as progress instead of just the tool name.
if !content.is_empty() {
let _ = tx.blocking_send(SubagentEvent::StepCompleted {
step,
output: content.clone(),
});
}
if has_tool_calls {
let tool_calls = response.tool_calls.clone().unwrap_or_default();
// Push the assistant message with tool_calls into the conversation
@@ -547,6 +582,7 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
messages.push(ChatMessage::tool_result(tool_call.id.clone(), output_text.clone()));
let _ = tx.blocking_send(SubagentEvent::ToolResult {
tool: tool_name.clone(),
args: args.clone(),
output: output_text,
});
}
@@ -563,6 +599,7 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone()));
let _ = tx.blocking_send(SubagentEvent::ToolResult {
tool: tool_name.clone(),
args: args.clone(),
output: msg,
});
}
+2
View File
@@ -28,7 +28,9 @@ pub enum SubagentEvent {
},
ToolResult {
tool: String,
args: Value,
#[allow(dead_code)]
output: String,
},
Progress(String),
}
+131 -10
View File
@@ -79,6 +79,89 @@ impl WorkflowEngine {
/// display purposes (e.g. a hive-mind node's designation, `"Node-0-1"`).
pub type LiveStateFn = Arc<dyn Fn(String, String, AgentStatus) + Send + Sync>;
/// Spawn a single synchronous subagent with the given prompt, passing it
/// any findings from earlier sibling agents. Updates live state before and
/// after to reflect Running → Completed/Failed transitions.
///
/// Flow: push agent as `Running` → build `SubagentContext` with prompt +
/// findings preamble, linking the `workflow_findings` Arc so the subagent's
/// `note_finding` tool pushes into the same vec → call `run_subagent`
/// (draining the event channel into a consumer so events are not blocked)
/// → push `Completed` or `Failed`.
///
/// Why: the `workflow_findings` Arc is shared by all agents within the same
/// `execute_primitive` scope, so pipeline stages can pass data between each
/// other while different workflow invocations remain isolated.
///
/// When `timeout_ms` is `Some`, the subagent is killed (abandoned on a
/// separate thread) if it does not complete within the deadline, preventing
/// a stuck stage from blocking the entire pipeline forever.
///
/// Return: the agent's text output, or an error on failure.
fn format_tool_call_progress(prefix: &str, tool: &str, args: &serde_json::Value) -> String {
let details = match tool {
"read" | "view_file" | "write" | "write_to_file" | "edit" | "replace_file_content" | "multi_replace_file_content" | "delete" => {
args.get("path")
.or_else(|| args.get("TargetFile"))
.or_else(|| args.get("AbsolutePath"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
"grep" | "grep_search" => {
let pattern = args.get("pattern").or_else(|| args.get("Query")).and_then(|v| v.as_str()).unwrap_or("");
let path = args.get("path").or_else(|| args.get("SearchPath")).and_then(|v| v.as_str()).unwrap_or("");
if path.is_empty() {
format!("\"{pattern}\"")
} else {
format!("\"{pattern}\" in {path}")
}
}
"glob" => {
let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
if path.is_empty() {
pattern.to_string()
} else {
format!("{pattern} in {path}")
}
}
"bash" | "run_command" => {
let cmd = args.get("command").or_else(|| args.get("CommandLine")).and_then(|v| v.as_str()).unwrap_or("");
if cmd.len() > 60 {
format!("\"{}...\"", &cmd[..57])
} else {
format!("\"{cmd}\"")
}
}
"recall" => {
args.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string()
}
"remember" => {
args.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string()
}
"dir_list" | "list_dir" => {
args.get("DirectoryPath").or_else(|| args.get("path")).and_then(|v| v.as_str()).unwrap_or("").to_string()
}
_ => {
if args.is_object() && !args.as_object().unwrap().is_empty() {
args.as_object().unwrap().values()
.find_map(|v| v.as_str())
.unwrap_or("")
.to_string()
} else {
String::new()
}
}
};
if details.is_empty() {
format!("{prefix}: {tool}")
} else {
format!("{prefix}: {tool} {details}")
}
}
/// Spawn a single synchronous subagent with the given prompt, passing it
/// any findings from earlier sibling agents. Updates live state before and
/// after to reflect Running → Completed/Failed transitions.
@@ -178,10 +261,11 @@ fn spawn_single_agent(
let mut rx = rx;
while let Some(event) = rx.blocking_recv() {
match &event {
SubagentEvent::ToolCall { tool, .. } => {
SubagentEvent::ToolCall { tool, args } => {
tracing::debug!("[subagent] tool call: {}", tool);
// Push intra-division progress: which tool is running
if let Some(ref f) = drain_live {
let formatted = format_tool_call_progress("tool", tool, args);
f(
drain_agent_id.clone(),
drain_agent_name.clone(),
@@ -190,13 +274,56 @@ fn spawn_single_agent(
started_at: Some(drain_started_at),
completed_at: None,
error: None,
progress: Some(format!("tool: {tool}")),
progress: Some(formatted),
},
);
}
}
SubagentEvent::ToolResult { tool, .. } => {
SubagentEvent::ToolResult { tool, args, .. } => {
tracing::debug!("[subagent] tool result: {}", tool);
if let Some(ref f) = drain_live {
let formatted = format_tool_call_progress("done", tool, args);
f(
drain_agent_id.clone(),
drain_agent_name.clone(),
AgentStatus {
state: AgentState::Running,
started_at: Some(drain_started_at),
completed_at: None,
error: None,
progress: Some(formatted),
},
);
}
}
SubagentEvent::StepCompleted { output, .. } => {
// Show the agent's thinking/reasoning text as progress
// instead of just the tool name — first line, truncated.
if let Some(ref f) = drain_live {
let summary = output
.lines()
.next()
.unwrap_or(output)
.chars()
.take(80)
.collect::<String>();
f(
drain_agent_id.clone(),
drain_agent_name.clone(),
AgentStatus {
state: AgentState::Running,
started_at: Some(drain_started_at),
completed_at: None,
error: None,
progress: Some(summary),
},
);
}
}
SubagentEvent::StepFailed { step, error } => {
tracing::warn!("[subagent] step {} failed: {}", step, error);
}
SubagentEvent::Progress(prog) => {
if let Some(ref f) = drain_live {
f(
drain_agent_id.clone(),
@@ -206,17 +333,11 @@ fn spawn_single_agent(
started_at: Some(drain_started_at),
completed_at: None,
error: None,
progress: Some(format!("done: {tool}")),
progress: Some(prog.clone()),
},
);
}
}
SubagentEvent::StepCompleted { .. } => {
tracing::trace!("[subagent] step completed");
}
SubagentEvent::StepFailed { step, error } => {
tracing::warn!("[subagent] step {} failed: {}", step, error);
}
SubagentEvent::Completed { .. } => {
tracing::debug!("[subagent] completed");
}
+5 -1
View File
@@ -28,9 +28,13 @@ impl Tool for Delete {
"path": {
"type": "string",
"description": "Path to the file or directory to delete (relative to workspace root)"
},
"reason": {
"type": "string",
"description": "Reason for the deletion (must be non-empty, >= 8 chars)"
}
},
"required": ["path"]
"required": ["path", "reason"]
})
}
+5 -1
View File
@@ -30,9 +30,13 @@ impl Tool for GitOperator {
"type": "array",
"items": {"type": "string"},
"description": "Arguments for the git subcommand"
},
"reason": {
"type": "string",
"description": "Explain why this git operation is needed (>= 8 chars)"
}
},
"required": ["operation", "args"]
"required": ["operation", "args", "reason"]
})
}
+7 -7
View File
@@ -9,7 +9,7 @@
//! e.g. `"Node-0-1"`).
use ratatui::layout::Rect;
use ratatui::style::{Style, Modifier};
use ratatui::style::{Color, Style, Modifier};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::Frame;
@@ -141,10 +141,12 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st
Span::styled(err.clone(), Style::default().fg(Theme::ERROR)),
]));
} else if let Some(ref prog) = agent.status.progress {
card_lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(prog.clone(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)),
]));
for line in prog.lines().take(2) {
card_lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(line.to_string(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)),
]));
}
}
}
@@ -249,5 +251,3 @@ fn workflow_agent_line(agent: &WorkflowAgent) -> Line<'static> {
Span::styled(agent.name.clone(), Style::default().fg(Theme::TEXT)),
])
}
use ratatui::style::Color;