fix: rewrite 'no LLM provider' stderr to stop the capability-constraint misread

PR #285 introduced the stderr warning "No --plan and no LLM provider
configured. Using deterministic fallback..." The 2026-04-19 Run 1
agent self-debug said it read that as "I don't have a key, I can't do
LLM stuff, I have to accept fallback" - which is the exact wrong
mental model. The word "provider" referred to the engine's INTERNAL
planner credentials, but the agent parsed it as "I need credentials
to plan at all."

Rewritten to say plainly: YOU are the reasoning model hosting this
skill (Claude Code, Codex, Hermes, Gemini, or any agent runtime);
YOU ARE the planner; you do not need an API key or credentials - you
ARE the LLM. The --plan flag exists precisely so a reasoning model
generates its own plan upstream and passes it to the engine. The
deterministic fallback is the headless/cron path only.

Runtime enumeration is explicit so agents on every supported runtime
recognize themselves - this skill ships to Claude Code, Codex, Hermes,
and ~/.agents via sync.sh.

Tests: updated test_fallback_logs_warning_when_no_provider to assert
the new language (YOU ARE the planner, runtime names present) and
assert the old misleading phrasing is absent. Renamed the companion
test for clarity.
This commit is contained in:
Matt Van Horn
2026-04-19 10:28:48 -07:00
parent a7d6ef051a
commit 4388fed46a
2 changed files with 32 additions and 18 deletions
+15 -9
View File
@@ -113,17 +113,23 @@ def plan_query(
topic, available_sources, requested_sources, depth, topic, available_sources, requested_sources, depth,
note=f"fallback-plan (LLM error: {type(exc).__name__})", note=f"fallback-plan (LLM error: {type(exc).__name__})",
) )
# No provider configured and no --plan was passed. The deterministic # No --plan was passed and no engine-internal provider is configured.
# fallback path produces weaker plans than the LLM or the --plan path # The deterministic fallback path produces weaker plans than either the
# (narrower subquery breadth, no semantic expansion). Surface that # --plan path or the engine-internal LLM planner path. Surface this so
# so Claude Code callers know to pass --plan with a JSON plan they # the HOSTING reasoning model (Claude Code, Codex, Hermes, Gemini, etc.)
# generate themselves. See 2026-04-19 Hermes Agent Use Cases failure. # realizes it is supposed to generate the plan itself and pass --plan.
# The word "provider" in engine messages refers to the engine's INTERNAL
# planner credentials - NOT a prerequisite the caller needs. If you are
# the hosting reasoning model, YOU are the provider. LAW 7 / 2026-04-19
# Hermes Agent Use Cases failure mode.
import sys import sys
print( print(
"[Planner] No --plan and no LLM provider configured. " "[Planner] No --plan passed. If you are the reasoning model hosting "
"Using deterministic fallback; results will be weaker than the " "this skill (Claude Code, Codex, Hermes, Gemini, or any agent runtime), "
"LLM-planned path. If invoked from Claude Code, generate a JSON " "YOU ARE the planner: generate a JSON query plan yourself and pass it "
"query plan and pass --plan.", "via --plan. You do not need an API key or credentials; you ARE the "
"LLM. The deterministic fallback below is the headless/cron path only. "
"See LAW 7 in SKILL.md and Step 0.75 for the plan schema.",
file=sys.stderr, file=sys.stderr,
) )
return _fallback_plan(topic, available_sources, requested_sources, depth) return _fallback_plan(topic, available_sources, requested_sources, depth)
+17 -9
View File
@@ -413,14 +413,24 @@ class FallbackDefaultsTests(unittest.TestCase):
provider=None, provider=None,
model=None, model=None,
) )
self.assertIn("No --plan and no LLM provider configured", buf.getvalue()) output = buf.getvalue()
# New language: "No --plan passed" + "YOU ARE the planner" +
# runtime enumeration. Unit 4 (2026-04-19) rewrite to stop the
# "no provider = no LLM = I need a key" misread.
self.assertIn("No --plan passed", output)
self.assertIn("YOU ARE the planner", output)
self.assertIn("you ARE the LLM", output)
# Runtime-agnostic: each supported runtime name should appear.
for runtime_name in ("Claude Code", "Codex", "Hermes", "Gemini"):
self.assertIn(runtime_name, output)
# The old misleading phrasing must NOT appear.
self.assertNotIn("No --plan and no LLM provider configured", output)
def test_fallback_does_not_log_warning_when_provider_present(self): def test_fallback_does_not_log_new_warning_when_provider_present(self):
# When a provider is configured, the provider path runs; if it # When a provider is configured, the provider path runs; if it
# succeeds, no fallback warning should appear. # errors, we get the "LLM planning failed" message, NOT the
# (The existing sanitize tests cover this; we just confirm the # "No --plan passed" guidance (which is specifically for the
# warning string gating is on provider-presence, not on fallback # no-provider-no-plan caller path).
# activation.)
import io import io
import contextlib import contextlib
buf = io.StringIO() buf = io.StringIO()
@@ -438,11 +448,9 @@ class FallbackDefaultsTests(unittest.TestCase):
provider=_NoopProvider(), provider=_NoopProvider(),
model="some-model", model="some-model",
) )
# Provider was present — we expect the "LLM planning failed" message,
# NOT the "No --plan and no LLM provider" message.
output = buf.getvalue() output = buf.getvalue()
self.assertIn("LLM planning failed", output) self.assertIn("LLM planning failed", output)
self.assertNotIn("No --plan and no LLM provider configured", output) self.assertNotIn("No --plan passed", output)
if __name__ == "__main__": if __name__ == "__main__":