Add LLM-backed planning agent (A4/A11/A13) — step 5a
- 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:
1 parent
ac9ae27bdc
commit
cf85648719
7 files changed
+522
-5
No files matched your search
@@ -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"):
|
||||
|
||||
Reference in new issue
Block a user