feat(tui): introduce comprehensive state management for TUI interface
- Add AppStateRest as the central state struct for managing TUI state. - Implement InputState for handling user input, autocomplete, and history. - Create MiscState to manage overlays, notifications, and editor state. - Introduce ScrollState for viewport scrolling functionality. - Develop TranscriptCache for efficient message rendering in the chat pane. - Implement SimpleAgent and SimpleWorkflowEngine for agent lifecycle management. - Add helper functions for managing effort levels and token counting. - Organize state-related modules for better maintainability and clarity.
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
//! Flow: construct system message → call LLM → parse tool calls → execute
|
||||
//! tools → continue until the model returns a final text response (no more
|
||||
//! tool calls) or the iteration limit is reached.
|
||||
//!
|
||||
//! Progress reporting: when a `TurnEvent` queue is available via the
|
||||
//! `ToolCtx`, the engine emits `AgentProgress` events so the TUI can show
|
||||
//! which tool the subagent is currently executing.
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::{debug, info, instrument};
|
||||
@@ -11,17 +15,44 @@ use crate::llm::provider::LlmClient;
|
||||
use crate::subagent::context::SubagentContext;
|
||||
use crate::subagent::division::{tools_for, AccessTier};
|
||||
use crate::tools::{tool_defs, ToolCtx};
|
||||
use zesdex_domain::agent::progress::AgentProgress;
|
||||
use zesdex_domain::core::tool_call::sanitize_tool_arguments;
|
||||
use zesdex_domain::core::ChatMessage;
|
||||
use zesdex_domain::subagent_directive;
|
||||
|
||||
/// Maximum number of tool-call iterations before the engine gives up.
|
||||
const MAX_ITERATIONS: u32 = 25;
|
||||
|
||||
/// Emit an `AgentProgress` event onto the turn-event queue, if one is
|
||||
/// configured in the `ToolCtx`.
|
||||
fn report_progress(tool_ctx: &ToolCtx, progress: AgentProgress) {
|
||||
if let Some(ref queue) = tool_ctx.turn_events {
|
||||
if let Ok(mut q) = queue.lock() {
|
||||
q.push_back(zesdex_domain::agent::TurnEvent::AgentProgress(progress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the system message for a subagent, including current working
|
||||
/// directory and workspace root information.
|
||||
fn build_system_message(directive: &str, tool_ctx: &ToolCtx) -> ChatMessage {
|
||||
let cwd = std::env::current_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
let ws_root = tool_ctx
|
||||
.workspaces
|
||||
.first()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
|
||||
ChatMessage::system(subagent_directive(directive, &cwd, &ws_root))
|
||||
}
|
||||
|
||||
/// Run an agent with a directive, access tier, and tool context.
|
||||
///
|
||||
/// Flow:
|
||||
/// 1. Resolve allowed tools for the given `access` tier.
|
||||
/// 2. Build a system prompt from the directive.
|
||||
/// 2. Build a system prompt from the directive using the domain prompt module.
|
||||
/// 3. Loop (up to `MAX_ITERATIONS`):
|
||||
/// a. Call the LLM (non-streaming) with accumulated messages + tool defs.
|
||||
/// b. If the response has no tool calls → return the text content.
|
||||
@@ -29,6 +60,9 @@ const MAX_ITERATIONS: u32 = 25;
|
||||
/// tool-role message.
|
||||
/// d. If the response also contained text, append an assistant message.
|
||||
/// 4. If the loop exits naturally, return the iteration-limit message.
|
||||
///
|
||||
/// Progress: each tool invocation is reported via `AgentProgress` if a
|
||||
/// turn-event queue is available in the `ToolCtx`.
|
||||
#[instrument(skip(ctx, tool_ctx))]
|
||||
pub async fn run_agent(
|
||||
ctx: SubagentContext,
|
||||
@@ -41,23 +75,9 @@ pub async fn run_agent(
|
||||
let tools = tools_for(&access);
|
||||
let defs = tool_defs(&tools);
|
||||
|
||||
let cwd = std::env::current_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
let ws_root = tool_ctx
|
||||
.workspaces
|
||||
.first()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
let sys_msg = build_system_message(directive, &tool_ctx);
|
||||
|
||||
let mut messages = vec![ChatMessage::system(format!(
|
||||
"You are a focused subagent.\n\n\
|
||||
Current directory (PWD): {cwd}\n\
|
||||
Workspace root: {ws_root}\n\n\
|
||||
Your directive:\n{directive}\n\n\
|
||||
Complete the directive autonomously using the tools available to you. \
|
||||
Return your final answer when done."
|
||||
))];
|
||||
let mut messages = vec![sys_msg];
|
||||
|
||||
let client = LlmClient::new(
|
||||
ctx.api_key.clone(),
|
||||
@@ -68,12 +88,9 @@ pub async fn run_agent(
|
||||
// Limited iteration loop so we don't run forever
|
||||
for iteration in 0..MAX_ITERATIONS {
|
||||
use zesdex_application::ports::ProviderService;
|
||||
let (response_msg, _usage) = client.chat(
|
||||
&messages,
|
||||
Some(defs.clone()),
|
||||
Some(4096),
|
||||
None,
|
||||
).await?;
|
||||
let (response_msg, _usage) = client
|
||||
.chat(&messages, Some(defs.clone()), Some(4096), None)
|
||||
.await?;
|
||||
|
||||
let content = response_msg.content.clone().unwrap_or_default();
|
||||
let tool_calls = response_msg.tool_calls.unwrap_or_default();
|
||||
@@ -81,6 +98,10 @@ pub async fn run_agent(
|
||||
// If no tool calls, we're done — return content
|
||||
if tool_calls.is_empty() {
|
||||
info!("Subagent completed after {iteration} iterations");
|
||||
report_progress(
|
||||
&tool_ctx,
|
||||
AgentProgress::completed("subagent", directive),
|
||||
);
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
@@ -91,14 +112,24 @@ pub async fn run_agent(
|
||||
|
||||
debug!("Subagent executing tool: {tool_name}");
|
||||
|
||||
let result = if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) {
|
||||
match tool.run(&tool_ctx, &args) {
|
||||
Ok(output) => output,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
} else {
|
||||
format!("Unknown tool: {tool_name}")
|
||||
};
|
||||
report_progress(
|
||||
&tool_ctx,
|
||||
AgentProgress::running(
|
||||
"subagent",
|
||||
format!("{}:{}", directive, tool_name),
|
||||
Some(tool_name.clone()),
|
||||
),
|
||||
);
|
||||
|
||||
let result =
|
||||
if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) {
|
||||
match tool.run(&tool_ctx, &args) {
|
||||
Ok(output) => output,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
} else {
|
||||
format!("Unknown tool: {tool_name}")
|
||||
};
|
||||
|
||||
messages.push(ChatMessage::tool(tc.id.clone(), result));
|
||||
}
|
||||
@@ -109,5 +140,14 @@ pub async fn run_agent(
|
||||
}
|
||||
}
|
||||
|
||||
Ok("Subagent reached iteration limit".to_string())
|
||||
info!("Subagent reached iteration limit ({MAX_ITERATIONS})");
|
||||
report_progress(
|
||||
&tool_ctx,
|
||||
AgentProgress::failed(
|
||||
"subagent",
|
||||
directive,
|
||||
format!("iteration limit ({MAX_ITERATIONS})"),
|
||||
),
|
||||
);
|
||||
Ok(format!("Subagent reached iteration limit ({MAX_ITERATIONS})"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user