Fix create_skill wedge: short-circuit single-option navigation, never leave scheduler busy

An empty category produced a one-option SemIf leaf decision; the backend rejects
<2 options, the ValueError unwound past the current-process reset, and every
later request queued forever behind a phantom current. Navigation now
short-circuits empty trees/categories straight to the create branch (no SemIf
decision when there's nothing to choose), and the scheduler clears its current
process even when dispatch raises. Authoring generation is capped at 128 tokens.
This commit is contained in:
Denton Social
2026-09-23 23:41:38 -05:00
parent 225b4df959
commit e68c56dca4
4 changed files with 80 additions and 8 deletions
+27 -1
View File
@@ -137,4 +137,30 @@ def test_generate_skill(tmp_path):
)
print(f"draft: {draft.name!r}{draft.description!r}")
assert isinstance(draft, SkillDraft)
assert draft.name and draft.description
assert draft.name and draft.description
def test_create_skill_empty_category_does_not_wedge(tmp_path):
"""A dispatch that lands on an empty category must not leave the scheduler wedged.
Regression: navigation on a category with no skills produced a single-option
SemIf decision, which the backend rejects; the exception unwound past the
current-process reset, so every later request queued forever behind a phantom
current. The empty category must now short-circuit straight to create_skill.
"""
config = load_config()
require_real(config)
config["log"] = str(tmp_path / "decisions.jsonl")
config["trace"] = str(tmp_path / "runs.jsonl")
scheduler, config = build_scheduler(config)
scheduler.tree["travel_planning"] = []
for _ in range(2):
status, detail = scheduler.submit("look up flights to japan for february")
print(f"[{status}] {detail}")
assert status in ("running", "preempted", "queued", "rejected", "error")
assert scheduler.current is None, "scheduler must never stay wedged after a submit"
status, detail = scheduler.submit("tell me if my package was delivered")
print(f"[{status}] {detail}")
assert scheduler.current is None
+15 -1
View File
@@ -9,9 +9,11 @@ import pytest
from semif_agent.decisions import Request
from semif_agent.engine import EngineConfig, EngineUnavailable, SemIfEngine
from semif_agent.log import DecisionLog
from semif_agent.skills import (
CategoryDraft,
CategoryRegistry,
CreateCategory,
SkillDraft,
build_category_prompt,
build_skill_prompt,
@@ -20,9 +22,11 @@ from semif_agent.skills import (
generate_category,
generate_skill,
merge_registry,
navigate,
parse_category_draft,
parse_skill_draft,
)
from semif_agent.trace import TraceLog
def test_build_category_prompt_contains_request_and_tree():
@@ -177,4 +181,14 @@ def test_merge_registry_loads_categories_and_skills(tmp_path):
merge_registry(tree, registry.read())
names = [s.name for s in tree["delivery"]]
assert names == ["track_live"]
assert tree["delivery"][0].category == "delivery"
assert tree["delivery"][0].category == "delivery"
def test_navigate_empty_tree_short_circuits(tmp_path):
"""An empty tree goes straight to CreateCategory without a SemIf call."""
log = DecisionLog(str(tmp_path / "decisions.jsonl"))
trace = TraceLog(str(tmp_path / "runs.jsonl"))
result = navigate(None, log, trace, Request("anything"), {})
assert isinstance(result, CreateCategory)
assert log.read() == []
assert any(e["kind"] == "create_category" for e in trace.read())