From 147b6cba5fecd4d80f02ebd6c0220ef09a38539d Mon Sep 17 00:00:00 2001 From: Denton Social Date: Wed, 23 Sep 2026 18:24:13 -0500 Subject: [PATCH] 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. --- AGENTS.md | 5 ++-- IDEA.md | 4 ++-- semif_agent/scheduler.py | 5 ++-- semif_agent/skills.py | 49 +++++++++++++++++++++++++++++++++------- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a863eec..7d0384d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`) diff --git a/IDEA.md b/IDEA.md index fffc0dd..ff91900 100644 --- a/IDEA.md +++ b/IDEA.md @@ -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. diff --git a/semif_agent/scheduler.py b/semif_agent/scheduler.py index 74fab93..5f464f4 100644 --- a/semif_agent/scheduler.py +++ b/semif_agent/scheduler.py @@ -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: diff --git a/semif_agent/skills.py b/semif_agent/skills.py index 4584194..c3c4a6b 100644 --- a/semif_agent/skills.py +++ b/semif_agent/skills.py @@ -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)