Add LLM-backed planning agent (A4/A11/A13) — step 5a
CI / checks (push) Successful in 12s
CI / checks (pull_request) Successful in 11s

- planner.py: LLMPlanningAgent offers exactly two tools, submit_plan
  and ask_human, so gating stays structural at the tool layer; no
  proceed tool exists. System prompt requires explicit, low-inference
  plans because a weaker model executes them (A13)
- Model output is appended to the canonical log as it arrives, before
  being interpreted, so a crash mid-planning leaves a truthful
  transcript
- A bad turn (prose instead of a tool call, malformed arguments, or a
  failed call) retries once, then raises PlanningError; the
  orchestrator gates to AWAIT_INPUT rather than inventing a plan
- Orchestrator: appends every operator message, plan_submitted, and
  gate_opened to the transcript, so re-planning sees rejection
  feedback as conversation rather than out-of-band parameters; pins
  the A11 role mapping at task start
- llm.py: non-JSON or non-object tool arguments now raise LLMError
  instead of escaping as JSONDecodeError — observed from llama3.2,
  which emits truncated argument strings
- Live planning test is opt-in on NISSE_LIVE_PLANNER_MODEL: planning
  escalates unconditionally (A12) and the local model does not hold
  the tool-call contract, so it awaits a frontier route

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dongarandClaude Opus 5 committed 2026-09-07 14:47:12 +00:00
1 parent ac9ae27bdc
commit cf85648719
7 files changed
+522 -5

No files matched your search

+32
View File
@@ -76,6 +76,38 @@ def test_tool_calls_get_locally_minted_canonical_ids() -> None:
assert event.call_id.startswith("call-")
def test_truncated_tool_arguments_raise_llm_error() -> None:
message = {
"content": None,
"tool_calls": [
{
"id": "c",
"type": "function",
"function": {"name": "submit_plan", "arguments": '{"content": "'},
}
],
}
client = make_client(lambda request: completion(message))
with pytest.raises(LLMError, match="non-JSON arguments"):
client.complete(EVENTS, tools=None, model="local/qwen")
def test_non_object_tool_arguments_raise_llm_error() -> None:
message = {
"content": None,
"tool_calls": [
{
"id": "c",
"type": "function",
"function": {"name": "submit_plan", "arguments": '"just a string"'},
}
],
}
client = make_client(lambda request: completion(message))
with pytest.raises(LLMError, match="expected object"):
client.complete(EVENTS, tools=None, model="local/qwen")
def test_http_error_status_raises_llm_error() -> None:
client = make_client(lambda request: httpx.Response(429, text="rate limited"))
with pytest.raises(LLMError, match="429"):