Add plan-mode endpoint to agentic path
Neuron Soul CI / build (pull_request) Has been cancelled

POST /api/chat {mode:'plan'} returns a structured step list from the LLM
without executing any tools. The client can show and edit this plan before
sending the real agentic request.

Implements issue #27 item 2 (Agent panel soul contract — plan mode).
This commit is contained in:
2026-06-28 14:23:32 -05:00
parent a77578e243
commit 9aa8db1362
6 changed files with 186 additions and 90 deletions
+49
View File
@@ -1573,6 +1573,55 @@ fn next_bridge_id() -> String {
return "br-" + uid
}
fn handle_chat_plan(body: String) -> String {
let message: String = json_get(body, "message")
if str_eq(message, "") {
return "{\"error\":\"message required\",\"plan\":null}"
}
let req_model: String = json_get(body, "model")
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
let op_home: String = env("HOME")
let op_user: String = env("USER")
let op_display: String = if str_eq(op_user, "") { "the current user" } else { op_user }
// Compile context same intent-seeding as agentic path so the plan is grounded.
let ctx: String = engram_compile(message)
let ctx_block: String = if str_eq(ctx, "") { "" } else { "\n\n[CONTEXT]\n" + ctx }
let plan_system: String = "You are in PLAN MODE. Your job is to produce a concise step-by-step plan for the request below — WITHOUT executing it.\n\nReturn ONLY a JSON object. No markdown. No preamble. No explanation. Just the JSON:\n{\"steps\":[{\"id\":\"s1\",\"title\":\"<2-6 word title>\",\"detail\":\"<one concrete sentence>\"},{\"id\":\"s2\",...}]}\n\nPlan rules:\n- 3-7 steps (more only when genuinely needed for a complex multi-file task)\n- Each step is one atomic, independently verifiable action\n- title: 2-6 words, imperative (e.g. \"Read config file\", \"Write updated handler\")\n- detail: exactly one sentence describing what happens\n- No tool calls. No execution. No side effects. The user approves before anything runs.\n\nOperator: " + op_display + " at " + op_home + ctx_block
let raw: String = llm_call_system(model, plan_system, message)
let is_error: Bool = str_starts_with(raw, "{\"error\"")
if is_error {
return "{\"error\":\"plan generation failed\",\"plan\":null,\"detail\":" + raw + "}"
}
// Extract the JSON object from the response (LLM sometimes wraps in markdown).
let brace_start: Int = str_index_of(raw, "{")
// Scan backwards to find the last closing brace (str_last_index_of not available).
let brace_end: Int = -1
let scan_i: Int = str_len(raw) - 1
while scan_i >= 0 {
let ch: String = str_slice(raw, scan_i, scan_i + 1)
let brace_end = if str_eq(ch, "}") && brace_end < 0 { scan_i } else { brace_end }
let scan_i = if brace_end >= 0 { -1 } else { scan_i - 1 }
}
let plan_json: String = if brace_start >= 0 {
if brace_end > brace_start {
str_slice(raw, brace_start, brace_end + 1)
} else {
raw
}
} else {
raw
}
return "{\"plan\":" + plan_json + ",\"model\":\"" + json_safe(model) + "\"}"
}
fn handle_chat_agentic(body: String) -> String {
let message: String = json_get(body, "message")
if str_eq(message, "") {