LLM call logging, plan persistence API, quote-to-feedback UX, requirement input improvements

- Add llm_call_log table and per-call timing/token tracking in agent loop
- New GET /workflows/{id}/plan endpoint to restore plan from snapshots on page load
- New GET /workflows/{id}/llm-calls endpoint + WS LlmCallLog broadcast
- Parse Usage from LLM API response (prompt_tokens, completion_tokens)
- Detailed mode toggle in execution log showing LLM call cards with phase/tokens/latency
- Quote-to-feedback: hover quote buttons on plan steps and log entries, multi-quote chips in comment input
- Requirement input: larger textarea, multi-line display with pre-wrap and scroll

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 09:16:51 +00:00
parent 46424cfbc4
commit 0a8eee0285
14 changed files with 601 additions and 26 deletions

View File

@@ -8,8 +8,9 @@ use axum::{
};
use serde::Deserialize;
use crate::AppState;
use crate::agent::AgentEvent;
use crate::db::{Workflow, ExecutionLogEntry, Comment};
use crate::agent::{AgentEvent, PlanStepInfo};
use crate::db::{Workflow, ExecutionLogEntry, Comment, LlmCallLogEntry};
use crate::state::AgentState;
use super::{ApiResult, db_err};
#[derive(serde::Serialize)]
@@ -33,6 +34,8 @@ pub fn router(state: Arc<AppState>) -> Router {
.route("/workflows/{id}/steps", get(list_steps))
.route("/workflows/{id}/comments", get(list_comments).post(create_comment))
.route("/workflows/{id}/report", get(get_report))
.route("/workflows/{id}/plan", get(get_plan))
.route("/workflows/{id}/llm-calls", get(list_llm_calls))
.with_state(state)
}
@@ -153,3 +156,38 @@ async fn get_report(
None => Err((StatusCode::NOT_FOUND, "Workflow not found").into_response()),
}
}
async fn get_plan(
State(state): State<Arc<AppState>>,
Path(workflow_id): Path<String>,
) -> ApiResult<Vec<PlanStepInfo>> {
let snapshot_json: Option<String> = sqlx::query_scalar(
"SELECT state_json FROM agent_state_snapshots WHERE workflow_id = ? ORDER BY created_at DESC LIMIT 1"
)
.bind(&workflow_id)
.fetch_optional(&state.db.pool)
.await
.map_err(db_err)?;
if let Some(json) = snapshot_json {
if let Ok(agent_state) = serde_json::from_str::<AgentState>(&json) {
return Ok(Json(crate::agent::plan_infos_from_state(&agent_state)));
}
}
Ok(Json(vec![]))
}
async fn list_llm_calls(
State(state): State<Arc<AppState>>,
Path(workflow_id): Path<String>,
) -> ApiResult<Vec<LlmCallLogEntry>> {
sqlx::query_as::<_, LlmCallLogEntry>(
"SELECT * FROM llm_call_log WHERE workflow_id = ? ORDER BY created_at"
)
.bind(&workflow_id)
.fetch_all(&state.db.pool)
.await
.map(Json)
.map_err(db_err)
}