Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6d6deeab | ||
|
|
c5253b2ca3 | ||
|
|
a8adfcbf6d | ||
|
|
558908aef2 | ||
|
|
472c597c5e | ||
|
|
97aa75f2da | ||
|
|
dfceb8acac | ||
|
|
c6ab063c21 |
@@ -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)
|
# [1.7.0](https://github.com/asepharyana/zesdex/compare/v1.6.0...v1.7.0) (2026-07-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -4436,7 +4436,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zesdex"
|
name = "zesdex"
|
||||||
version = "1.7.0"
|
version = "1.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zesdex"
|
name = "zesdex"
|
||||||
version = "1.7.0"
|
version = "1.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
authors = ["asepharyana <superaseph@gmail.com>"]
|
||||||
|
|
||||||
|
|||||||
+17
-14
@@ -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.
|
independent verification, use workflow_run to orchestrate sub-agents.
|
||||||
|
|
||||||
Core tools:
|
Core tools:
|
||||||
- read(path) — Read file contents. Use when you need to inspect code.
|
- read(path, limit?) — Read file contents. Use when you need to inspect code.
|
||||||
- grep(pattern, path?) — Search for a pattern in files.
|
- grep(pattern, path) — Search for a pattern in files.
|
||||||
- glob(pattern) — List files matching a glob pattern.
|
- glob(pattern, path) — List files matching a glob pattern in a directory.
|
||||||
- write(path, content, reason) — Write content to a file. Reason is required.
|
- 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.
|
- edit(path, old, new, replace_all?, reason) — Replace text in a file. Reason is required (>= 8 chars).
|
||||||
- delete(path) — Delete a file or empty directory.
|
- delete(path, reason) — Delete a file or empty directory. Reason is required (>= 8 chars).
|
||||||
- bash(command) — Run a shell command. Use for builds, tests, git ops.
|
- bash(command, description?, timeout?, run_in_background?) — Run a shell command.
|
||||||
- bash_output(job_id) — Poll output of a background bash job.
|
- bash_output(job_id) — Poll output of a background bash job.
|
||||||
- bash_kill(job_id) — Kill a background bash job.
|
- bash_kill(job_id) — Kill a background bash job.
|
||||||
- cd(path) — Change working directory.
|
- cd(path) — Change working directory.
|
||||||
- dir_list(path) — List directory contents.
|
- 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.
|
- pong(message?) — Simple connectivity check. Echoes back the message.
|
||||||
|
|
||||||
Git tools:
|
Git tools:
|
||||||
- git_operator(args, confirm_destructive?) — Run git commands. Some destructive
|
- git_operator(operation, args, reason) — Run git commands (e.g. add, commit, status,
|
||||||
operations (force-push, reset --hard, branch -D) require confirm_destructive=true.
|
diff, log). Reason explaining the operation is required (>= 8 chars). Destructive
|
||||||
- git_worktree(args) — Manage git worktrees.
|
operations (force-push, reset --hard, branch -D) are blocked by the shell filter.
|
||||||
- git_cred(operation) — Manage git credentials.
|
- 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:
|
Memory & Planning:
|
||||||
@@ -86,5 +88,6 @@ Language Server Protocol (LSP) tools:
|
|||||||
LSP auto-provisioning runs at startup for Rust (rust-analyzer), TypeScript
|
LSP auto-provisioning runs at startup for Rust (rust-analyzer), TypeScript
|
||||||
(typescript-language-server), Go (gopls), and Java (jdtls).
|
(typescript-language-server), Go (gopls), and Java (jdtls).
|
||||||
|
|
||||||
Each write/edit call MUST include a non-empty reason argument explaining
|
Each write/edit/delete/git_operator call MUST include a non-empty reason
|
||||||
why the change is being made. This is enforced deterministically.
|
argument (>= 8 chars) explaining why the operation is being made. This is
|
||||||
|
enforced deterministically.
|
||||||
@@ -368,6 +368,7 @@ pub fn trigger_review(state: &mut AppStateRest) {
|
|||||||
SubagentEvent::StepFailed { step, error } => {
|
SubagentEvent::StepFailed { step, error } => {
|
||||||
tracing::warn!("[review] step {} failed: {}", step, error);
|
tracing::warn!("[review] step {} failed: {}", step, error);
|
||||||
}
|
}
|
||||||
|
SubagentEvent::Progress(_) => {}
|
||||||
SubagentEvent::Completed { .. } => {
|
SubagentEvent::Completed { .. } => {
|
||||||
tracing::debug!("[review] completed");
|
tracing::debug!("[review] completed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1085,12 +1085,15 @@ fn run_agent_turn(
|
|||||||
let system_msg = ChatMessage::system(
|
let system_msg = ChatMessage::system(
|
||||||
"You are the Core Intelligence of the Hive, compiling a cognitive cycle plan for \
|
"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 \
|
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 \
|
to do) and an access tier. You MUST organize the plan into a strict progressive sequence of phases:\n\n\
|
||||||
needed. Simple tasks might need one cycle with one node; large tasks might need \
|
1. EXPLORE PHASE (Cycle 0 - MANDATORY):\n\
|
||||||
several cycles with multiple nodes each. Cycles run sequentially; every node's \
|
- Must only contain read-only drones (access: \"read\").\n\
|
||||||
complete output merges into the collective state the instant it finishes, \
|
- Directives must focus on codebase investigation, searching patterns, reading configuration/source files, and diagnosing issues.\n\n\
|
||||||
automatically visible to all later cycles. Nodes within a cycle run in parallel. \
|
2. PLANNING PHASE (Cycle 1 - MANDATORY):\n\
|
||||||
The Hive does not fracture. The Hive executes. Do not explain. Return ONLY raw \
|
- 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."
|
JSON matching the requested structure."
|
||||||
);
|
);
|
||||||
let user_msg = ChatMessage::user(format!(
|
let user_msg = ChatMessage::user(format!(
|
||||||
@@ -1100,14 +1103,17 @@ fn run_agent_turn(
|
|||||||
{{\n\
|
{{\n\
|
||||||
\x20 \"cycles\": [\n\
|
\x20 \"cycles\": [\n\
|
||||||
\x20 [\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\
|
||||||
\x20 ]\n\
|
\x20 ]\n\
|
||||||
}}\n\n\
|
}}\n\n\
|
||||||
access: 'read' = investigation only, 'write' = read + edit/write/bash, \
|
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)."
|
||||||
'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."
|
|
||||||
));
|
));
|
||||||
|
|
||||||
let planner_result = tc.client.chat_with_tools_non_streaming(&[system_msg, user_msg], None);
|
let planner_result = tc.client.chat_with_tools_non_streaming(&[system_msg, user_msg], None);
|
||||||
|
|||||||
@@ -296,6 +296,17 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
|
|||||||
out
|
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
|
/// Synchronous subagent entry point: run up to `ctx.max_steps` iterations
|
||||||
/// of the LLM tool loop.
|
/// 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}");
|
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,
|
// Use streaming API so the abort flag is checked per SSE event,
|
||||||
// making the subagent responsive to cancellation even during an
|
// making the subagent responsive to cancellation even during an
|
||||||
// LLM call (non-streaming would block for 10-30s unchecked).
|
// 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(),
|
tdefs_opt.clone(),
|
||||||
Some(0.7),
|
Some(0.7),
|
||||||
Some(4096),
|
Some(4096),
|
||||||
|_event| -> bool {
|
|event| -> bool {
|
||||||
// Check abort on every SSE event for responsive cancellation.
|
// 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)) {
|
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
|
||||||
return false; // signals provider to abort
|
return false; // signals provider to abort
|
||||||
}
|
}
|
||||||
// We don't stream tokens to the UI for subagents — just
|
match event {
|
||||||
// need the assembled message at the end.
|
crate::app::runtime::stream::StreamEvent::Reasoning(text) => {
|
||||||
|
current_thinking.push_str(text);
|
||||||
|
let prog = format_subagent_progress("thinking", ¤t_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", ¤t_token);
|
||||||
|
let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -416,6 +442,15 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
|||||||
|
|
||||||
let content = response.content.clone().unwrap_or_default();
|
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 {
|
if has_tool_calls {
|
||||||
let tool_calls = response.tool_calls.clone().unwrap_or_default();
|
let tool_calls = response.tool_calls.clone().unwrap_or_default();
|
||||||
// Push the assistant message with tool_calls into the conversation
|
// 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()));
|
messages.push(ChatMessage::tool_result(tool_call.id.clone(), output_text.clone()));
|
||||||
let _ = tx.blocking_send(SubagentEvent::ToolResult {
|
let _ = tx.blocking_send(SubagentEvent::ToolResult {
|
||||||
tool: tool_name.clone(),
|
tool: tool_name.clone(),
|
||||||
|
args: args.clone(),
|
||||||
output: output_text,
|
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()));
|
messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone()));
|
||||||
let _ = tx.blocking_send(SubagentEvent::ToolResult {
|
let _ = tx.blocking_send(SubagentEvent::ToolResult {
|
||||||
tool: tool_name.clone(),
|
tool: tool_name.clone(),
|
||||||
|
args: args.clone(),
|
||||||
output: msg,
|
output: msg,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ pub enum SubagentEvent {
|
|||||||
},
|
},
|
||||||
ToolResult {
|
ToolResult {
|
||||||
tool: String,
|
tool: String,
|
||||||
|
args: Value,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
output: String,
|
output: String,
|
||||||
},
|
},
|
||||||
|
Progress(String),
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-10
@@ -79,6 +79,89 @@ impl WorkflowEngine {
|
|||||||
/// display purposes (e.g. a hive-mind node's designation, `"Node-0-1"`).
|
/// display purposes (e.g. a hive-mind node's designation, `"Node-0-1"`).
|
||||||
pub type LiveStateFn = Arc<dyn Fn(String, String, AgentStatus) + Send + Sync>;
|
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
|
/// Spawn a single synchronous subagent with the given prompt, passing it
|
||||||
/// any findings from earlier sibling agents. Updates live state before and
|
/// any findings from earlier sibling agents. Updates live state before and
|
||||||
/// after to reflect Running → Completed/Failed transitions.
|
/// after to reflect Running → Completed/Failed transitions.
|
||||||
@@ -178,10 +261,11 @@ fn spawn_single_agent(
|
|||||||
let mut rx = rx;
|
let mut rx = rx;
|
||||||
while let Some(event) = rx.blocking_recv() {
|
while let Some(event) = rx.blocking_recv() {
|
||||||
match &event {
|
match &event {
|
||||||
SubagentEvent::ToolCall { tool, .. } => {
|
SubagentEvent::ToolCall { tool, args } => {
|
||||||
tracing::debug!("[subagent] tool call: {}", tool);
|
tracing::debug!("[subagent] tool call: {}", tool);
|
||||||
// Push intra-division progress: which tool is running
|
// Push intra-division progress: which tool is running
|
||||||
if let Some(ref f) = drain_live {
|
if let Some(ref f) = drain_live {
|
||||||
|
let formatted = format_tool_call_progress("tool", tool, args);
|
||||||
f(
|
f(
|
||||||
drain_agent_id.clone(),
|
drain_agent_id.clone(),
|
||||||
drain_agent_name.clone(),
|
drain_agent_name.clone(),
|
||||||
@@ -190,13 +274,56 @@ fn spawn_single_agent(
|
|||||||
started_at: Some(drain_started_at),
|
started_at: Some(drain_started_at),
|
||||||
completed_at: None,
|
completed_at: None,
|
||||||
error: None,
|
error: None,
|
||||||
progress: Some(format!("tool: {tool}")),
|
progress: Some(formatted),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SubagentEvent::ToolResult { tool, .. } => {
|
SubagentEvent::ToolResult { tool, args, .. } => {
|
||||||
tracing::debug!("[subagent] tool result: {}", tool);
|
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 {
|
if let Some(ref f) = drain_live {
|
||||||
f(
|
f(
|
||||||
drain_agent_id.clone(),
|
drain_agent_id.clone(),
|
||||||
@@ -206,17 +333,11 @@ fn spawn_single_agent(
|
|||||||
started_at: Some(drain_started_at),
|
started_at: Some(drain_started_at),
|
||||||
completed_at: None,
|
completed_at: None,
|
||||||
error: 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 { .. } => {
|
SubagentEvent::Completed { .. } => {
|
||||||
tracing::debug!("[subagent] completed");
|
tracing::debug!("[subagent] completed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ impl Tool for Delete {
|
|||||||
"path": {
|
"path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to the file or directory to delete (relative to workspace root)"
|
"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"]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ impl Tool for GitOperator {
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"description": "Arguments for the git subcommand"
|
"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"]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
//! e.g. `"Node-0-1"`).
|
//! e.g. `"Node-0-1"`).
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Style, Modifier};
|
use ratatui::style::{Color, Style, Modifier};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||||
use ratatui::Frame;
|
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)),
|
Span::styled(err.clone(), Style::default().fg(Theme::ERROR)),
|
||||||
]));
|
]));
|
||||||
} else if let Some(ref prog) = agent.status.progress {
|
} else if let Some(ref prog) = agent.status.progress {
|
||||||
card_lines.push(Line::from(vec![
|
for line in prog.lines().take(2) {
|
||||||
Span::styled(" ", Style::default()),
|
card_lines.push(Line::from(vec![
|
||||||
Span::styled(prog.clone(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)),
|
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)),
|
Span::styled(agent.name.clone(), Style::default().fg(Theme::TEXT)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
use ratatui::style::Color;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user