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:
@@ -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, "") {
|
||||
|
||||
Reference in New Issue
Block a user