import "memory.el" import "chat.el" // sessions.el — Persistent conversation session management. // // Sessions are Engram nodes with: // node_type = "Conversation" // label = "session:meta" // content = JSON: {id, title, created_at, updated_at} // // Message history is kept in state under "session_hist_SESSION_ID" // and also persisted to Engram as nodes with label "session:messages:SESSION_ID". // session_title_from_message — derive a session title from the first user message. // Takes up to 60 characters; falls back to "New conversation". fn session_title_from_message(message: String) -> String { if str_eq(message, "") { return "New conversation" } let trimmed: String = str_trim(message) if str_len(trimmed) <= 60 { return trimmed } return str_slice(trimmed, 0, 60) } // session_make_content — build the JSON blob stored as session:meta node content. // IMPORTANT: "type":"session:meta" must appear in the content so engram_search_json // can find these nodes by text search. Do not remove it. fn session_make_content(id: String, title: String, created_at: Int, updated_at: Int, folder: String) -> String { let safe_title: String = json_safe(title) let safe_folder: String = json_safe(folder) return "{\"type\":\"session:meta\"" + ",\"id\":\"" + id + "\"" + ",\"title\":\"" + safe_title + "\"" + ",\"folder\":\"" + safe_folder + "\"" + ",\"created_at\":" + int_to_str(created_at) + ",\"updated_at\":" + int_to_str(updated_at) + "}" } // session_create — create a new session, return {id, title, created_at}. fn session_create(body: String) -> String { let ts: Int = time_now() let id: String = uuid_v4() let title_req: String = json_get(body, "title") let title: String = if str_eq(title_req, "") { "New conversation" } else { title_req } let folder: String = json_get(body, "folder") let content: String = session_make_content(id, title, ts, ts, folder) let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]" let node_id: String = engram_node_full( content, "Conversation", "session:meta", el_from_float(0.7), el_from_float(0.7), el_from_float(0.9), "Episodic", tags ) if str_eq(node_id, "") { return "{\"error\":\"failed to create session\"}" } // Store the engram node_id mapping so we can look up the node for this session state_set("session_node_" + id, node_id) // Maintain a state-based index for fast listing within this daemon run. // Newest sessions first (prepend). let existing_idx: String = state_get("session_index") let idx_entry: String = "{\"id\":\"" + id + "\",\"title\":\"" + json_safe(title) + "\",\"folder\":\"" + json_safe(folder) + "\",\"created_at\":" + int_to_str(ts) + ",\"updated_at\":" + int_to_str(ts) + ",\"last_message\":\"\"}" let new_idx: String = if str_eq(existing_idx, "") { "[" + idx_entry + "]" } else { let inner: String = str_slice(existing_idx, 1, str_len(existing_idx) - 1) "[" + idx_entry + "," + inner + "]" } state_set("session_index", new_idx) return "{\"id\":\"" + id + "\"" + ",\"title\":\"" + json_safe(title) + "\"" + ",\"folder\":\"" + json_safe(folder) + "\"" + ",\"node_id\":\"" + node_id + "\"" + ",\"created_at\":" + int_to_str(ts) + "}" } // session_list — list all sessions. Returns [{id, title, last_message, created_at, updated_at}]. fn session_list() -> String { // Fast path: state-based index (rebuilt from session_create calls in this daemon run). let state_idx: String = state_get("session_index") if !str_eq(state_idx, "") && !str_eq(state_idx, "[]") { return state_idx } // Slow path: engram search (works across restarts for new-format nodes). let results: String = engram_search_json("session:meta", 50) if str_eq(results, "") { return "[]" } if str_eq(results, "[]") { return "[]" } // Filter to only session:meta nodes; build output array let total: Int = json_array_len(results) let out: String = "" let i: Int = 0 while i < total { let node: String = json_array_get(results, i) let label: String = json_get(node, "label") let node_type: String = json_get(node, "node_type") let is_session: Bool = str_eq(label, "session:meta") && str_eq(node_type, "Conversation") let content: String = json_get(node, "content") let sess_id: String = json_get(content, "id") // Use the nested content JSON fields let eff_id: String = if str_eq(sess_id, "") { json_get(node, "id") } else { sess_id } let title_inner: String = json_get(content, "title") let eff_title: String = if str_eq(title_inner, "") { "New conversation" } else { title_inner } let folder_inner: String = json_get(content, "folder") let created_inner: String = json_get(content, "created_at") let updated_inner: String = json_get(content, "updated_at") let eff_created: String = if str_eq(created_inner, "") { "0" } else { created_inner } let eff_updated: String = if str_eq(updated_inner, "") { eff_created } else { updated_inner } let entry: String = if is_session { "{\"id\":\"" + json_safe(eff_id) + "\"" + ",\"title\":\"" + json_safe(eff_title) + "\"" + ",\"folder\":\"" + json_safe(folder_inner) + "\"" + ",\"last_message\":\"\"" + ",\"created_at\":" + eff_created + ",\"updated_at\":" + eff_updated + "}" } else { "" } let out = if !str_eq(entry, "") { if str_eq(out, "") { entry } else { out + "," + entry } } else { out } let i = i + 1 } return "[" + out + "]" } // session_get — get a session's metadata + message history. // Returns {id, title, created_at, updated_at, messages: [{role, content, timestamp}]} fn session_get(session_id: String) -> String { if str_eq(session_id, "") { return "{\"error\":\"session_id is required\"}" } // Load session meta from engram let results: String = engram_search_json("session:meta " + session_id, 10) let meta_content: String = "" let meta_title: String = "New conversation" let meta_folder: String = "" let meta_created: String = "0" let meta_updated: String = "0" let found: Bool = false let total: Int = if str_eq(results, "") { 0 } else { json_array_len(results) } let i: Int = 0 while i < total { let node: String = json_array_get(results, i) let label: String = json_get(node, "label") let content: String = json_get(node, "content") let sid: String = json_get(content, "id") let is_match: Bool = str_eq(label, "session:meta") && str_eq(sid, session_id) && !found let found = if is_match { true } else { found } let meta_title = if is_match { json_get(content, "title") } else { meta_title } let meta_folder = if is_match { json_get(content, "folder") } else { meta_folder } let meta_created_raw: String = json_get(content, "created_at") let meta_created = if is_match && !str_eq(meta_created_raw, "") { meta_created_raw } else { meta_created } let meta_updated_raw: String = json_get(content, "updated_at") let meta_updated = if is_match && !str_eq(meta_updated_raw, "") { meta_updated_raw } else { meta_updated } let i = i + 1 } // Load message history from state (primary) or engram (fallback) let state_hist: String = state_get("session_hist_" + session_id) let hist_raw: String = if str_eq(state_hist, "") { // Try loading from engram let engram_hist: String = engram_search_json("session:messages:" + session_id, 3) if str_eq(engram_hist, "") { "[]" } else { if str_eq(engram_hist, "[]") { "[]" } else { let h_node: String = json_array_get(engram_hist, 0) let h_content: String = json_get(h_node, "content") if str_starts_with(h_content, "[") { h_content } else { "[]" } } } } else { state_hist } let safe_title: String = json_safe(meta_title) return "{\"id\":\"" + session_id + "\"" + ",\"title\":\"" + safe_title + "\"" + ",\"folder\":\"" + json_safe(meta_folder) + "\"" + ",\"created_at\":" + meta_created + ",\"updated_at\":" + meta_updated + ",\"messages\":" + hist_raw + "}" } // session_delete — delete a session and its history nodes from engram. fn session_delete(session_id: String) -> String { if str_eq(session_id, "") { return "{\"error\":\"session_id is required\"}" } // Find and delete session:meta node let results: String = engram_search_json("session:meta " + session_id, 10) let total: Int = if str_eq(results, "") { 0 } else { json_array_len(results) } let deleted_meta: Int = 0 let i: Int = 0 while i < total { let node: String = json_array_get(results, i) let label: String = json_get(node, "label") let content: String = json_get(node, "content") let sid: String = json_get(content, "id") let is_match: Bool = str_eq(label, "session:meta") && str_eq(sid, session_id) let node_id: String = json_get(node, "id") let deleted_meta = if is_match && !str_eq(node_id, "") { engram_forget(node_id) deleted_meta + 1 } else { deleted_meta } let i = i + 1 } // Find and delete session:messages:SESSION_ID nodes let msg_results: String = engram_search_json("session:messages:" + session_id, 10) let m_total: Int = if str_eq(msg_results, "") { 0 } else { json_array_len(msg_results) } let deleted_msgs: Int = 0 let j: Int = 0 while j < m_total { let node: String = json_array_get(msg_results, j) let label: String = json_get(node, "label") let is_msgs: Bool = str_eq(label, "session:messages:" + session_id) let node_id: String = json_get(node, "id") let deleted_msgs = if is_msgs && !str_eq(node_id, "") { engram_forget(node_id) deleted_msgs + 1 } else { deleted_msgs } let j = j + 1 } // Clear state state_set("session_hist_" + session_id, "") state_set("session_node_" + session_id, "") return "{\"ok\":true,\"session_id\":\"" + session_id + "\"" + ",\"deleted_meta\":" + int_to_str(deleted_meta) + ",\"deleted_msgs\":" + int_to_str(deleted_msgs) + "}" } // session_update_patch — update a session's title and/or folder via PATCH body. // Body may contain "title", "folder", or both. Preserves unmentioned fields. fn session_update_patch(session_id: String, body: String) -> String { if str_eq(session_id, "") { return "{\"error\":\"session_id is required\"}" } let has_title: Bool = str_contains(body, "\"title\"") let has_folder: Bool = str_contains(body, "\"folder\"") if !has_title && !has_folder { return "{\"error\":\"title or folder required in body\"}" } // Find the existing session:meta node. // Use broad label search (not UUID search) because Engram text search // does not reliably match UUID strings with dashes. let results: String = engram_search_json("session:meta", 50) let total: Int = if str_eq(results, "") { 0 } else { json_array_len(results) } let found: Bool = false let old_title: String = "New conversation" let old_folder: String = "" let old_created: String = "0" let old_node_id: String = "" let i: Int = 0 while i < total { let node: String = json_array_get(results, i) let label: String = json_get(node, "label") let content: String = json_get(node, "content") let sid: String = json_get(content, "id") let is_match: Bool = str_eq(label, "session:meta") && str_eq(sid, session_id) && !found let found = if is_match { true } else { found } let title_raw: String = json_get(content, "title") let old_title = if is_match && !str_eq(title_raw, "") { title_raw } else { old_title } let folder_raw: String = json_get(content, "folder") let old_folder = if is_match { folder_raw } else { old_folder } let created_raw: String = json_get(content, "created_at") let old_created = if is_match && !str_eq(created_raw, "") { created_raw } else { old_created } let nid: String = json_get(node, "id") let old_node_id = if is_match { nid } else { old_node_id } let i = i + 1 } if !found { return "{\"error\":\"session not found\",\"session_id\":\"" + session_id + "\"}" } // Apply updates — preserve field if not in body let req_title: String = json_get(body, "title") let eff_title: String = if has_title && !str_eq(req_title, "") { req_title } else { old_title } let eff_folder: String = if has_folder { json_get(body, "folder") } else { old_folder } // Delete old node, create updated one if !str_eq(old_node_id, "") { engram_forget(old_node_id) } let ts: Int = time_now() let created_int: Int = str_to_int(old_created) let new_content: String = session_make_content(session_id, eff_title, created_int, ts, eff_folder) let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]" let new_node_id: String = engram_node_full( new_content, "Conversation", "session:meta", el_from_float(0.7), el_from_float(0.7), el_from_float(0.9), "Episodic", tags ) state_set("session_node_" + session_id, new_node_id) // Invalidate the session_index state cache so session_list re-fetches // from Engram on the next call (the updated node has the new folder/title). state_set("session_index", "") return "{\"ok\":true,\"id\":\"" + session_id + "\"" + ",\"title\":\"" + json_safe(eff_title) + "\"" + ",\"folder\":\"" + json_safe(eff_folder) + "\"" + ",\"updated_at\":" + int_to_str(ts) + "}" } // session_search — search session:meta nodes whose content matches query. fn session_search(query: String) -> String { if str_eq(query, "") { return "[]" } let results: String = engram_search_json("session:meta " + query, 20) if str_eq(results, "") { return "[]" } if str_eq(results, "[]") { return "[]" } let total: Int = json_array_len(results) let out: String = "" let i: Int = 0 while i < total { let node: String = json_array_get(results, i) let label: String = json_get(node, "label") let content: String = json_get(node, "content") let is_session: Bool = str_eq(label, "session:meta") let sess_id: String = json_get(content, "id") let title: String = json_get(content, "title") let created_raw: String = json_get(content, "created_at") let updated_raw: String = json_get(content, "updated_at") let eff_created: String = if str_eq(created_raw, "") { "0" } else { created_raw } let eff_updated: String = if str_eq(updated_raw, "") { eff_created } else { updated_raw } let entry: String = if is_session && !str_eq(sess_id, "") { "{\"id\":\"" + json_safe(sess_id) + "\"" + ",\"title\":\"" + json_safe(title) + "\"" + ",\"created_at\":" + eff_created + ",\"updated_at\":" + eff_updated + "}" } else { "" } let out = if !str_eq(entry, "") { if str_eq(out, "") { entry } else { out + "," + entry } } else { out } let i = i + 1 } return "[" + out + "]" } // session_hist_load — load a session's message history from state or engram. fn session_hist_load(session_id: String) -> String { let state_hist: String = state_get("session_hist_" + session_id) if !str_eq(state_hist, "") { return state_hist } // Try engram fallback let results: String = engram_search_json("session:messages:" + session_id, 3) if str_eq(results, "") { return "" } if str_eq(results, "[]") { return "" } let node: String = json_array_get(results, 0) let label: String = json_get(node, "label") if !str_eq(label, "session:messages:" + session_id) { return "" } let content: String = json_get(node, "content") if str_starts_with(content, "[") { return content } return "" } // session_hist_save — persist message history for a session to state and engram. fn session_hist_save(session_id: String, hist: String) -> Void { state_set("session_hist_" + session_id, hist) // Delete old history node and write fresh one let old_results: String = engram_search_json("session:messages:" + session_id, 3) let o_total: Int = if str_eq(old_results, "") { 0 } else { json_array_len(old_results) } let oi: Int = 0 while oi < o_total { let node: String = json_array_get(old_results, oi) let label: String = json_get(node, "label") let nid: String = json_get(node, "id") if str_eq(label, "session:messages:" + session_id) && !str_eq(nid, "") { engram_forget(nid) } let oi = oi + 1 } let tags: String = "[\"session\",\"session-history\",\"Conversation\"]" let discard: String = engram_node_full( hist, "Conversation", "session:messages:" + session_id, el_from_float(0.6), el_from_float(0.6), el_from_float(0.9), "Episodic", tags ) } // session_update_meta_timestamp — update the updated_at field in the session:meta node. fn session_update_meta_timestamp(session_id: String) -> Void { let results: String = engram_search_json("session:meta " + session_id, 10) let total: Int = if str_eq(results, "") { 0 } else { json_array_len(results) } let found: Bool = false let old_title: String = "New conversation" let old_folder: String = "" let old_created: String = "0" let old_node_id: String = "" let i: Int = 0 while i < total { let node: String = json_array_get(results, i) let label: String = json_get(node, "label") let content: String = json_get(node, "content") let sid: String = json_get(content, "id") let is_match: Bool = str_eq(label, "session:meta") && str_eq(sid, session_id) && !found let found = if is_match { true } else { found } let title_raw: String = json_get(content, "title") let old_title = if is_match && !str_eq(title_raw, "") { title_raw } else { old_title } let folder_raw: String = json_get(content, "folder") let old_folder = if is_match { folder_raw } else { old_folder } let created_raw: String = json_get(content, "created_at") let old_created = if is_match && !str_eq(created_raw, "") { created_raw } else { old_created } let nid: String = json_get(node, "id") let old_node_id = if is_match { nid } else { old_node_id } let i = i + 1 } if !found { return "" } if !str_eq(old_node_id, "") { engram_forget(old_node_id) } let ts: Int = time_now() let created_int: Int = str_to_int(old_created) let new_content: String = session_make_content(session_id, old_title, created_int, ts, old_folder) let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]" let new_id: String = engram_node_full( new_content, "Conversation", "session:meta", el_from_float(0.7), el_from_float(0.7), el_from_float(0.9), "Episodic", tags ) state_set("session_node_" + session_id, new_id) } // session_auto_title — if the session title is still "New conversation", update it // using the first user message. fn session_auto_title(session_id: String, first_message: String) -> Void { let results: String = engram_search_json("session:meta " + session_id, 10) let total: Int = if str_eq(results, "") { 0 } else { json_array_len(results) } let found: Bool = false let cur_title: String = "" let old_folder: String = "" let old_created: String = "0" let old_node_id: String = "" let i: Int = 0 while i < total { let node: String = json_array_get(results, i) let label: String = json_get(node, "label") let content: String = json_get(node, "content") let sid: String = json_get(content, "id") let is_match: Bool = str_eq(label, "session:meta") && str_eq(sid, session_id) && !found let found = if is_match { true } else { found } let title_raw: String = json_get(content, "title") let cur_title = if is_match { title_raw } else { cur_title } let folder_raw: String = json_get(content, "folder") let old_folder = if is_match { folder_raw } else { old_folder } let created_raw: String = json_get(content, "created_at") let old_created = if is_match && !str_eq(created_raw, "") { created_raw } else { old_created } let nid: String = json_get(node, "id") let old_node_id = if is_match { nid } else { old_node_id } let i = i + 1 } if !found { return "" } if !str_eq(cur_title, "New conversation") { return "" } // Update title, preserve folder let new_title: String = session_title_from_message(first_message) if !str_eq(old_node_id, "") { engram_forget(old_node_id) } let ts: Int = time_now() let created_int: Int = str_to_int(old_created) let new_content: String = session_make_content(session_id, new_title, created_int, ts, old_folder) let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]" let new_id: String = engram_node_full( new_content, "Conversation", "session:meta", el_from_float(0.7), el_from_float(0.7), el_from_float(0.9), "Episodic", tags ) state_set("session_node_" + session_id, new_id) } // handle_session_approve — handle tool approval for a pending agentic tool call. // action: "allow" | "deny" | "always" // Resumes the agentic loop from where it was paused. fn handle_session_approve(session_id: String, body: String) -> String { if str_eq(session_id, "") { return "{\"error\":\"session_id is required\"}" } let call_id: String = json_get(body, "call_id") let action: String = json_get(body, "action") if str_eq(call_id, "") { return "{\"error\":\"call_id is required\"}" } if str_eq(action, "") { return "{\"error\":\"action is required (allow|deny|always)\"}" } // Load the pending tool state let pending_raw: String = state_get("pending_tool_" + session_id) if str_eq(pending_raw, "") { return "{\"error\":\"no pending tool for session\",\"session_id\":\"" + session_id + "\"}" } let pending_call_id: String = json_get(pending_raw, "call_id") if !str_eq(pending_call_id, call_id) { return "{\"error\":\"call_id mismatch\",\"expected\":\"" + pending_call_id + "\"}" } let tool_name: String = json_get(pending_raw, "tool_name") let tool_input: String = json_get_raw(pending_raw, "tool_input") let messages: String = json_get_raw(pending_raw, "messages_so_far") let model: String = json_get(pending_raw, "model") let safe_sys: String = json_get(pending_raw, "system") // For "always": add to always-allow list let always_key: String = "always_allow_" + session_id let always_list: String = state_get(always_key) let discard_always: Bool = if str_eq(action, "always") { let new_always: String = if str_eq(always_list, "") { tool_name } else { always_list + "," + tool_name } state_set(always_key, new_always) true } else { false } // Clear pending state state_set("pending_tool_" + session_id, "") let eff_action: String = if str_eq(action, "always") { "allow" } else { action } // Build tool result let tool_result: String = if str_eq(eff_action, "allow") { let raw: String = dispatch_tool(tool_name, tool_input) if str_len(raw) > 6000 { str_slice(raw, 0, 6000) + "...[truncated]" } else { raw } } else { json_safe("{\"error\":\"User denied this tool call\"}") } let tool_msg: String = "{\"type\":\"tool_result\",\"tool_use_id\":\"" + call_id + "\",\"content\":\"" + tool_result + "\"}" // Reconstruct messages with the tool result appended // messages_so_far is the messages array at the point of the tool call // We need to append a user turn with the tool result and re-enter the loop let inner: String = str_slice(messages, 1, str_len(messages) - 1) let resumed_messages: String = "[" + inner + ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}]" // Re-enter the agentic loop with the resumed messages let api_key: String = agentic_api_key() let tools_json: String = agentic_tools_literal() let api_url: String = "https://api.anthropic.com/v1/messages" let h: Map = {} map_set(h, "x-api-key", api_key) map_set(h, "anthropic-version", "2023-06-01") map_set(h, "content-type", "application/json") let final_text: String = "" let tools_log: String = "" let iteration: Int = 0 let keep_going: Bool = true let cur_messages: String = resumed_messages while keep_going && iteration < 8 { let req_body: String = "{\"model\":\"" + model + "\"" + ",\"max_tokens\":4096" + ",\"system\":\"" + safe_sys + "\"" + ",\"tools\":" + tools_json + ",\"messages\":" + cur_messages + "}" let raw_resp: String = http_post_with_headers(api_url, req_body, h) let is_error: Bool = str_starts_with(raw_resp, "{\"error\"") || str_starts_with(raw_resp, "{\"type\":\"error\"") || str_contains(raw_resp, "authentication_error") if is_error { return "{\"error\":\"llm unavailable\",\"reply\":\"\"}" } let stop_reason: String = json_get(raw_resp, "stop_reason") let content_arr: String = json_get_raw(raw_resp, "content") let eff_content: String = if str_eq(content_arr, "") { "[]" } else { content_arr } let text_out: String = "" let has_tool: Bool = false let next_tool_id: String = "" let next_tool_name: String = "" let next_tool_input: String = "" let ci: Int = 0 let c_total: Int = json_array_len(eff_content) while ci < c_total { let block: String = json_array_get(eff_content, ci) let btype: String = json_get(block, "type") let text_out = if str_eq(btype, "text") { text_out + json_get(block, "text") } else { text_out } let is_new_tool: Bool = str_eq(btype, "tool_use") && !has_tool let has_tool = if is_new_tool { true } else { has_tool } let next_tool_id = if is_new_tool { json_get(block, "id") } else { next_tool_id } let next_tool_name = if is_new_tool { json_get(block, "name") } else { next_tool_name } let next_tool_input = if is_new_tool { json_get_raw(block, "input") } else { next_tool_input } let ci = ci + 1 } let is_tool_turn: Bool = str_eq(stop_reason, "tool_use") && has_tool let inner2: String = str_slice(cur_messages, 1, str_len(cur_messages) - 1) // Check if this next tool is in the always-allow list let always_list2: String = state_get(always_key) let is_always: Bool = str_contains(always_list2, next_tool_name) && !str_eq(next_tool_name, "") // For approval-required sessions, pause on tool use if not always-allowed let require_approval: String = state_get("session_require_approval_" + session_id) let needs_pause: Bool = is_tool_turn && str_eq(require_approval, "true") && !is_always let next_tool_result: String = if is_tool_turn && !needs_pause { let raw2: String = dispatch_tool(next_tool_name, next_tool_input) if str_len(raw2) > 6000 { str_slice(raw2, 0, 6000) + "...[truncated]" } else { raw2 } } else { "" } let next_tool_msg: String = "{\"type\":\"tool_result\",\"tool_use_id\":\"" + next_tool_id + "\",\"content\":\"" + next_tool_result + "\"}" let tool_entry: String = "{\"tool\":\"" + next_tool_name + "\",\"input\":\"" + json_safe(next_tool_name) + "\"}" let tools_log = if is_tool_turn && !needs_pause { if str_eq(tools_log, "") { tool_entry } else { tools_log + "," + tool_entry } } else { tools_log } let cur_messages = if is_tool_turn && !needs_pause { "[" + inner2 + ",{\"role\":\"assistant\",\"content\":" + eff_content + "}" + ",{\"role\":\"user\",\"content\":[" + next_tool_msg + "]}" + "]" } else { cur_messages } // Pause if approval needed for next tool let discard_pause: Bool = if needs_pause { let safe_sys2: String = json_safe(safe_sys) let msgs_with_assistant: String = "[" + inner2 + ",{\"role\":\"assistant\",\"content\":" + eff_content + "}]" let pending: String = "{\"call_id\":\"" + next_tool_id + "\"" + ",\"tool_name\":\"" + next_tool_name + "\"" + ",\"tool_input\":" + next_tool_input + ",\"messages_so_far\":" + msgs_with_assistant + ",\"model\":\"" + model + "\"" + ",\"system\":\"" + safe_sys2 + "\"}" state_set("pending_tool_" + session_id, pending) true } else { false } let final_text = if !is_tool_turn { text_out } else { final_text } let keep_going = if !is_tool_turn { false } else { if needs_pause { false } else { keep_going } } let iteration = iteration + 1 } // Check if we paused on a new tool let new_pending: String = state_get("pending_tool_" + session_id) if !str_eq(new_pending, "") { let np_tool_name: String = json_get(new_pending, "tool_name") let np_call_id: String = json_get(new_pending, "call_id") let np_tool_input: String = json_get_raw(new_pending, "tool_input") return "{\"status\":\"tool_pending\"" + ",\"call_id\":\"" + np_call_id + "\"" + ",\"tool_name\":\"" + np_tool_name + "\"" + ",\"tool_input\":" + np_tool_input + ",\"session_id\":\"" + session_id + "\"}" } if str_eq(final_text, "") { return "{\"error\":\"no response after approval\",\"reply\":\"\"}" } // Save updated history let hist: String = session_hist_load(session_id) let updated_hist: String = hist_append(hist, "assistant", final_text) let final_hist: String = if json_array_len(updated_hist) > 20 { hist_trim(updated_hist) } else { updated_hist } session_hist_save(session_id, final_hist) session_update_meta_timestamp(session_id) let safe_text: String = json_safe(final_text) let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" } return "{\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":" + tools_arr + ",\"session_id\":\"" + session_id + "\"}" }