diff --git a/chat.el b/chat.el index e76e245..6897a20 100644 --- a/chat.el +++ b/chat.el @@ -2138,6 +2138,18 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: let pend_tool_name: String = "" let pend_tool_input: String = "" let pend_tool_tier: String = "" + let pend_narration: String = "" + + // Live run-progress ledger (2026-07-13, proposed with the narrated-runs work): + // the model already narrates its intent in a text block before every tool call, + // and the loop previously DISCARDED that prose on tool rounds. Each iteration now + // appends {"i":N,"t":"","tool":""} to state key + // run_progress_; the client polls GET /api/run-progress/ + // during a run to render live step updates (the Cowork pattern) without needing + // streaming. Reset at loop start; a {"done":true} entry lands on completion. + if !str_eq(session_id, "") { + state_set("run_progress_" + session_id, "") + } while keep_going && iteration < 8 { let req_body: String = "{\"model\":\"" + model + "\"" @@ -2231,12 +2243,27 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: "[" + inner2 + ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}]" } else { messages } + // Live progress ledger: one entry per round — the model's own narration + // (its pre-tool prose, previously discarded here) plus the tool it reached + // for. Clients poll /api/run-progress/ to render these live. + if !str_eq(session_id, "") { + let prog_key: String = "run_progress_" + session_id + let prog_prev: String = state_get(prog_key) + let prog_snip: String = if str_len(text_out) > 280 { str_slice(text_out, 0, 280) } else { text_out } + let prog_entry: String = "{\"i\":" + int_to_str(iteration) + + ",\"t\":\"" + json_safe(prog_snip) + "\"" + + ",\"tool\":\"" + json_safe(tool_name) + "\"}" + let prog_next: String = if str_eq(prog_prev, "") { prog_entry } else { prog_prev + "," + prog_entry } + state_set(prog_key, prog_next) + } + // Bridge turn: persist the continuation and stop the loop. let pending = if needs_bridge { true } else { pending } let pend_tool_id = if needs_bridge { tool_id } else { pend_tool_id } let pend_tool_name = if needs_bridge { tool_name } else { pend_tool_name } let pend_tool_input = if needs_bridge { tool_input } else { pend_tool_input } let pend_tool_tier = if needs_bridge { risk_tier } else { pend_tool_tier } + let pend_narration = if needs_bridge { text_out } else { pend_narration } // Stash messages-with-the-assistant-request so resume only needs to append the // client's tool_result block. messages_with_assistant is only meaningful when a // tool was requested, so guard on needs_bridge before persisting. @@ -2258,6 +2285,7 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: + ",\"tool_name\":\"" + pend_tool_name + "\"" + ",\"tool_input\":" + safe_in + ",\"risk_tier\":\"" + pend_tool_tier + "\"" + + ",\"narration\":\"" + json_safe(pend_narration) + "\"" + ",\"model\":\"" + model + "\"" + ",\"agentic\":true" + ",\"tools_used\":" + tools_arr + "}" @@ -2279,6 +2307,13 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: let safe_text: String = json_safe(final_text) let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" } + // Close the live-progress ledger: pollers see {"done":true} and stop. + if !str_eq(session_id, "") { + let done_key: String = "run_progress_" + session_id + let done_prev: String = state_get(done_key) + let done_next: String = if str_eq(done_prev, "") { "{\"done\":true}" } else { done_prev + ",{\"done\":true}" } + state_set(done_key, done_next) + } return "{\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":" + tools_arr + ",\"iterations\":" + int_to_str(iteration) + "}" } diff --git a/routes.el b/routes.el index ec92061..0f76980 100644 --- a/routes.el +++ b/routes.el @@ -491,6 +491,18 @@ fn handle_request(method: String, path: String, body: String) -> String { if str_starts_with(clean, "/api/connectors") { return handle_connectors(method, clean, body) } + // GET /api/run-progress/:session_id — live agentic-run ledger (2026-07-13, + // narrated-runs). agentic_loop appends one {"i","t","tool"} entry per round + // (the model's own pre-tool narration); a {"done":true} entry closes the run. + // Clients poll this during a run to render live step updates without streaming. + if str_starts_with(clean, "/api/run-progress/") { + let rp_id: String = str_slice(clean, 18, str_len(clean)) + if !str_eq(rp_id, "") { + let rp_raw: String = state_get("run_progress_" + rp_id) + let rp_arr: String = if str_eq(rp_raw, "") { "[]" } else { "[" + rp_raw + "]" } + return "{\"progress\":" + rp_arr + "}" + } + } // GET /api/sessions — list all sessions if str_eq(clean, "/api/sessions") { return session_list() diff --git a/studio.el b/studio.el index b5b7ece..dfc40f0 100644 --- a/studio.el +++ b/studio.el @@ -46,7 +46,9 @@ fn handle_config(method: String, body: String) -> String { } } let current_model: String = state_get("soul_model") - let display: String = if str_eq(current_model, "") { "claude-sonnet-4-5" } else { current_model } + // Display fallback aligned with the intended product default (was claude-sonnet-4-5, + // which silently became the app's picker default on fresh profiles — 2026-07-13). + let display: String = if str_eq(current_model, "") { "claude-opus-4-8" } else { current_model } return "{\"model\":\"" + display + "\",\"ok\":true}" }