Agent loop state machine refactor, unified LLM interface, and UI improvements

- Rewrite agent loop as Planning→Executing(N)→Completed state machine with
  per-step context isolation to prevent token explosion
- Split tools and prompts by phase (planning vs execution)
- Add advance_step/save_memo tools for step transitions and cross-step memory
- Unify LLM interface: remove duplicate types, single chat_with_tools path
- Add UTF-8 safe truncation (truncate_str) to prevent panics on Chinese text
- Extract CreateForm component, add auto-scroll to execution log
- Add report generation with app access URL, non-blocking title generation
- Add timer system, file serving, app proxy, exec module
- Update Dockerfile with uv, deployment config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 22:35:33 +00:00
parent e2d5a6a7eb
commit 2df4e12d30
31 changed files with 3924 additions and 571 deletions

View File

@@ -11,6 +11,11 @@ use crate::AppState;
use crate::agent::AgentEvent;
use crate::db::{Workflow, PlanStep, Comment};
#[derive(serde::Serialize)]
struct ReportResponse {
report: String,
}
type ApiResult<T> = Result<Json<T>, Response>;
fn db_err(e: sqlx::Error) -> Response {
@@ -33,6 +38,7 @@ pub fn router(state: Arc<AppState>) -> Router {
.route("/projects/{id}/workflows", get(list_workflows).post(create_workflow))
.route("/workflows/{id}/steps", get(list_steps))
.route("/workflows/{id}/comments", get(list_comments).post(create_comment))
.route("/workflows/{id}/report", get(get_report))
.with_state(state)
}
@@ -134,3 +140,22 @@ async fn create_comment(
Ok(Json(comment))
}
async fn get_report(
State(state): State<Arc<AppState>>,
Path(workflow_id): Path<String>,
) -> Result<Json<ReportResponse>, Response> {
let wf = sqlx::query_as::<_, Workflow>(
"SELECT * FROM workflows WHERE id = ?"
)
.bind(&workflow_id)
.fetch_optional(&state.db.pool)
.await
.map_err(db_err)?;
match wf {
Some(w) if !w.report.is_empty() => Ok(Json(ReportResponse { report: w.report })),
Some(_) => Err((StatusCode::NOT_FOUND, "Report not yet generated").into_response()),
None => Err((StatusCode::NOT_FOUND, "Workflow not found").into_response()),
}
}