Replace create_skill branches with category/skill suggestion events

Navigation now offers create_category at the category level and create_skill
at the leaf. Both are stubs that log a suggestion event (state, question,
SemIf output) to the trace instead of returning an authoring sentinel.
This commit is contained in:
Denton Social
2026-09-23 18:24:13 -05:00
parent add860c0bb
commit 147b6cba5f
4 changed files with 48 additions and 15 deletions
+3 -2
View File
@@ -111,8 +111,9 @@ unit tests (24) + box integration tests (2).
validate (accuracy/ECE on a held-out slice, prompt-hash regression), swap the
pinned model revision. GPU offload: train on a beefier GPU; the running agent
keeps a frozen inference revision until a swap validates.
- `create_skill` branch: invoke opencode to author a skill manifest at a tree
leaf (currently a stub that only logs the request).
- `create_skill` / `create_category` branches: navigation logs a suggestion event
(state, query, SemIf output) to the trace — currently a stub; opencode
authoring at a tree leaf is deferred.
- Queue persistence (durable across restarts).
- Event/timer intake sources beyond typed input.
- Concurrency: SemIf shared-state mode (`score_shared` / `SerialPrefixScorer`)
+2 -2
View File
@@ -32,7 +32,7 @@ All decisions are SemIf calls: `{state, question, options[]}`. State is the curr
- **`choice`** — binary: `interrupt` / `defer`. Interrupt iff `P(interrupt) >= τ`.
- **`score`** — ordinal urgency: `critical` / `high` / `medium` / `low`, mapped to numeric weights for sorting.
- **skill navigation** — at each tree level: choose category / descend / `create_skill`.
- **skill navigation** — at each tree level: choose category / descend; the category level offers a `create_category` suggestion and the leaf level a `create_skill` suggestion.
- **`read_next()`** — argument selection within a skill (e.g., which contact is "girlfriend").
**LLM/SemIf boundary**: SemIf for fast, repeated, low-latency decisions (gating, scoring, routing, argument selection). LLM for generation and assessment (email body, self-assessment summary). Never the reverse.
@@ -41,7 +41,7 @@ All decisions are SemIf calls: `{state, question, options[]}`. State is the curr
- Structure: categories → skills → actions. Top level listed at each level.
- Navigation is a chain of SemIf choices, one per level, descending until a leaf skill matches.
- At each level a `create_skill` branch exists: **opencode authors the skill** (its only role) and drops a skill manifest into the registry. The new skill becomes a leaf immediately.
- Navigation offers a `create_category` suggestion at the category level and a `create_skill` suggestion at the leaf level. Both are stubs that log a suggestion event (state, query, SemIf output) to the trace — **opencode authors the skill** (its only role) and drops a skill manifest into the registry, deferred to v2. The new skill becomes a leaf immediately.
### Skill manifest
- name, category, description, allowed inputs, action list, cost budget, decision log reference.
+2 -3
View File
@@ -198,12 +198,11 @@ class Scheduler:
# ---- dispatch ----
def _dispatch(self, request: Request) -> DispatchResult:
navigation = navigate(self.engine, self.log, request, self.tree)
navigation = navigate(self.engine, self.log, self.trace, request, self.tree)
if isinstance(navigation, CreateSkill):
self.trace.append("create_skill", request.id, category=navigation.category)
return DispatchResult(
kind="create_skill",
summary="skill authoring via opencode is deferred to v2; request logged.",
summary="skill authoring via opencode is deferred to v2; suggestion logged.",
)
outcome = self.runner.run(navigation, request)
if outcome.error:
+41 -8
View File
@@ -1,8 +1,9 @@
"""The skill tree, registry, and SemIf-driven navigation.
A skill is a leaf reached by a chain of SemIf choices (category -> skill).
At every level a "create_skill" branch exists; opencode is the authoring tool
there (deferred to v2, stubbed as CreateSkill).
The category level carries a "create_category" branch and the leaf level a
"create_skill" branch; both are stubs that log a suggestion event to the trace
(deferred to v2 — no actual authoring yet).
Only the real skills live here; navigation uses the real decision engine.
"""
@@ -18,6 +19,7 @@ from typing import Callable
from .decisions import DecisionRequest, Option, Request
from .engine import SemIfEngine
from .log import DecisionLog
from .trace import TraceLog
@dataclass
@@ -56,7 +58,12 @@ class Skill:
@dataclass
class CreateSkill:
"""Sentinel for the 'create a missing skill' branch at a tree level."""
"""Stub for a missing-category/skill suggestion at a tree level.
Navigation logs the suggestion event to the trace; actual authoring is
deferred to v2. `category` is None for a new-category suggestion, else the
category that needs the new skill.
"""
category: str | None = None
@@ -162,32 +169,58 @@ def build_tree(skills: list[Skill]) -> dict[str, list[Skill]]:
def navigate(
engine: SemIfEngine,
log: DecisionLog,
trace: TraceLog,
request: Request,
tree: dict[str, list[Skill]],
) -> Skill | CreateSkill:
"""Descend the tree one SemIf choice per level. Every choice is logged."""
"""Descend the tree one SemIf choice per level. Every choice is logged.
The category level offers a "create_category" branch and the leaf level a
"create_skill" branch; both log a suggestion event to the trace and return
a CreateSkill stub (actual authoring is deferred to v2).
"""
categories = sorted(tree.keys())
create = Option("create_skill", "Create a new skill for this.")
create_category = Option("create_category", "Suggest a new category for this.")
top = DecisionRequest(
state=compose_state(request),
question="Which top-level category handles this request?",
options=[Option(c, c) for c in categories] + [create],
options=[Option(c, c) for c in categories] + [create_category],
)
top_result = engine.call(top)
log.append(top, top_result, extra={"phase": "navigate:category", "run_id": request.id})
category = top_result.selected
if category == "create_skill":
if category == "create_category":
trace.append(
"create_category",
request.id,
state=top.state,
question=top.question,
options=[o.id for o in top.options],
selected=top_result.selected,
probs=top_result.probs,
)
return CreateSkill(category=None)
skills = tree[category]
create_skill = Option("create_skill", "Suggest creating a new skill.")
leaf = DecisionRequest(
state=compose_state(request, current=category),
question=f"Within {category}, which skill?",
options=[Option(s.name, s.description) for s in skills] + [create],
options=[Option(s.name, s.description) for s in skills] + [create_skill],
)
leaf_result = engine.call(leaf)
log.append(leaf, leaf_result, extra={"phase": "navigate:leaf", "run_id": request.id})
pick = leaf_result.selected
if pick == "create_skill":
trace.append(
"skill_needed",
request.id,
category=category,
state=leaf.state,
question=leaf.question,
options=[o.id for o in leaf.options],
selected=leaf_result.selected,
probs=leaf_result.probs,
)
return CreateSkill(category=category)
return next(s for s in skills if s.name == pick)